From 3195c2a9827a02da70938ee67c1aa60e2e9afaef Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Fri, 8 May 2026 15:41:11 -0400 Subject: [PATCH 01/65] chore Co-authored-by: Copilot --- .../src/charmlibs/interfaces/otlp/_rules.py | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/interfaces/otlp/src/charmlibs/interfaces/otlp/_rules.py b/interfaces/otlp/src/charmlibs/interfaces/otlp/_rules.py index 9956969e2..275dbe89f 100644 --- a/interfaces/otlp/src/charmlibs/interfaces/otlp/_rules.py +++ b/interfaces/otlp/src/charmlibs/interfaces/otlp/_rules.py @@ -21,8 +21,8 @@ from pathlib import Path from cosl.juju_topology import JujuTopology -from cosl.rules import HOST_METRICS_MISSING_RULE_NAME, CosTool, Rules, generic_alert_groups -from cosl.types import OfficialRuleFileFormat, SingleRuleFormat +from cosl.rules import HOST_METRICS_MISSING_RULE_NAME, CosTool, Rules, SigmaRules, generic_alert_groups +from cosl.types import OfficialRuleFileFormat, SigmaRuleFileFormat, SingleRuleFormat from ops import CharmBase from pydantic import BaseModel, Field @@ -43,10 +43,12 @@ class RuleStore: topology: JujuTopology logql: Rules = field(init=False) promql: Rules = field(init=False) + sigma: SigmaRules = field(init=False) def __post_init__(self): self.logql = Rules(query_type='logql', topology=self.topology) self.promql = Rules(query_type='promql', topology=self.topology) + self.sigma = SigmaRules(query_type='sigma', topology=self.topology) def add_logql( self, @@ -110,12 +112,36 @@ def add_promql_path(self, dir_path: str | Path, *, recursive: bool = False) -> ' self.promql.add_path(dir_path, recursive=recursive) return self + def add_sigma(self, rule_dict: OfficialRuleFileFormat | SingleRuleFormat) -> 'RuleStore': + """Add rules from dict to the existing Sigma ruleset. + + Args: + rule_dict: a single-rule or official-rule YAML dict + """ + self.sigma.add(rule_dict) + return self + + def add_sigma_path(self, dir_path: str | Path, *, recursive: bool = False) -> 'RuleStore': + """Add Sigma rules from a dir path. + + All rules from files are aggregated into a data structure representing a single rule file. + All group names are augmented with juju topology. + + Args: + dir_path: either a rules file or a dir of rules files. + recursive: whether to read files recursively or not (no impact if `path` is a file). + """ + self.sigma.add_path(dir_path, recursive=recursive) + return self + def combine(self, other: 'RuleStore') -> 'RuleStore': """Combine rules from another RuleStore with this RuleStore.""" if other_logql := other.logql.as_dict(): self.logql.add(other_logql) if other_promql := other.promql.as_dict(): self.promql.add(other_promql) + if other_sigma := other.sigma.as_dict(): + self.sigma.add(other_sigma) return self @@ -132,6 +158,11 @@ class _RulesModel(BaseModel): 'OfficialRuleFileFormat from cos-lib.', default_factory=OfficialRuleFileFormat, ) + sigma: SigmaRuleFileFormat = Field( + description='Sigma detection rules, following the ' + 'SigmaRuleFileFormat from cos-lib.', + default_factory=SigmaRuleFileFormat, + ) def duplicate_rules_per_unit( @@ -235,4 +266,6 @@ def inject_extra_labels_into_rules( for group in rule_groups.get('groups', []): for rule in group.get('rules', []): rule.setdefault('labels', {}).update(extra_alert_labels) + for sigma_rule in rules_copy.sigma.as_dict().get('rules', []): + sigma_rule.setdefault('labels', {}).update(extra_alert_labels) return rules_copy From df9c6b8cbd82f5fdcc4d8135d117567f33cc9904 Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Fri, 8 May 2026 16:45:53 -0400 Subject: [PATCH 02/65] chore Co-authored-by: Copilot --- interfaces/otlp/pyproject.toml | 2 +- .../src/charmlibs/interfaces/otlp/_rules.py | 25 +++++++++++++------ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/interfaces/otlp/pyproject.toml b/interfaces/otlp/pyproject.toml index eeff2bb23..7fb3cf257 100644 --- a/interfaces/otlp/pyproject.toml +++ b/interfaces/otlp/pyproject.toml @@ -16,7 +16,7 @@ classifiers = [ dynamic = ["version"] dependencies = [ # "ops", - "cosl>=1.6.1", + "cosl @ git+https://github.com/canonical/cos-lib@feat/sigma-rules-3", "requests", ] diff --git a/interfaces/otlp/src/charmlibs/interfaces/otlp/_rules.py b/interfaces/otlp/src/charmlibs/interfaces/otlp/_rules.py index 275dbe89f..0cd516031 100644 --- a/interfaces/otlp/src/charmlibs/interfaces/otlp/_rules.py +++ b/interfaces/otlp/src/charmlibs/interfaces/otlp/_rules.py @@ -21,8 +21,19 @@ from pathlib import Path from cosl.juju_topology import JujuTopology -from cosl.rules import HOST_METRICS_MISSING_RULE_NAME, CosTool, Rules, SigmaRules, generic_alert_groups -from cosl.types import OfficialRuleFileFormat, SigmaRuleFileFormat, SingleRuleFormat +from cosl.rules import ( + HOST_METRICS_MISSING_RULE_NAME, + CosTool, + Rules, + SigmaRules, + generic_alert_groups, +) +from cosl.types import ( + OfficialRuleFileFormat, + SigmaRuleFileFormat, + SigmaRuleFormat, + SingleRuleFormat, +) from ops import CharmBase from pydantic import BaseModel, Field @@ -48,7 +59,7 @@ class RuleStore: def __post_init__(self): self.logql = Rules(query_type='logql', topology=self.topology) self.promql = Rules(query_type='promql', topology=self.topology) - self.sigma = SigmaRules(query_type='sigma', topology=self.topology) + self.sigma = SigmaRules(topology=self.topology) def add_logql( self, @@ -112,11 +123,11 @@ def add_promql_path(self, dir_path: str | Path, *, recursive: bool = False) -> ' self.promql.add_path(dir_path, recursive=recursive) return self - def add_sigma(self, rule_dict: OfficialRuleFileFormat | SingleRuleFormat) -> 'RuleStore': + def add_sigma(self, rule_dict: SigmaRuleFileFormat | SigmaRuleFormat) -> 'RuleStore': """Add rules from dict to the existing Sigma ruleset. Args: - rule_dict: a single-rule or official-rule YAML dict + rule_dict: a single sigma rule dict or a ``{"rules": [...]}`` collection. """ self.sigma.add(rule_dict) return self @@ -124,8 +135,8 @@ def add_sigma(self, rule_dict: OfficialRuleFileFormat | SingleRuleFormat) -> 'Ru def add_sigma_path(self, dir_path: str | Path, *, recursive: bool = False) -> 'RuleStore': """Add Sigma rules from a dir path. - All rules from files are aggregated into a data structure representing a single rule file. - All group names are augmented with juju topology. + All rules from files are aggregated into a single collection. + Topology labels are injected into each rule. Args: dir_path: either a rules file or a dir of rules files. From 9c85e89feb44650b85fd2c967ba950a29f382af4 Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Fri, 8 May 2026 17:07:38 -0400 Subject: [PATCH 03/65] feat: Sigma rules pt.2 Co-authored-by: Copilot --- interfaces/otlp/pyproject.toml | 3 + .../src/charmlibs/interfaces/otlp/_otlp.py | 4 + interfaces/otlp/tests/unit/conftest.py | 29 ++- interfaces/otlp/tests/unit/test_rules.py | 181 ++++++++++++++++++ interfaces/otlp/uv.lock | 10 +- 5 files changed, 219 insertions(+), 8 deletions(-) diff --git a/interfaces/otlp/pyproject.toml b/interfaces/otlp/pyproject.toml index 7fb3cf257..b422a50e0 100644 --- a/interfaces/otlp/pyproject.toml +++ b/interfaces/otlp/pyproject.toml @@ -42,6 +42,9 @@ integration = [ # installed for `just integration otlp` requires = ["hatchling"] build-backend = "hatchling.build" +[tool.hatch.metadata] +allow-direct-references = true + [tool.hatch.build.targets.wheel] packages = ["src/charmlibs"] diff --git a/interfaces/otlp/src/charmlibs/interfaces/otlp/_otlp.py b/interfaces/otlp/src/charmlibs/interfaces/otlp/_otlp.py index 7d849f24b..d11d532d9 100644 --- a/interfaces/otlp/src/charmlibs/interfaces/otlp/_otlp.py +++ b/interfaces/otlp/src/charmlibs/interfaces/otlp/_otlp.py @@ -218,6 +218,7 @@ def publish(self): 'rules': { 'logql': self._rules.logql.as_dict(), 'promql': self._rules.promql.as_dict(), + 'sigma': self._rules.sigma.as_dict(), }, 'metadata': self._topology.as_dict(), }) @@ -337,6 +338,9 @@ def rules(self) -> dict[int, RuleStore]: rules.logql.add(logql_result.rules) if promql_result.rules and not promql_result.errmsg: rules.promql.add(promql_result.rules) + # Sigma rules: inject topology labels (no expression rewriting needed) + if sigma_dict := requirer.rules.sigma: + rules.sigma.add(sigma_dict) for errmsg in [logql_result.errmsg, promql_result.errmsg]: if errmsg and self._charm.unit.is_leader(): relation.data[self._charm.app]['event'] = json.dumps({'errors': errmsg}) diff --git a/interfaces/otlp/tests/unit/conftest.py b/interfaces/otlp/tests/unit/conftest.py index 2a94efdc3..49742a3c5 100644 --- a/interfaces/otlp/tests/unit/conftest.py +++ b/interfaces/otlp/tests/unit/conftest.py @@ -25,7 +25,12 @@ import ops import pytest from cosl.juju_topology import JujuTopology -from cosl.types import AlertingRuleFormat, OfficialRuleFileFormat, RecordingRuleFormat +from cosl.types import ( + AlertingRuleFormat, + OfficialRuleFileFormat, + RecordingRuleFormat, + SigmaRuleFormat, +) from ops import testing from ops.charm import CharmBase @@ -74,6 +79,26 @@ }, ] ) +SINGLE_SIGMA_RULE: SigmaRuleFormat = { + 'title': 'Failed SSH Login Attempt', + 'id': '5f3a4e20-1b2c-4d5e-9f8a-7b6c3d4e5f6a', + 'status': 'experimental', + 'logsource': {'category': 'authentication', 'product': 'linux', 'service': 'sshd'}, + 'detection': {'selection': {'event_type': 'authentication_failure'}, 'condition': 'selection'}, + 'level': 'medium', +} +SIGMA_COLLECTION: dict = { + 'rules': [ + SINGLE_SIGMA_RULE, + { + 'title': 'High CPU Usage', + 'logsource': {'category': 'process_monitoring', 'product': 'linux'}, + 'detection': {'selection': {'metric': 'cpu_percent'}, 'condition': 'selection'}, + 'level': 'high', + }, + ] +} + ALL_PROTOCOLS: list[Literal['grpc', 'http']] = ['grpc', 'http'] ALL_TELEMETRIES: list[Literal['logs', 'metrics', 'traces']] = ['logs', 'metrics', 'traces'] @@ -102,6 +127,8 @@ def _publish_rules(self, _: ops.EventBase) -> None: .add_promql(deepcopy(SINGLE_PROMQL_RECORD), group_name='test_promql_record') .add_logql(deepcopy(OFFICIAL_LOGQL_RULES)) .add_promql(deepcopy(OFFICIAL_PROMQL_RULES)) + .add_sigma(deepcopy(SINGLE_SIGMA_RULE)) + .add_sigma(deepcopy(SIGMA_COLLECTION)) ) OtlpRequirer( self, diff --git a/interfaces/otlp/tests/unit/test_rules.py b/interfaces/otlp/tests/unit/test_rules.py index d7b9e27d5..74e196c08 100644 --- a/interfaces/otlp/tests/unit/test_rules.py +++ b/interfaces/otlp/tests/unit/test_rules.py @@ -26,10 +26,12 @@ from charmlibs.interfaces.otlp._rules import _RulesModel, duplicate_rules_per_unit from conftest import ( PEERS_ENDPOINT, + SIGMA_COLLECTION, SINGLE_LOGQL_ALERT, SINGLE_LOGQL_RECORD, SINGLE_PROMQL_ALERT, SINGLE_PROMQL_RECORD, + SINGLE_SIGMA_RULE, ) MODEL_NAME = 'foo-model' @@ -423,3 +425,182 @@ def test_rulestore_combine_returns_self(): # THEN combine returns the target (self) for chaining assert result is target + + +# --- Sigma rules --- + + +def test_rulestore_add_single_sigma_rule(): + # GIVEN a RuleStore + store = _make_store() + + # WHEN a single sigma rule is added + store.add_sigma(SINGLE_SIGMA_RULE) + + # THEN the sigma collection contains 1 rule + sigma = store.sigma.as_dict() + assert len(sigma.get('rules', [])) == 1 + # AND topology labels are injected + labels = sigma['rules'][0].get('labels', {}) + assert labels['juju_model'] == MODEL_NAME + assert labels['juju_application'] == 'test-app' + + +def test_rulestore_add_sigma_collection(): + # GIVEN a RuleStore + store = _make_store() + + # WHEN a sigma collection is added + store.add_sigma(SIGMA_COLLECTION) + + # THEN the sigma collection contains 2 rules + sigma = store.sigma.as_dict() + assert len(sigma.get('rules', [])) == 2 + titles = {r['title'] for r in sigma['rules']} + assert titles == {'Failed SSH Login Attempt', 'High CPU Usage'} + + +def test_rulestore_add_sigma_returns_self(): + # GIVEN a RuleStore + store = _make_store() + + # WHEN add_sigma is called + result = store.add_sigma(SINGLE_SIGMA_RULE) + + # THEN it returns self for chaining + assert result is store + + +def test_rulestore_combine_sigma(): + # GIVEN a source RuleStore with sigma rules + source = _make_store().add_sigma(SINGLE_SIGMA_RULE) + # AND an empty target RuleStore + target = _make_store() + + # WHEN combined + target.combine(source) + + # THEN the target now contains the sigma rules + sigma = target.sigma.as_dict() + assert len(sigma.get('rules', [])) == 1 + assert sigma['rules'][0]['title'] == 'Failed SSH Login Attempt' + + +def test_rulestore_combine_empty_sigma_does_not_clear_target(): + # GIVEN a target RuleStore with sigma rules + target = _make_store().add_sigma(SINGLE_SIGMA_RULE) + # AND an empty source RuleStore + source = _make_store() + + # WHEN combined + target.combine(source) + + # THEN the target sigma rules are unchanged + assert len(target.sigma.as_dict().get('rules', [])) == 1 + + +def test_sigma_rules_in_databag(otlp_requirer_ctx: testing.Context[ops.CharmBase]): + # GIVEN a send-otlp relation + state = State(relations=[Relation(SEND)], leader=True, model=MODEL) + + # WHEN the update_status event is fired + state_out = otlp_requirer_ctx.run(otlp_requirer_ctx.on.update_status(), state=state) + for relation in list(state_out.relations): + # THEN the sigma rules appear in the compressed databag + decompressed = _decompress(relation.local_app_data.get('rules')) + assert decompressed + sigma_rules = decompressed.get('sigma', {}).get('rules', []) + # 1 single rule + 2 from collection = 3 + assert len(sigma_rules) == 3 + + +def test_sigma_topology_in_databag(otlp_requirer_ctx: testing.Context[ops.CharmBase]): + # GIVEN a send-otlp relation + state = State(relations=[Relation(SEND)], leader=True, model=MODEL) + + # WHEN the update_status event is fired + state_out = otlp_requirer_ctx.run(otlp_requirer_ctx.on.update_status(), state=state) + for relation in list(state_out.relations): + decompressed = _decompress(relation.local_app_data.get('rules')) + sigma_rules = decompressed.get('sigma', {}).get('rules', []) + for rule in sigma_rules: + # THEN each sigma rule has topology labels + labels = rule.get('labels', {}) + assert labels.get('juju_model') == MODEL_NAME + assert labels.get('juju_application') == 'otlp-requirer' + + +def test_provider_sigma_rules(otlp_provider_ctx: testing.Context[ops.CharmBase]): + # GIVEN a requirer offers sigma rules in the databag + rules = { + 'logql': {}, + 'promql': {}, + 'sigma': { + 'rules': [SINGLE_SIGMA_RULE], + }, + } + metadata = { + 'model': MODEL_NAME, + 'model_uuid': MODEL_UUID, + 'application': 'otlp-requirer', + 'charm_name': 'otlp-requirer', + 'unit': 'otlp-requirer/0', + } + receiver = Relation( + RECEIVE, remote_app_data={'rules': json.dumps(rules), 'metadata': json.dumps(metadata)} + ) + state = State(leader=True, relations=[receiver], model=MODEL) + with otlp_provider_ctx(otlp_provider_ctx.on.update_status(), state=state) as mgr: + # WHEN the provider aggregates the rules from the databag + rule_store = OtlpProvider(mgr.charm, RECEIVE).rules[receiver.id] + sigma = rule_store.sigma.as_dict() + + # THEN the sigma rules exist in the RuleStore + assert len(sigma.get('rules', [])) == 1 + rule = sigma['rules'][0] + assert rule['title'] == 'Failed SSH Login Attempt' + + # AND the rule has topology labels from the provider + labels = rule.get('labels', {}) + assert labels.get('juju_model') == MODEL_NAME + assert labels.get('juju_model_uuid') == MODEL_UUID + assert labels.get('juju_application') == 'otlp-provider' + + +@pytest.mark.parametrize( + 'otlp_requirer_ctx,expected_labels', + [ + pytest.param( + {'aggregator': True, 'extra_alert_labels': {'env': 'prod'}}, + {'env': 'prod'}, + id='single-label', + ), + pytest.param( + {'aggregator': True, 'extra_alert_labels': {'env': 'prod', 'team': 'core'}}, + {'env': 'prod', 'team': 'core'}, + id='multiple-labels', + ), + ], + indirect=['otlp_requirer_ctx'], +) +def test_extra_alert_labels_injected_into_sigma( + otlp_requirer_ctx: testing.Context[ops.CharmBase], expected_labels: dict[str, str] +): + # GIVEN a send-otlp relation with peers + peers = PeerRelation(endpoint=PEERS_ENDPOINT) + state = State(relations=[peers, Relation(SEND)], leader=True, model=MODEL) + + # WHEN the update_status event is fired + state_out = otlp_requirer_ctx.run(otlp_requirer_ctx.on.update_status(), state=state) + for relation in list(state_out.relations): + if relation.endpoint != SEND: + continue + decompressed = _decompress(relation.local_app_data.get('rules')) + sigma_rules = decompressed.get('sigma', {}).get('rules', []) + assert sigma_rules + + # THEN each sigma rule has the extra alert labels + for rule in sigma_rules: + labels = rule.get('labels', {}) + for k, v in expected_labels.items(): + assert labels.get(k) == v diff --git a/interfaces/otlp/uv.lock b/interfaces/otlp/uv.lock index dcfd08de9..858a9456f 100644 --- a/interfaces/otlp/uv.lock +++ b/interfaces/otlp/uv.lock @@ -39,7 +39,7 @@ unit = [ [package.metadata] requires-dist = [ - { name = "cosl", specifier = ">=1.6.1" }, + { name = "cosl", git = "https://github.com/canonical/cos-lib?rev=feat%2Fsigma-rules-3" }, { name = "requests" }, ] @@ -152,8 +152,8 @@ wheels = [ [[package]] name = "cosl" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } +version = "1.9.1" +source = { git = "https://github.com/canonical/cos-lib?rev=feat%2Fsigma-rules-3#b9dd4e92e4c36bbc6504ee76a9605f5714343d24" } dependencies = [ { name = "ops" }, { name = "pydantic" }, @@ -161,10 +161,6 @@ dependencies = [ { name = "tenacity" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/21/96/bf85615902a6cc36bfe1c72b1dbc4e97076c6e05a28fa675a124146fc67c/cosl-1.9.0.tar.gz", hash = "sha256:360491aa2b6d37be36cefbeb31855aea19bf61ce36d6078dc1d09a51b5eefa92", size = 149850, upload-time = "2026-03-30T20:42:17.642Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/68/7e/65837d35b2dab96b70fe55aa4a4b4fb7d2cd902e87c6074493392de769d3/cosl-1.9.0-py3-none-any.whl", hash = "sha256:026700e712cae75a5db038710997b0ffa27612fddc736a5a23d94111dec1f533", size = 37936, upload-time = "2026-03-30T20:42:16.487Z" }, -] [[package]] name = "exceptiongroup" From 4ac40cba866d12e6fc245294bbc639f26b96eadc Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Fri, 8 May 2026 17:11:29 -0400 Subject: [PATCH 04/65] chore --- interfaces/otlp/src/charmlibs/interfaces/otlp/_rules.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/interfaces/otlp/src/charmlibs/interfaces/otlp/_rules.py b/interfaces/otlp/src/charmlibs/interfaces/otlp/_rules.py index 0cd516031..a4add560c 100644 --- a/interfaces/otlp/src/charmlibs/interfaces/otlp/_rules.py +++ b/interfaces/otlp/src/charmlibs/interfaces/otlp/_rules.py @@ -170,8 +170,7 @@ class _RulesModel(BaseModel): default_factory=OfficialRuleFileFormat, ) sigma: SigmaRuleFileFormat = Field( - description='Sigma detection rules, following the ' - 'SigmaRuleFileFormat from cos-lib.', + description='Sigma detection rules, following the SigmaRuleFileFormat from cos-lib.', default_factory=SigmaRuleFileFormat, ) From 671867084abb0e4d04ee59066404505d63c241f0 Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Fri, 8 May 2026 17:13:08 -0400 Subject: [PATCH 05/65] chore --- interfaces/otlp/pyproject.toml | 3 +++ interfaces/otlp/src/charmlibs/interfaces/otlp/_rules.py | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/interfaces/otlp/pyproject.toml b/interfaces/otlp/pyproject.toml index 7fb3cf257..b422a50e0 100644 --- a/interfaces/otlp/pyproject.toml +++ b/interfaces/otlp/pyproject.toml @@ -42,6 +42,9 @@ integration = [ # installed for `just integration otlp` requires = ["hatchling"] build-backend = "hatchling.build" +[tool.hatch.metadata] +allow-direct-references = true + [tool.hatch.build.targets.wheel] packages = ["src/charmlibs"] diff --git a/interfaces/otlp/src/charmlibs/interfaces/otlp/_rules.py b/interfaces/otlp/src/charmlibs/interfaces/otlp/_rules.py index 0cd516031..a4add560c 100644 --- a/interfaces/otlp/src/charmlibs/interfaces/otlp/_rules.py +++ b/interfaces/otlp/src/charmlibs/interfaces/otlp/_rules.py @@ -170,8 +170,7 @@ class _RulesModel(BaseModel): default_factory=OfficialRuleFileFormat, ) sigma: SigmaRuleFileFormat = Field( - description='Sigma detection rules, following the ' - 'SigmaRuleFileFormat from cos-lib.', + description='Sigma detection rules, following the SigmaRuleFileFormat from cos-lib.', default_factory=SigmaRuleFileFormat, ) From c03c5302aba2bd4e095f7e1e172f389ba326d32a Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Fri, 8 May 2026 17:15:34 -0400 Subject: [PATCH 06/65] chore --- .../otlp/src/charmlibs/interfaces/otlp/_version.py | 2 +- interfaces/otlp/uv.lock | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/interfaces/otlp/src/charmlibs/interfaces/otlp/_version.py b/interfaces/otlp/src/charmlibs/interfaces/otlp/_version.py index 8bf75cb8c..79460eca5 100644 --- a/interfaces/otlp/src/charmlibs/interfaces/otlp/_version.py +++ b/interfaces/otlp/src/charmlibs/interfaces/otlp/_version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = '0.5.0' +__version__ = '0.6.0' diff --git a/interfaces/otlp/uv.lock b/interfaces/otlp/uv.lock index dcfd08de9..858a9456f 100644 --- a/interfaces/otlp/uv.lock +++ b/interfaces/otlp/uv.lock @@ -39,7 +39,7 @@ unit = [ [package.metadata] requires-dist = [ - { name = "cosl", specifier = ">=1.6.1" }, + { name = "cosl", git = "https://github.com/canonical/cos-lib?rev=feat%2Fsigma-rules-3" }, { name = "requests" }, ] @@ -152,8 +152,8 @@ wheels = [ [[package]] name = "cosl" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } +version = "1.9.1" +source = { git = "https://github.com/canonical/cos-lib?rev=feat%2Fsigma-rules-3#b9dd4e92e4c36bbc6504ee76a9605f5714343d24" } dependencies = [ { name = "ops" }, { name = "pydantic" }, @@ -161,10 +161,6 @@ dependencies = [ { name = "tenacity" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/21/96/bf85615902a6cc36bfe1c72b1dbc4e97076c6e05a28fa675a124146fc67c/cosl-1.9.0.tar.gz", hash = "sha256:360491aa2b6d37be36cefbeb31855aea19bf61ce36d6078dc1d09a51b5eefa92", size = 149850, upload-time = "2026-03-30T20:42:17.642Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/68/7e/65837d35b2dab96b70fe55aa4a4b4fb7d2cd902e87c6074493392de769d3/cosl-1.9.0-py3-none-any.whl", hash = "sha256:026700e712cae75a5db038710997b0ffa27612fddc736a5a23d94111dec1f533", size = 37936, upload-time = "2026-03-30T20:42:16.487Z" }, -] [[package]] name = "exceptiongroup" From ab0642bf0f75495df7af19f645c995f3227c8900 Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Thu, 25 Jun 2026 14:06:53 -0400 Subject: [PATCH 07/65] chore --- interfaces/otlp/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/interfaces/otlp/CHANGELOG.md b/interfaces/otlp/CHANGELOG.md index b5a4320b8..69124fc4a 100644 --- a/interfaces/otlp/CHANGELOG.md +++ b/interfaces/otlp/CHANGELOG.md @@ -46,3 +46,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Separated the package into 2 private modules: _otlp and _rules - Enable user-defined rule labels + +## [0.6.0] + +### Added + +- Sigma rules in databag via the `RuleStore` API From f270abd40f11ffc42f259d5ee432c7d14b9e600c Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Fri, 26 Jun 2026 09:55:55 -0400 Subject: [PATCH 08/65] feat(otlp): sort keys when serializing rules to databag for deterministic output --- .../otlp/src/charmlibs/interfaces/otlp/_otlp.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/interfaces/otlp/src/charmlibs/interfaces/otlp/_otlp.py b/interfaces/otlp/src/charmlibs/interfaces/otlp/_otlp.py index 7d849f24b..6e8dfc000 100644 --- a/interfaces/otlp/src/charmlibs/interfaces/otlp/_otlp.py +++ b/interfaces/otlp/src/charmlibs/interfaces/otlp/_otlp.py @@ -105,8 +105,20 @@ def _deserialize_rules(cls, rules: str | dict[str, Any] | _RulesModel) -> Any: @field_serializer('rules') def _serialize_rules(self, rules: _RulesModel) -> str: - """LZMA-compress rules to reduce content size for larger deployments.""" - return LZMABase64.compress(rules.model_dump_json()) + """LZMA-compress rules to reduce content size for larger deployments. + + Keys are sorted so the serialized payload is deterministic for a given logical + ruleset. Without this, nested mappings (e.g. a Sigma rule's ``detection`` block, + which is an arbitrary dict) could serialize in a different key order between hooks, + changing the databag bytes and triggering spurious ``relation-changed`` events. + Note: this normalizes *mapping key* order only; list ordering (e.g. Sigma ``tags``) + is made deterministic at the source in ``cosl.rules.SigmaRules``. + + Pydantic performs the actual value encoding (via ``model_dump_json``); we only + re-serialize the resulting JSON with sorted keys to preserve that encoding fidelity. + """ + normalized = json.dumps(json.loads(rules.model_dump_json()), sort_keys=True) + return LZMABase64.compress(normalized) class OtlpRequirer: From 17a8bbd8d48de25204e1026503e7410a42085f73 Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Fri, 26 Jun 2026 10:40:36 -0400 Subject: [PATCH 09/65] chore --- interfaces/otlp/tests/unit/test_rules.py | 35 ++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/interfaces/otlp/tests/unit/test_rules.py b/interfaces/otlp/tests/unit/test_rules.py index 74e196c08..fed20bed5 100644 --- a/interfaces/otlp/tests/unit/test_rules.py +++ b/interfaces/otlp/tests/unit/test_rules.py @@ -91,6 +91,41 @@ def test_rules_compression(otlp_requirer_ctx: testing.Context[ops.CharmBase]): assert set(_RulesModel.model_fields.keys()).issubset(decompressed.keys()) +def test_published_rules_are_byte_stable_across_hooks( + otlp_requirer_ctx: testing.Context[ops.CharmBase], +): + # GIVEN a requirer that publishes rules (including Sigma rules with topology tags) + state = State(relations=[Relation(SEND)], leader=True) + + # WHEN the publish event fires twice, as it would across two separate hooks + first = otlp_requirer_ctx.run(otlp_requirer_ctx.on.update_status(), state=state) + first_rules = next(iter(first.relations)).local_app_data.get('rules') + + second = otlp_requirer_ctx.run(otlp_requirer_ctx.on.update_status(), state=first) + second_rules = next(iter(second.relations)).local_app_data.get('rules') + + # THEN the serialized databag value is byte-identical, so Juju sees no change and + # does not fire a spurious relation-changed event. + assert first_rules is not None + assert first_rules == second_rules + + +def test_sigma_rule_id_is_preserved_in_published_databag( + otlp_requirer_ctx: testing.Context[ops.CharmBase], +): + # GIVEN a requirer publishing a Sigma rule that carries a fixed UUID + state = State(relations=[Relation(SEND)], leader=True) + + # WHEN the rules are published + state_out = otlp_requirer_ctx.run(otlp_requirer_ctx.on.update_status(), state=state) + rules = next(iter(state_out.relations)).local_app_data.get('rules') + sigma_rules = _decompress(json.loads(rules))['sigma']['rules'] + + # THEN the original UUID survives untouched (it is never regenerated per hook) + ids = {r.get('id') for r in sigma_rules} + assert SINGLE_SIGMA_RULE['id'] in ids + + @pytest.mark.parametrize('subordinate', [True, False]) def test_duplicate_rules_per_unit(subordinate: bool): # GIVEN the charm is (or is not) a subordinate From f07f4184b529034c762fec2a14965177ecb8c6e7 Mon Sep 17 00:00:00 2001 From: Trong Nhan Mai Date: Mon, 11 May 2026 16:59:42 +1000 Subject: [PATCH 10/65] docs: replace the PGP key with the upstream page in SECURITY.md (#468) This PR replaces the PGP key in `SECURITY.md` to the upstream link https://ubuntu.com/security/disclosure-policy#contact-us. It is a part of https://github.com/canonical/operator/issues/2287. --- SECURITY.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 6b5a17349..036c545f5 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -21,9 +21,8 @@ to determine whether the issue qualifies as a security issue and, if so, in which component. We will then figure out a fix, get a CVE assigned, and coordinate the release of the fix. -You may also send email to security@ubuntu.com. Email may optionally be -encrypted to OpenPGP key -[`4072 60F7 616E CE4D 9D12 4627 98E9 740D C345 39E0`](https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x407260f7616ece4d9d12462798e9740dc34539e0) +You may also send email to security@ubuntu.com. If you want to encrypt your email, +follow [Canonical's reporting instructions](https://ubuntu.com/security/disclosure-policy#contact-us). If you have a deadline for public disclosure, please let us know. Our vulnerability management team intends to respond within 3 working From edcde2e7de9af7cf31c710df79d908e48afae219 Mon Sep 17 00:00:00 2001 From: James Garner Date: Thu, 14 May 2026 13:27:35 +0200 Subject: [PATCH 11/65] docs: remove broken link (#471) This PR removes a broken link to a full code sample. The snippets in the docs are sufficient as-is. --- .docs/how-to/design-relation-interfaces.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/.docs/how-to/design-relation-interfaces.md b/.docs/how-to/design-relation-interfaces.md index 77c7ece40..221ea5702 100644 --- a/.docs/how-to/design-relation-interfaces.md +++ b/.docs/how-to/design-relation-interfaces.md @@ -334,8 +334,6 @@ def test_secret_content(secret_content: dict[str, Any], status): assert state_out.unit_status == status ``` -[Full test code](https://github.com/dimaqq/op083-samples/blob/main/test_secret_content.py) - Or a unit test: ```py From 55c68d0cd507abb03170e97393a2c18adcaae619 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 08:57:13 +0200 Subject: [PATCH 12/65] chore(deps): bump urllib3 from 2.6.3 to 2.7.0 in /interfaces/otlp (#469) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.6.3 to 2.7.0.
Release notes

Sourced from urllib3's releases.

2.7.0

🚀 urllib3 is fundraising for HTTP/2 support

urllib3 is raising ~$40,000 USD to release HTTP/2 support and ensure long-term sustainable maintenance of the project after a sharp decline in financial support. If your company or organization uses Python and would benefit from HTTP/2 support in Requests, pip, cloud SDKs, and thousands of other projects please consider contributing financially to ensure HTTP/2 support is developed sustainably and maintained for the long-haul.

Thank you for your support.

Security

Addressed high-severity security issues. Impact was limited to specific use cases detailed in the accompanying advisories; overall user exposure was estimated to be marginal.

  • Decompression-bomb safeguards of the streaming API were bypassed:

    1. When HTTPResponse.drain_conn() was called after the response had been read and decompressed partially. (Reported by @​Cycloctane)
    2. During the second HTTPResponse.read(amt=N) or HTTPResponse.stream(amt=N) call when the response was decompressed using the official Brotli library. (Reported by @​kimkou2024)

    See GHSA-mf9v-mfxr-j63j for details.

  • HTTP pools created using ProxyManager.connection_from_url did not strip sensitive headers specified in Retry.remove_headers_on_redirect when redirecting to a different host. (GHSA-qccp-gfcp-xxvc reported by @​christos-spearbit)

Deprecations and Removals

  • Used FutureWarning instead of DeprecationWarning for better visibility of existing deprecation notices. Rescheduled the removal of deprecated features to version 3.0. (urllib3/urllib3#3763)
  • Removed support for end-of-life Python 3.9. (urllib3/urllib3#3720)
  • Removed support for end-of-life PyPy3.10. (urllib3/urllib3#4979)
  • Bumped the minimum supported pyOpenSSL version to 19.0.0. (urllib3/urllib3#3777)

Bugfixes

  • Fixed a bug where HTTPResponse.read(amt=None) was ignoring decompressed data buffered from previous partial reads. (urllib3/urllib3#3636)
  • Fixed a bug where HTTPResponse.read() could cache only part of the response after a partial read when cache_content=True. (urllib3/urllib3#4967)
  • Fixed HTTPResponse.stream() and HTTPResponse.read_chunked() to handle amt=0. (urllib3/urllib3#3793)
  • Updated _TYPE_BODY type alias to include missing Iterable[str], matching the documented and runtime behavior of chunked request bodies. (urllib3/urllib3#3798)
  • Fixed LocationParseError when paths resembling schemeless URIs were passed to HTTPConnectionPool.urlopen(). (urllib3/urllib3#3352)
  • Fixed BaseHTTPResponse.readinto() type annotation to accept memoryview in addition to bytearray, matching the io.RawIOBase.readinto contract and enabling use with io.BufferedReader without type errors. (urllib3/urllib3#3764)
Changelog

Sourced from urllib3's changelog.

2.7.0 (2026-05-07)

Security

Addressed high-severity security issues. Impact was limited to specific use cases detailed in the accompanying advisories; overall user exposure was estimated to be marginal.

  • Decompression-bomb safeguards of the streaming API were bypassed:

    1. When HTTPResponse.drain_conn() was called after the response had been read and decompressed partially.
    2. During the second HTTPResponse.read(amt=N) or HTTPResponse.stream(amt=N) call when the response was decompressed using the official Brotli <https://pypi.org/project/brotli/>__ library.

    See GHSA-mf9v-mfxr-j63j <https://github.com/urllib3/urllib3/security/advisories/GHSA-mf9v-mfxr-j63j>__ for details.

  • HTTP pools created using ProxyManager.connection_from_url did not strip sensitive headers specified in Retry.remove_headers_on_redirect when redirecting to a different host. (GHSA-qccp-gfcp-xxvc <https://github.com/urllib3/urllib3/security/advisories/GHSA-qccp-gfcp-xxvc>__)

Deprecations and Removals

  • Used FutureWarning instead of DeprecationWarning for better visibility of existing deprecation notices. Rescheduled the removal of deprecated features to version 3.0. ([#3763](https://github.com/urllib3/urllib3/issues/3763) <https://github.com/urllib3/urllib3/issues/3763>__)
  • Removed support for end-of-life Python 3.9. ([#3720](https://github.com/urllib3/urllib3/issues/3720) <https://github.com/urllib3/urllib3/issues/3720>__)
  • Removed support for end-of-life PyPy3.10. ([#4979](https://github.com/urllib3/urllib3/issues/4979) <https://github.com/urllib3/urllib3/issues/4979>__)
  • Bumped the minimum supported pyOpenSSL version to 19.0.0. ([#3777](https://github.com/urllib3/urllib3/issues/3777) <https://github.com/urllib3/urllib3/issues/3777>__)

Bugfixes

  • Fixed a bug where HTTPResponse.read(amt=None) was ignoring decompressed data buffered from previous partial reads. ([#3636](https://github.com/urllib3/urllib3/issues/3636) <https://github.com/urllib3/urllib3/issues/3636>__)
  • Fixed a bug where HTTPResponse.read() could cache only part of the response after a partial read when cache_content=True.

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=urllib3&package-manager=uv&previous-version=2.6.3&new-version=2.7.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/canonical/charmlibs/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- interfaces/otlp/uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/interfaces/otlp/uv.lock b/interfaces/otlp/uv.lock index 858a9456f..71f0912d4 100644 --- a/interfaces/otlp/uv.lock +++ b/interfaces/otlp/uv.lock @@ -595,11 +595,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]] From a71b6826358962f05d22e385b0b92a33d8d4f5f5 Mon Sep 17 00:00:00 2001 From: Patricia Reinoso Date: Fri, 15 May 2026 09:50:23 +0200 Subject: [PATCH 13/65] docs(rollingops): improve docs for base_dir parameter (#470) This PR improves the documentation for the `base_dir` parameter in the `RollingOpsManager` constructor. It adds details about how the library uses this directory and guidance for rootless charms --- .../src/charmlibs/rollingops/__init__.py | 24 +++++++++++++++++++ .../rollingops/_rollingops_manager.py | 8 +++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/rollingops/src/charmlibs/rollingops/__init__.py b/rollingops/src/charmlibs/rollingops/__init__.py index 7c99a76f8..a018442d8 100644 --- a/rollingops/src/charmlibs/rollingops/__init__.py +++ b/rollingops/src/charmlibs/rollingops/__init__.py @@ -275,6 +275,30 @@ def (self, **kwargs) -> OperationResult with etcd. +Runtime storage (``base_dir``) +------------------------------- + +RollingOps requires a writable base directory to store runtime files +used during operation. This directory is configured through the ``base_dir`` +parameter. + +By default, RollingOps uses: ``/var/lib/rollingops``. + +If the directory does not exist, RollingOps will attempt to create it automatically. +The process running RollingOps must have permission to write to ``base_dir``. + +For Kubernetes charms, ``base_dir`` must refer to a writable path inside the charm container +filesystem. + +What is stored in ``base_dir`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +RollingOps uses this directory for runtime artifacts, including: + +- logs generated by background processes +- etcd connection assets, such as client certificates and related configuration files + + Usage ---------- diff --git a/rollingops/src/charmlibs/rollingops/_rollingops_manager.py b/rollingops/src/charmlibs/rollingops/_rollingops_manager.py index 66b65302c..6c32caf26 100644 --- a/rollingops/src/charmlibs/rollingops/_rollingops_manager.py +++ b/rollingops/src/charmlibs/rollingops/_rollingops_manager.py @@ -78,8 +78,12 @@ class RollingOpsManager(Object): sync_lock_targets: Optional mapping of sync lock backend identifiers to backend implementations used when acquiring synchronous locks through the peer backend. - base_dir: base directory where all files related to rollingops will be written. - Written to ``/var/lib/rollingops`` by default. + base_dir: Base directory used by rollingops to store runtime files, including + etcd connection information and logs from background processes. + Defaults to ``/var/lib/rollingops``, which will be created if missing. + The process running rollingops must have permission to create and write to + this directory. For Kubernetes charms, this path must exist within the + charm container filesystem. """ def __init__( From 58ae1d019af4e87aa1a61cac90ff2bd58a324c07 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 15:03:04 +0200 Subject: [PATCH 14/65] chore(deps): bump cryptography from 46.0.5 to 46.0.7 in /interfaces/tls-certificates (#407) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [//]: # (dependabot-start) ⚠️ **Dependabot is rebasing this PR** ⚠️ Rebasing might not happen immediately, so don't worry if this takes some time. Note: if you make any changes to this PR yourself, they will take precedence over the rebase. --- [//]: # (dependabot-end) Bumps [cryptography](https://github.com/pyca/cryptography) from 46.0.5 to 46.0.7.
Changelog

Sourced from cryptography's changelog.

46.0.7 - 2026-04-07


* **SECURITY ISSUE**: Fixed an issue where non-contiguous buffers could
be
  passed to APIs that accept Python buffers, which could lead to buffer
  overflow. **CVE-2026-39892**
* Updated Windows, macOS, and Linux wheels to be compiled with OpenSSL
3.5.6.

.. _v46-0-6:

46.0.6 - 2026-03-25

  • SECURITY ISSUE: Fixed a bug where name constraints were not applied to peer names during verification when the leaf certificate contains a wildcard DNS SAN. Ordinary X.509 topologies are not affected by this bug, including those used by the Web PKI. Credit to Oleh Konko (1seal) for reporting the issue. CVE-2026-34073

.. _v46-0-5:

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=cryptography&package-manager=uv&previous-version=46.0.5&new-version=46.0.7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) You can trigger a rebase of this PR by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/canonical/charmlibs/network/alerts).
> **Note** > Automatic rebases have been disabled on this pull request as it has been open for over 30 days. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- interfaces/tls-certificates/uv.lock | 102 ++++++++++++++-------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/interfaces/tls-certificates/uv.lock b/interfaces/tls-certificates/uv.lock index 6a0bcb32e..a2c9d8f30 100644 --- a/interfaces/tls-certificates/uv.lock +++ b/interfaces/tls-certificates/uv.lock @@ -150,62 +150,62 @@ wheels = [ [[package]] name = "cryptography" -version = "46.0.5" +version = "46.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, - { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, - { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, - { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, - { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, - { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, - { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, - { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, - { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, - { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, - { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, - { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, - { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, - { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, - { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, - { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, - { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, - { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, - { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, - { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, - { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, - { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, - { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, - { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, - { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, - { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, - { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, - { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, - { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, - { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, - { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, - { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, - { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, - { url = "https://files.pythonhosted.org/packages/eb/dd/2d9fdb07cebdf3d51179730afb7d5e576153c6744c3ff8fded23030c204e/cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", size = 3476964, upload-time = "2026-02-10T19:18:20.687Z" }, - { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" }, - { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" }, - { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" }, - { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" }, - { url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, + { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, + { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, + { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, + { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, + { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, + { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" }, + { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, + { url = "https://files.pythonhosted.org/packages/63/0c/dca8abb64e7ca4f6b2978769f6fea5ad06686a190cec381f0a796fdcaaba/cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", size = 3476879, upload-time = "2026-04-08T01:57:38.664Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", size = 4219700, upload-time = "2026-04-08T01:57:40.625Z" }, + { url = "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", size = 4385982, upload-time = "2026-04-08T01:57:42.725Z" }, + { url = "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", size = 4219115, upload-time = "2026-04-08T01:57:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", size = 4385479, upload-time = "2026-04-08T01:57:46.86Z" }, + { url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" }, ] [[package]] From 89dd0d16867b956a72759f09db7a86ceeb8aab62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sinclert=20P=C3=A9rez?= Date: Thu, 21 May 2026 14:40:17 +0200 Subject: [PATCH 15/65] feat(rollingops): waiting condition in other units (#475) --- rollingops/CHANGELOG.md | 6 +- .../rollingops/_rollingops_manager.py | 47 ++++++-- .../src/charmlibs/rollingops/_version.py | 2 +- .../integration/test_etcd_rolling_ops.py | 6 +- .../unit/test_etcd_rollingops_in_charm.py | 101 ++++++++++++++++++ 5 files changed, 149 insertions(+), 13 deletions(-) diff --git a/rollingops/CHANGELOG.md b/rollingops/CHANGELOG.md index cd02a5dde..a9c7ae807 100644 --- a/rollingops/CHANGELOG.md +++ b/rollingops/CHANGELOG.md @@ -1,7 +1,11 @@ +# 1.1.0 - 20 May 2026 + +Extend the `RollingOpsManager` is_waiting_* helpers to receive a unit name. + # 1.0.1 - 04 May 2026 Fix the `ModelError` messages generated during rollback operations. # 1.0.0 - 28 April 2026 -Initial release of `charmlibs-rollingops` library \ No newline at end of file +Initial release of `charmlibs-rollingops` library diff --git a/rollingops/src/charmlibs/rollingops/_rollingops_manager.py b/rollingops/src/charmlibs/rollingops/_rollingops_manager.py index 6c32caf26..5a21a2e41 100644 --- a/rollingops/src/charmlibs/rollingops/_rollingops_manager.py +++ b/rollingops/src/charmlibs/rollingops/_rollingops_manager.py @@ -18,7 +18,7 @@ from contextlib import contextmanager from typing import Any -from ops import CharmBase, Object, Relation, RelationBrokenEvent +from ops import CharmBase, Object, Relation, RelationBrokenEvent, Unit from ops.framework import EventBase from charmlibs import pathops @@ -40,7 +40,7 @@ from charmlibs.rollingops._common._utils import ETCD_FAILED_HOOK_NAME, LOCK_GRANTED_HOOK_NAME from charmlibs.rollingops._etcd._backend import _EtcdRollingOpsBackend from charmlibs.rollingops._peer._backend import _PeerRollingOpsBackend -from charmlibs.rollingops._peer._models import PeerUnitOperations +from charmlibs.rollingops._peer._models import PeerUnitOperations, iter_peer_units logger = logging.getLogger(__name__) @@ -386,6 +386,14 @@ def _on_rollingops_etcd_failed(self, event: _RollingOpsEtcdFailedEvent) -> None: self._peer_backend.ensure_processing() logger.info('Fell back to peer backend.') + def _get_peer_unit(self, unit_name: str) -> Unit: + """Return the peer unit having the provided name.""" + for peer_unit in iter_peer_units(self.model, self.peer_relation_name): + if peer_unit.name == unit_name: + return peer_unit + + raise ValueError('unit_name provided does not belong to the peer relation.') + def _get_sync_lock_backend(self, backend_id: str) -> SyncLockBackend: """Instantiate the configured peer sync lock backend. @@ -511,28 +519,51 @@ def state(self) -> RollingOpsState: processing_backend=self._backend_state.backend, ) - def is_waiting_callback(self, callback_id: str) -> bool: - """Return whether the current unit has a pending operation matching callback.""" + def is_waiting_callback(self, callback_id: str, unit_name: str | None = None) -> bool: + """Return whether the desired unit has a pending operation matching callback. + + Args: + callback_id: callback ID to search for in the unit list of operations. + unit_name: name of the unit to search for specific callback ID operations. + If not specified, defaults to this unit. + + Raises: + ValueError: If the unit name is not found within the peer relation units. + """ if self._peer_relation is None: return False + if unit_name is None: + unit_name = self.model.unit.name + operations = PeerUnitOperations( self.model, self.peer_relation_name, - self.model.unit, + self._get_peer_unit(unit_name), ).queue.operations return any(op.callback_id == callback_id for op in operations) - def is_waiting(self) -> bool: - """Return whether the current unit has a pending operations.""" + def is_waiting(self, unit_name: str | None = None) -> bool: + """Return whether the desired unit has pending operations. + + Args: + unit_name: name of the unit to search for operations. + If not specified, defaults to this unit. + + Raises: + ValueError: If the unit name is not found within the peer relation units. + """ if self._peer_relation is None: return False + if unit_name is None: + unit_name = self.model.unit.name + operations = PeerUnitOperations( self.model, self.peer_relation_name, - self.model.unit, + self._get_peer_unit(unit_name), ).queue.operations return bool(operations) diff --git a/rollingops/src/charmlibs/rollingops/_version.py b/rollingops/src/charmlibs/rollingops/_version.py index fe9c6b4a6..f9e828ee1 100644 --- a/rollingops/src/charmlibs/rollingops/_version.py +++ b/rollingops/src/charmlibs/rollingops/_version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = '1.0.1' +__version__ = '1.1.0' diff --git a/rollingops/tests/integration/test_etcd_rolling_ops.py b/rollingops/tests/integration/test_etcd_rolling_ops.py index 1936ae849..07a3daaf7 100644 --- a/rollingops/tests/integration/test_etcd_rolling_ops.py +++ b/rollingops/tests/integration/test_etcd_rolling_ops.py @@ -205,6 +205,7 @@ def test_retry_hold_operation_two_units_single_app(juju: jubilant.Juju, app_name unit_b = units[3] juju.run(unit_a, 'deferred-restart', {'delay': 15, 'max-retry': 2}, wait=TIMEOUT) + time.sleep(2) juju.run(unit_b, 'restart', {'delay': 2}, wait=TIMEOUT) juju.wait( @@ -253,9 +254,8 @@ def test_retry_release_two_units_single_app(juju: jubilant.Juju, app_name: str): juju.run(unit_a, 'failed-restart', {'delay': 10, 'max-retry': 2}, wait=TIMEOUT) juju.run(unit_b, 'failed-restart', {'delay': 15, 'max-retry': 2}, wait=TIMEOUT) - time.sleep( - 60 * 3 - ) # wait for operation execution. TODO: in charm use lock state to clear status. + # TODO: in charm use lock state to clear status. + time.sleep(60 * 3) all_events: list[dict[str, str]] = [] all_events.extend(get_unit_events(juju, unit_a)) diff --git a/rollingops/tests/unit/test_etcd_rollingops_in_charm.py b/rollingops/tests/unit/test_etcd_rollingops_in_charm.py index 6822ddf2c..4b7003184 100644 --- a/rollingops/tests/unit/test_etcd_rollingops_in_charm.py +++ b/rollingops/tests/unit/test_etcd_rollingops_in_charm.py @@ -396,3 +396,104 @@ def test_is_waiting_returns_false_when_no_operations(ctx: Context[RollingOpsChar with ctx(ctx.on.update_status(), state) as mgr: assert mgr.charm.restart_manager.is_waiting_callback('restart') is False assert mgr.charm.restart_manager.is_waiting() is False + + +def test_is_waiting_returns_true_when_matching_operation_exists_in_unit( + ctx: Context[RollingOpsCharm], +): + peer_rel = PeerRelation( + peers_data={ + 1: { + 'state': 'request', + 'operations': _OperationQueue([ + _Operation.create('restart', {'delay': 1}), + _Operation.create('restart', {'delay': 2}), + ]).to_string(), + 'executed_at': '', + 'processing_backend': 'peer', + 'etcd_cleanup_needed': 'false', + }, + }, + endpoint='restart', + interface='rollingops', + local_app_data={}, + local_unit_data={ + 'state': 'request', + 'operations': _OperationQueue([]).to_string(), + 'executed_at': '', + 'processing_backend': 'peer', + 'etcd_cleanup_needed': 'false', + }, + ) + + state = State(leader=False, relations={peer_rel}) + + with ctx(ctx.on.update_status(), state) as mgr: + assert mgr.charm.restart_manager.is_waiting_callback('restart', 'charm/1') is True + assert mgr.charm.restart_manager.is_waiting('charm/1') is True + + +def test_is_waiting_returns_false_when_callback_does_not_match_in_unit( + ctx: Context[RollingOpsCharm], +): + peer_rel = PeerRelation( + peers_data={ + 1: { + 'state': 'request', + 'operations': _OperationQueue([ + _Operation.create('restart', {'delay': 1}), + ]).to_string(), + 'executed_at': '', + 'processing_backend': 'peer', + 'etcd_cleanup_needed': 'false', + }, + }, + endpoint='restart', + interface='rollingops', + local_app_data={}, + local_unit_data={ + 'state': 'request', + 'operations': _OperationQueue([]).to_string(), + 'executed_at': '', + 'processing_backend': 'peer', + 'etcd_cleanup_needed': 'false', + }, + ) + state = State(leader=False, relations={peer_rel}) + + with ctx(ctx.on.update_status(), state) as mgr: + assert mgr.charm.restart_manager.is_waiting_callback('other-callback', 'charm/1') is False + assert mgr.charm.restart_manager.is_waiting('charm/1') is True + + +def test_is_waiting_returns_false_when_no_operations_in_unit( + ctx: Context[RollingOpsCharm], +): + peer_rel = PeerRelation( + peers_data={ + 1: { + 'state': 'request', + 'operations': _OperationQueue([]).to_string(), + 'executed_at': '', + 'processing_backend': 'peer', + 'etcd_cleanup_needed': 'false', + }, + }, + endpoint='restart', + interface='rollingops', + local_app_data={}, + local_unit_data={ + 'state': 'request', + 'operations': _OperationQueue([ + _Operation.create('restart', {'delay': 1}), + ]).to_string(), + 'executed_at': '', + 'processing_backend': 'peer', + 'etcd_cleanup_needed': 'false', + }, + ) + state = State(leader=False, relations={peer_rel}) + + with ctx(ctx.on.update_status(), state) as mgr: + assert mgr.charm.restart_manager.is_waiting_callback('restart', 'charm/1') is False + assert mgr.charm.restart_manager.is_waiting('charm/1') is False From 815ac95ff3fb949264d207b3d82e285cecbcd771 Mon Sep 17 00:00:00 2001 From: James Garner Date: Fri, 22 May 2026 09:46:13 +1200 Subject: [PATCH 16/65] ci: upload charmcraft logs (#456) We see regular `charmcraft pack` failures in CI, which appear to be due to `snapd` startup issues when packing from a cold start. Here's an [example failure](https://github.com/canonical/charmlibs/actions/runs/25139482207/job/73685763313). Here's the `charmcraft` issue: https://github.com/canonical/charmcraft/issues/2515 --- This PR adds a follow-up step to upload the `charmcraft` logs after running integration tests (whether we succeed or fail -- so long as either the pack or integration step ran). In order for the uploaded artifact to have a unique name, it is prefixed with the name of the package under test. The package name provided as input to the workflow may contain a slash (e.g. `interfaces/tls-certificates`), which is invalid for an artifact name. The cleanest way to get a valid and unique name is to get the distribution package name via `.scripts/ls.py`. To avoid having to perform `jq` wizardry to extract the bare name from the JSON list that `ls.py` would output, I've updated `ls.py` with a `--no-json` argument (which prints each entry on a separate line instead -- so the output for a single field is just `\n`). As a driveby improvement, I've collapsed the `--name-only` and `--path-only` arguments into the more general `--output-only `, which simplifies the handling of the various `--output` arguments and makes querying any single field possible without needing to add a custom argument. --- There's a charmcraft issue tracking what appears to be the same failure that we see: https://github.com/canonical/charmcraft/issues/2515 --- .github/get-changed.py | 2 +- .github/workflows/test-interface.yaml | 2 +- .github/workflows/test-package.yaml | 19 +++++++++++++++++++ .scripts/ls.py | 23 ++++++++++++----------- 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/.github/get-changed.py b/.github/get-changed.py index 1a8b06a06..88df1635e 100755 --- a/.github/get-changed.py +++ b/.github/get-changed.py @@ -52,7 +52,7 @@ def _main() -> None: else: cmd.append(args.git_base_ref) if args.name_only: - cmd.append('--name-only') + cmd.extend(['--output-only', 'name']) result = subprocess.check_output(cmd, text=True).strip() output = f'result={result}' logger.info(output) diff --git a/.github/workflows/test-interface.yaml b/.github/workflows/test-interface.yaml index 2df15b79a..077da3ba7 100644 --- a/.github/workflows/test-interface.yaml +++ b/.github/workflows/test-interface.yaml @@ -33,7 +33,7 @@ jobs: id: schema run: | set -xueo pipefail - SCHEMA=$(.scripts/ls.py interfaces --regex "interfaces/$INTERFACE" --output schema_path | jq -r '.[].schema_path') + SCHEMA=$(.scripts/ls.py interfaces --regex "interfaces/$INTERFACE" --output-only schema_path --no-json) echo "result=$SCHEMA" >> "$GITHUB_OUTPUT" - name: Check schema can be run if it exists if: steps.schema.outputs.result diff --git a/.github/workflows/test-package.yaml b/.github/workflows/test-package.yaml index d0afa917a..21502aaf1 100644 --- a/.github/workflows/test-package.yaml +++ b/.github/workflows/test-package.yaml @@ -32,6 +32,7 @@ jobs: init: runs-on: ubuntu-latest outputs: + package-name: ${{ steps.package-info.outputs.name }} tests: ${{ steps.tests.outputs.tests }} python: ${{ steps.python.outputs.versions }} min_python_version: ${{ steps.python.outputs.min_version }} @@ -55,6 +56,14 @@ jobs: *) uv lock --check ;; esac + - name: Get package info + id: package-info + run: | + set -xueo pipefail + NAME=$(.scripts/ls.py packages --no-json --exclude-testing --output-only name --regex "$PACKAGE") + echo "name=$NAME" + echo "name=$NAME" >> "$GITHUB_OUTPUT" + - name: Check which Python versions this package supports id: python run: uv run --no-project --script .github/get-supported-python-versions.py "$PACKAGE" @@ -222,14 +231,24 @@ jobs: - name: Pack charms if: ${{ hashFiles(format('{0}/tests/integration/pack.sh', inputs.package)) != '' }} + id: pack env: TAG: ${{ matrix.tag }} SUBSTRATE: ${{ matrix.substrate }} run: uvx --from rust-just just tag="$TAG" "pack-$SUBSTRATE" "$PACKAGE" - name: Run Juju integration tests + id: integration env: PYTHON: ${{ needs.init.outputs.min_python_version }} RECIPE: integration-${{ matrix.substrate }} TAG: ${{ matrix.tag }} run: uvx --from rust-just just python="$PYTHON" tag="$TAG" "$RECIPE" "$PACKAGE" + + - name: Upload Charmcraft logs + if: ${{ !cancelled() && (steps.pack.conclusion != 'skipped' || steps.integration.conclusion != 'skipped') }} + uses: actions/upload-artifact@v4 + with: + name: ${{ needs.init.outputs.package-name }}-charmcraft-logs-${{ matrix.substrate }}-${{ matrix.tag }} + path: /home/runner/.local/state/charmcraft/log/charmcraft-*.log + if-no-files-found: warn # pack.sh scripts aren't required to produce charmcraft logs (though they do currently) diff --git a/.scripts/ls.py b/.scripts/ls.py index 9f4434d40..6a25192e9 100755 --- a/.scripts/ls.py +++ b/.scripts/ls.py @@ -85,15 +85,13 @@ def _main() -> None: parser.add_argument('--only-if-version-changed', action='store_true') parser.add_argument('--indent-json', action='store_true') parser.add_argument('--regex', default=None) + parser.add_argument('--no-json', action='store_true') group = parser.add_mutually_exclusive_group() all_fields = [f.name for f in dataclasses.fields(Info)] - group.add_argument('--output', action='append', choices=all_fields) - group.add_argument('--output-all', action='store_const', const=all_fields, default=[]) - group.add_argument('--name-only', action='store_true') - group.add_argument('--path-only', action='store_true') # default behaviour + group.add_argument('--output', action='append', choices=all_fields, dest='output') + group.add_argument('--output-all', action='store_const', const=all_fields, dest='output') + group.add_argument('--output-only', action='store', default='path') # default behaviour args = parser.parse_args() - output = args.output or args.output_all - single_output = 'name' if args.name_only else 'path' infos = _ls( category=args.category, old_ref=args.old_ref, @@ -103,16 +101,19 @@ def _main() -> None: include_testing=not args.exclude_testing, only_if_version_changed=args.only_if_version_changed, regex=args.regex, - output=output or [single_output], + output=args.output or [args.output_only], ) - if output: + if args.output: result = sorted( - (info.to_dict(*output) for info in infos), + (info.to_dict(*args.output) for info in infos), key=lambda di: tuple(di.items()), ) else: - result = sorted(getattr(info, single_output) for info in infos) - print(json.dumps(result, indent=2 if args.indent_json else None)) + result = sorted(getattr(info, args.output_only) for info in infos) + if args.no_json: + print('\n'.join(str(r) for r in result)) + else: + print(json.dumps(result, indent=2 if args.indent_json else None)) def _ls( From 9be5beea414248e0645208bb850540d667f83f7f Mon Sep 17 00:00:00 2001 From: James Garner Date: Fri, 22 May 2026 13:01:51 +1200 Subject: [PATCH 17/65] ci: use a more recent version of actions/upload-artifact (#484) In #456 I accidentally used an old version of `actions/upload-artifact`. This PR bumps it to a more recent version. --- .github/workflows/test-package.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-package.yaml b/.github/workflows/test-package.yaml index 21502aaf1..ed7ad0281 100644 --- a/.github/workflows/test-package.yaml +++ b/.github/workflows/test-package.yaml @@ -247,7 +247,7 @@ jobs: - name: Upload Charmcraft logs if: ${{ !cancelled() && (steps.pack.conclusion != 'skipped' || steps.integration.conclusion != 'skipped') }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: ${{ needs.init.outputs.package-name }}-charmcraft-logs-${{ matrix.substrate }}-${{ matrix.tag }} path: /home/runner/.local/state/charmcraft/log/charmcraft-*.log From d352b8029bdef901d12c93a02e522a6c21d28e43 Mon Sep 17 00:00:00 2001 From: James Garner Date: Mon, 25 May 2026 09:41:57 +1200 Subject: [PATCH 18/65] docs: fix schema links (#473) This small PR fixes some schema links in interface readmes, requiring codeowner review to merge: - `certificate-transfer`, `cos_agent`: links were described as JSON schema, corrected to Pydantic - `ldap`, `tls-certificates`: added missing links to existing schema files --- interfaces/certificate_transfer/interface/v1/README.md | 6 ++---- interfaces/cos_agent/interface/v0/README.md | 4 ++-- interfaces/ldap/interface/v0/README.md | 2 ++ interfaces/tls-certificates/interface/v1/README.md | 2 ++ 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/interfaces/certificate_transfer/interface/v1/README.md b/interfaces/certificate_transfer/interface/v1/README.md index 490be3182..297df98b1 100644 --- a/interfaces/certificate_transfer/interface/v1/README.md +++ b/interfaces/certificate_transfer/interface/v1/README.md @@ -31,9 +31,9 @@ Both the Requirer and the Provider need to adhere to criteria to be considered c ## Relation Data -### Provider +[\[Pydantic Schema\]](./schema.py) -[\[JSON Schema\]](./schema.py) +### Provider The provider writes any number of certficates to its relation data. @@ -52,8 +52,6 @@ relation-info: ### Requirer -[\[JSON Schema\]](./schema.py) - The requirer writes the version number of the interface to its application databag as soon as feasible. #### Example diff --git a/interfaces/cos_agent/interface/v0/README.md b/interfaces/cos_agent/interface/v0/README.md index 3374a4d8c..ca69b1c5c 100644 --- a/interfaces/cos_agent/interface/v0/README.md +++ b/interfaces/cos_agent/interface/v0/README.md @@ -38,9 +38,9 @@ As all Juju relations, the `cos_agent` interface consists of a provider and a re ## Relation Data -### Provider +[\[Pydantic Schema\]](./schema.py) -[\[JSON Schema\]] +### Provider #### Unit data diff --git a/interfaces/ldap/interface/v0/README.md b/interfaces/ldap/interface/v0/README.md index 3c5b7bfc5..74dc34fee 100644 --- a/interfaces/ldap/interface/v0/README.md +++ b/interfaces/ldap/interface/v0/README.md @@ -53,6 +53,8 @@ through the relation databag(s). ## Relation Data +[\[Pydantic Schema\]](./schema.py) + ### Provider The `provider` provides LDAP URL, base DN, and bind DN, and LDAP diff --git a/interfaces/tls-certificates/interface/v1/README.md b/interfaces/tls-certificates/interface/v1/README.md index c91a0078c..b7dc43e76 100644 --- a/interfaces/tls-certificates/interface/v1/README.md +++ b/interfaces/tls-certificates/interface/v1/README.md @@ -43,6 +43,8 @@ compatible with the interface. ## Relation Data +[\[Pydantic Schema\]](./schema.py) + ### Requirer The requirer specifies a set of certificate signing requests (CSR's). From ecea9d55786e23052cadb0693f0b7018d259b40c Mon Sep 17 00:00:00 2001 From: James Garner Date: Mon, 25 May 2026 16:13:29 +1200 Subject: [PATCH 19/65] docs: add pathops/CONTRIBUTING.md (#490) This PR builds on #489 to add a `CONTRIBUTING.md` document specifically for Pathops, outlining the unique developer-facing concerns of this library, namely compatibility with Python's `pathlib` across key Python versions (3.10, 3.12, 3.14), and the functional test suite that validates the library's behaviour quickly with a local Pebble instance. --------- Co-authored-by: Dave Wilding --- pathops/CONTRIBUTING.md | 90 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 pathops/CONTRIBUTING.md diff --git a/pathops/CONTRIBUTING.md b/pathops/CONTRIBUTING.md new file mode 100644 index 000000000..692221c91 --- /dev/null +++ b/pathops/CONTRIBUTING.md @@ -0,0 +1,90 @@ +# Contributing to charmlibs-pathops + +See the repo-level [CONTRIBUTING.md](../CONTRIBUTING.md) for the general monorepo contribution guide. + +## Source layout + +``` +src/charmlibs/pathops/ +├── __init__.py # Public API exports and module docstring +├── _container_path.py # ContainerPath: Pebble-backed remote path implementation +├── _local_path.py # LocalPath: pathlib.PosixPath subclass with extended signatures +├── _types.py # PathProtocol: Protocol definition (type-checking only) +├── _functions.py # Top-level helpers: ensure_contents +├── _errors.py # Maps Pebble errors to Python exceptions +├── _constants.py # Default permission modes +└── _version.txt # Package version (auto-generated) +``` + +## Design notes + +**`PathProtocol` is a `typing.Protocol`, not an ABC.** It exists to make type annotations work correctly for code that accepts either `ContainerPath` or `LocalPath`. It is defined in `_types.py` and is intended for import for type checking only. `PathProtocol` reflects the implementation of `ContainerPath` and `LocalPath`. User implementations of `PathProtocol` are explicitly not covered by semver. For example, if an argument type is broadened in `ContainerPath` and `LocalPath` (a backwards compatible change), `PathProtocol` would also broaden (a backwards incompatible change for alternative implementations that only support the narrower argument type). + +**Default permissions follow Pebble, not pathlib.** `_constants.py` defines `DEFAULT_WRITE_MODE = 0o644` and `DEFAULT_MKDIR_MODE = 0o755`, matching Pebble's defaults. `pathlib` uses `0o666` and `0o777` respectively. This is intentional — `ContainerPath` and `LocalPath` share the same defaults for consistency. + +**`_errors.py` maps Pebble errors to standard Python exceptions.** Where possible, `ContainerPath` methods raise standard exceptions (`FileNotFoundError`, `FileExistsError`, `PermissionError`, `OSError`) rather than Pebble-specific errors. `_errors.py` centralises the mapping logic. When adding new `ContainerPath` methods, follow the existing error handling patterns there. + +**`ContainerPath` and `LocalPath` must stay in sync.** The `PathProtocol` is updated to reflect the current combined interface of both classes. When adding or changing a method on one class, check whether the same change is needed on the other. + +## Testing + +Always run: + +```bash +just check pathops +``` + +### Unit tests and type checking + +Pathops is very concerned with `pathlib` compatibility across Python versions. Unit tests should be run with the Python versions supported by Ubuntu LTS releases (which charms will run with). Run the unit tests against these versions with: + +```bash +just python=3.10 unit pathops +just python=3.12 unit pathops +just python=3.14 unit pathops +``` + +Also run static analysis the same way to ensure typing compatibility is preserved as intended. +```bash +just python= static pathops +``` + +Unit tests live under `tests/unit/`. They test `ContainerPath`, `LocalPath`, and `ensure_contents` without a real Pebble instance. `ContainerPath` is constructed using a dummy `ops.Container` backend (see `tests/unit/conftest.py`), and Pebble API calls are monkeypatched using the helpers in `tests/unit/utils.py`. + +`tests/typecheck.py` is not a pytest file — it contains statements that verify `ContainerPath` and `LocalPath` implement `PathProtocol` and that `LocalPath` is a valid `pathlib.Path`. These are checked by pyright as part of `just lint pathops` / `just static pathops`. + +### Functional tests + +```bash +just functional pathops +``` + +Functional tests live under `tests/functional/` and exercise both `ContainerPath` and `LocalPath` against a real Pebble instance. They require `pebble` to be installed and available in `PATH`. + +`tests/functional/setup.sh` starts Pebble with `PEBBLE=/tmp/pebble-test` and `umask 0`. `teardown.sh` kills the process. The `umask 0` setting is important — tests that check file modes will fail if the umask masks bits. + +In CI, functional tests run against a matrix of Pebble versions and Ubuntu bases (see `[tool.charmlibs.functional]` in `pyproject.toml`). Locally you'll need to install the Pebble version you want to test against manually. + +The functional tests will fail early if `pebble` is unavailable. Pebble can be installed with `snap`: + +```bash +sudo snap install --classic pebble +``` + +### Integration tests + +```bash +# First pack the charm(s) if the library or charm code has changed: +# just [pack-k8s|pack-machine] +# CHARMLIBS_TAG must be set to a supported base +env CHARMLIBS_TAG=22.04 just pack-k8s pathops +# Then run the integration tests against a real Juju cloud: +# just [integration-k8s|integration-machine] +just integration-k8s pathops +``` + +Integration tests deploy real charms against a live Juju model. They require `charmcraft` for packing, and a live Juju controller. + +`pack.sh` copies the library's `src/` and `pyproject.toml` directly into a temporary charm directory before calling `charmcraft pack`. The `CHARMLIBS_TAG` environment variable (set to `22.04` or `24.04`, for example) determines which Ubuntu base to pack for. In CI, the integration test matrix runs across the supported bases and substrates. + +`tests/integration/test_meta.py` asserts that the `k8s` and `machine` test charms have identical `common.py` files — if this test fails, the charms have drifted and need to be reconciled. From 99bfd7eaaa44eb1e383ef6d8fd0da57f22281aba Mon Sep 17 00:00:00 2001 From: James Garner Date: Mon, 25 May 2026 17:41:12 +1200 Subject: [PATCH 20/65] docs: expand README.md, revamp CONTRIBUTING.md, add AGENTS.md (#489) This PR improves the experience for new users of the repository by updating the top-level developer-facing documentation. The repository level `README.md` has been expanded to clearly introduce the repository concepts (Juju, charms, charm libraries), and provide entrypoints for contributing to the monorepo, writing libraries more generally, and for using libraries in charms. The `CONTRIBUTING.md` includes a clearer introduction and documents missing parts of the contribution workflow. Additionally, an `AGENTS.md` file is added to explain the key concepts, monorepo organisation, developer tooling, commands to run whenever making changes, and so on. Resolves #486. --------- Co-authored-by: Tony Meyer --- AGENTS.md | 227 ++++++++++++++++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 55 ++++++++---- README.md | 63 +++++++++++++- 3 files changed, 328 insertions(+), 17 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..70083fd0a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,227 @@ +# Agent instructions for charmlibs + +## Overview + +`charmlibs` is a monorepo of Python libraries for Juju charms. + +**Key concepts:** + +- **Juju** — Canonical's open-source orchestration engine. Deploys and manages applications on Kubernetes and bare-metal/VM clouds. +- **Charm** — a Python program that uses the [ops](https://documentation.ubuntu.com/ops/) framework to respond to Juju lifecycle events and manage a workload. Charms can be Kubernetes-based (using [Pebble](https://documentation.ubuntu.com/pebble/) to manage container processes) or machine-based. +- **Juju relation** — a named connection between two charms, backed by shared key-value stores called *databags*. Relations are how charms communicate and exchange structured configuration. +- **Charm library** — a reusable Python package that charm authors import. All libraries in this repo are distributed as Python packages on PyPI (not Charmhub-hosted single-file modules, which are a legacy distribution method). + +**Two library categories:** + +| Category | Namespace | Example package | Import | +|----------|-----------|-----------------|--------| +| General | `charmlibs` | `charmlibs-pathops` | `from charmlibs import pathops` | +| Interface | `charmlibs.interfaces` | `charmlibs-interfaces-tls-certificates` | `from charmlibs.interfaces import tls_certificates` | + +General libraries live at the repo root (e.g. `pathops/`). Interface libraries live under `interfaces/` (for example, `interfaces/tls-certificates/`). + +## Repo structure + +``` +charmlibs/ +├── / # One directory per general library (e.g. pathops/, apt/, snap/) +│ ├── src/charmlibs// # Source (namespace package) +│ ├── tests/ +│ │ ├── unit/ +│ │ ├── functional/ # Optional; omit directory to skip +│ │ └── integration/ # Optional; Juju-based tests +│ ├── pyproject.toml +│ ├── uv.lock # Commit this +│ ├── README.md +│ └── CHANGELOG.md +├── interfaces/ # Interface libraries +│ └── / # Named exactly as in charmcraft.yaml (e.g. tls-certificates) +│ ├── src/charmlibs/interfaces// +│ ├── testing/ # Optional testing subpackage for charm unit tests +│ ├── pyproject.toml +│ └── ... +├── .docs/ # Sphinx source for documentation.ubuntu.com/charmlibs +├── .template/ # Cookiecutter template used by `just init` +├── .github/workflows/ # CI: ci.yaml, test-package.yaml, test-interface.yaml, publish.yaml +├── justfile # Task runner recipes +├── docs.just # Docs-specific recipes +├── interface.just # Interface-specific recipes +├── pyproject.toml # Repo-level config: ruff, coverage, shared linting settings +└── test-requirements.txt # Pinned versions of pytest, coverage, and other test tools +``` + +## Development workflow + +### Prerequisites + +`uv` and `just` are required. Verify they're available before proceeding: + +```bash +uv --version && just --version +``` + +If either is missing, they can be installed like this: + +```bash +sudo snap install --classic astral-uv # installs uv +uv tool install rust-just # installs just (once uv is available) +``` + +Run `just` or `just help` from anywhere in the repo to see all available commands. All `just` commands execute from the repo root regardless of where they're invoked. + +### Inner loop + +The command you'll run most often is: + +```bash +just check +``` + +This runs `just lint `, `just unit `, and `just docs html `. **Run this before every commit on the affected package.** + +The `` argument is the path from the repo root, e.g. `pathops` or `interfaces/tls-certificates`. + +### Command reference + +| Command | Description | +|---------|-------------| +| `just check ` | Lint + unit tests + docs (the standard pre-commit check) | +| `just lint ` | ruff + codespell + pyright | +| `just fast-lint [path]` | ruff only, across the whole repo or a specific path | +| `just format [package]` | Auto-fix ruff and formatting errors | +| `just static ` | pyright only | +| `just unit ` | Run unit tests | +| `just functional ` | Run functional tests (may need external software available, or to be run with sudo -- and note that this probably indicates a test that's destructive to the local environment (e.g. adding or removing packages)) | +| `just pack-k8s ` | Pack K8s test charm(s) for integration tests, running the libraries `tests/integration/pack.sh` script with environment variables set | +| `just pack-machine ` | Pack machine test charm(s) for integration tests, as above | +| `just integration-k8s ` | Run Juju integration tests, excluding `pytest.mark.machine_only` tests | +| `just integration-machine ` | Run Juju integration tests against machine, excluding `pytest.mark.k8s_only` tests | +| `just docs html [packages]` | Build docs (including reference docs for all packages by default, or named ones -- run `just docs html -` to exclude all package reference docs (faster when working on docs only) | +| `just docs` | Alias for `just docs html` | +| `just add ` | Add a dependency to a library | +| `just init` | Scaffold a new general library | +| `just interface init` | Scaffold a new interface library | + +Extra arguments to `just unit`, `just functional`, and `just integration-*` are passed through to pytest: + +```bash +just unit pathops -x -k test_copy # stop on first failure, filter by name +``` + +### Adding dependencies + +**Always use `just add ` instead of calling `uv add` directly.** This applies repo-level version constraints from `test-requirements.txt`, which is necessary to keep the lockfile consistent: + +```bash +just add pathops 'pydantic>=2' +just add interfaces/tls-certificates --requirements my-requirements.txt +``` + +## Test types + +| Type | Command | What it tests | External requirements | +|------|---------|---------------|----------------------| +| Unit | `just unit ` | Logic with mocked externals | None | +| Functional | `just functional ` | Interaction with real external processes (not Juju) | Varies (for example, pebble, sudo) | +| Integration | `just integration-k8s/machine ` | Library in a real Juju deployment | charmcraft + Juju controller | + +A test type is only executed if the corresponding `tests/` subdirectory exists. Remove a directory to skip that test type entirely. + +Read more: [types of tests in the charmlibs monorepo](https://documentation.ubuntu.com/charmlibs/explanation/charmlibs-tests/). + +## Commit and PR conventions + +**PRs are squash-merged.** The PR title becomes the single commit message on `main`. Branch commit messages are for local reference, and should follow conventional commits for clarity. + +PR titles must follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). When a PR affects a single library, use the distribution package name without the leading `charmlibs-` as the scope: + +``` +feat(pathops): add copytree and rmtree +fix(apt): handle multiarch package names correctly +chore(interfaces-tls-certificates): update to Pydantic v2 +docs: improve tutorial for adding integration tests +``` + +**One PR should normally touch only one library.** The CI uses changed files to determine which packages to test and, on merge, which packages to publish. + +### Versioning + +- Libraries use semantic versioning (`MAJOR.MINOR.PATCH`). +- Dev versions (`X.Y.Z.devN`) are excluded from release CI — safe for in-progress work. +- When bumping to a non-dev version, you **must** also update `CHANGELOG.md`. CI will block the merge otherwise. +- The CI automatically publishes to PyPI on merge when a non-dev version bump is detected. + +## Package anatomy + +A typical general library looks like this: + +``` +pathops/ +├── src/charmlibs/pathops/ +│ ├── __init__.py # Exports public API; module docstring appears in reference docs +│ ├── _pathops.py # Private implementation module(s) +│ └── _version.py # __version__ string (auto-generated) +├── tests/ +│ ├── unit/ +│ │ ├── conftest.py +│ │ └── test_*.py +│ ├── functional/ +│ │ ├── setup.sh # Sourced before tests (for example, start pebble) +│ │ ├── teardown.sh # Sourced after tests (for example, kill pebble) +│ │ └── test_*.py +│ └── integration/ +│ ├── pack.sh # Script to pack test charms +│ ├── conftest.py # Juju fixtures (jubilant) +│ ├── test_*.py +│ └── charms/ # Test charm source +├── pyproject.toml # Package metadata, dependencies, tool config +├── uv.lock # Locked dependencies — commit this +├── README.md +└── CHANGELOG.md # Must be updated before each release +``` + +The source uses a namespace package structure: `src/charmlibs//`. Public API is exported from `__init__.py` via `__all__`. Implementation lives in private submodules (prefixed with `_`). The module docstring in `__init__.py` appears as the top-level description in the generated reference docs. + +## Interface libraries + +Interface libraries manage the structured data that charms exchange over a Juju relation databag. Key differences from general libraries: + +- Live under `interfaces//`, named exactly as the interface name appears in `charmcraft.yaml`. +- Source under `src/charmlibs/interfaces//`. +- Usually include a `testing/` subdirectory with a separate `charmlibs-interfaces--testing` package. This provides `relation_for_provider()` and `relation_for_requirer()` helpers for charm unit tests. +- Typically have unit and integration tests but no functional tests (all meaningful interaction is through Juju). +- Use `just interface init` to scaffold. + +Read more: [how to design relation interfaces](https://documentation.ubuntu.com/charmlibs/how-to/design-relation-interfaces/), [how to provide relation data for charm tests](https://documentation.ubuntu.com/charmlibs/how-to/provide-relation-data-for-charm-tests/). + +## Documentation + +Reference docs are auto-generated from Python docstrings via Sphinx autodoc. The docs source lives in `.docs/`. Build reference docs for a specific package with: + +```bash +just docs html pathops +``` + +This is also run as part of `just check`. The full docs site (all packages) is built with `just docs`. + +When writing or editing docstrings in `__init__.py` or other public modules, remember they appear verbatim in the published reference at [documentation.ubuntu.com/charmlibs](https://documentation.ubuntu.com/charmlibs). Keep them informative for library users, not implementation notes. + +## Common pitfalls + +- **Don't call `uv add` directly** — use `just add ` to respect repo-level constraints. +- **Don't bump the package you're working on to a non-dev version without updating `CHANGELOG.md`** — CI will block the merge. +- **Making changes to multiple libraries in the same PR** — the CI matrix runs per changed package, and review requirements are determined by `CODEOWNERS`. +- **Don't add unnecessary features or refactor code beyond what's asked** — this is a multi-team monorepo with careful versioning; unintended public API changes require major version bumps. + +## External resources + +| Resource | URL | +|----------|-----| +| charmlibs docs | .docs/index.md | +| ops (charm framework) | https://documentation.ubuntu.com/ops/ | +| ops.testing reference | https://documentation.ubuntu.com/ops/latest/reference/ops-testing/ | +| Juju docs | https://documentation.ubuntu.com/juju/latest/llms.txt | +| Charmcraft docs | https://documentation.ubuntu.com/charmcraft/stable/ | +| Jubilant (integration test client) | https://documentation.ubuntu.com/jubilant/ | +| Pebble | https://documentation.ubuntu.com/pebble/ | +| Concierge (CI Juju setup) | https://raw.githubusercontent.com/canonical/concierge/refs/heads/main/README.md | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8990deb5e..46e7df479 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,7 @@ +# Contributing to charmlibs + +This monorepo contains Python libraries for [Juju](https://canonical.com/juju) charms. General libraries are defined in top-level subdirectories, and interface libraries and/or metadata are defined under the interfaces directory. + # Quick start This project uses [just](https://github.com/casey/just) as a task runner, backed by [uv](https://github.com/astral-sh/uv). @@ -9,7 +13,7 @@ sudo snap install --classic astral-uv uv tool install rust-just ``` -Then run `just` from anywhere in the repository for usage. +Then run `just` or `just help` from anywhere in the repository for usage. # Adding a new library @@ -19,27 +23,48 @@ If you're migrating a library that was published elsewhere, read the [how-to gui # Working on an existing library -Run `just check ` to run the following tests for your package: +Run `just check ` to run the primary checks for your package. This runs linting, unit tests, and builds the reference docs: -- `just lint ` runs linters and static type checkers. - - `just fast-lint` will run fast linters for all packages. - - `just format ` or `just format` will try to automatically fix errors. -- `just docs html ` builds the docs, only including reference docs for `` (for speed). - - `just docs html` or `just docs` will build docs for all packages. +- `just lint ` runs `ruff`, `codespell`, and `pyright` on your package. + - `just fast-lint` runs ruff across the whole repository. + - `just format [package]` will try to automatically fix formatting errors. + - `just static ` runs pyright only. - `just unit ` runs unit tests. +- `just docs html ` builds the reference docs for `` only (for speed). + - `just docs html` or `just docs` will build docs for all packages. + +## Adding dependencies + +Use `just add ` to add a dependency to a library, rather than calling `uv add` directly. This applies repo-level version constraints from `test-requirements.txt`, keeping the lockfile consistent. + +```bash +just add pathops 'pydantic>=2' +``` + +## Functional and integration tests -`functional` and `integration` tests are also executed in CI, and can be executed locally too. +`functional` and `integration` tests are also executed in CI, and can be run locally too. They're excluded from `just check` as they may require additional setup: -Read more: +- `just functional ` runs functional tests, which interact with real external processes (but not Juju itself). Some functional test suites may require additional software installed locally, like `pebble` or `sudo` access. +- Integration tests involve packing real test charms and deploying them on a Juju cloud. Pack first with `just pack-k8s ` or `just pack-machine `, then run `just integration-k8s ` or `just integration-machine `. -- [The different types of tests](https://documentation.ubuntu.com/charmlibs/explanation/charmlibs-tests/). -- [Publishing packages](https://documentation.ubuntu.com/charmlibs/explanation/charmlibs-publishing/). +Read more: [the different types of tests](https://documentation.ubuntu.com/charmlibs/explanation/charmlibs-tests/). # Pull requests -Pull request titles must follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). +PRs are squash-merged, so your PR title becomes the single commit message that lands on `main`. Commit messages on your branch don't need to follow any particular format. + +PR titles must follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). When a PR affects a single library, use the distribution package name without the leading `charmlibs-` or `charmlibs-interfaces` as the scope: + +``` +feat(pathops): add copytree and rmtree +chore(tls-certificates): update to Pydantic v2 +``` + +## Versioning and releases + +Libraries are automatically published to PyPI when a merged PR bumps the version to a non-dev version. Dev versions (like `1.0.0.dev0`) are excluded from the release CI, so you can safely merge in-progress work with a dev version. -When a PR affects a single library, use the distribution package name without the leading `charmlibs-` as the conventional commit scope. +Any PR that would trigger a release must also update the library's `CHANGELOG.md` — CI will block the merge otherwise. -For example: -`feat(pathops): ...` or `chore(interfaces-tls-certificates): ...`. +Read more: [publishing packages from the monorepo](https://documentation.ubuntu.com/charmlibs/explanation/charmlibs-publishing/). diff --git a/README.md b/README.md index 891809ccb..e9d1762b1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,64 @@ # charmlibs -This monorepo hosts the source code for Canonical's charm libraries. Read the [docs](https://documentation.ubuntu.com/charmlibs) for more. +`charmlibs` is the home of Canonical's charm libraries -- Python packages used by [Juju](https://canonical.com/juju) charms. -To get started with contributing, read [CONTRIBUTING.md](./CONTRIBUTING.md). +Charms are Python programs that use the [Ops](https://documentation.ubuntu.com/ops/) framework to manage workloads on Kubernetes or machine clouds. Charm libraries package up common functionality so that teams don't have to reinvent the wheel. + +> [!IMPORTANT] +> Each library in this monorepo is distributed as a separate Python package on PyPI, so you charms only include what they actually need. + +There are two kinds of charm libraries: + +- **General libraries** (such as [`charmlibs-apt`](apt/), [`charmlibs-pathops`](pathops/)) provide utility APIs for charms. Imported as `from charmlibs import apt`. +- **Interface libraries** (such as [`charmlibs-interfaces-tls-certificates`](interfaces/tls-certificates/)) manage the structured data that charms exchange over a Juju relation. Imported as `from charmlibs.interfaces import tls_certificates`. + +## Contributing to this monorepo + +`charmlibs` is for libraries that are broadly useful across different charms and teams. A library is a good fit if it both: + +- **Solves a common problem**: It is useful to charms across multiple products or teams, not just your own. +- **Has a design or a proven track record**: Either a specification with cross-team buy-in, or a pattern already tested in production. (Migrating an existing, widely-used Charmhub-hosted library doesn't need a separate specification.) + +All public interfaces intended for use by other charms should have a corresponding `charmlibs.interfaces` library. + +If your library is more of a team-internal utility, [Distribute charm libraries](https://documentation.ubuntu.com/charmlibs/how-to/python-package/) covers the alternatives. + +Got something in mind? You can talk to us on [Matrix](https://matrix.to/#/#charmhub-charmdev:ubuntu.com) before opening a PR — we'd love to hear about it. + +**Ready to dive in?** Follow the **[tutorial](https://documentation.ubuntu.com/charmlibs/tutorial)** to add a new library, or the **[migration guide](https://documentation.ubuntu.com/charmlibs/how-to/migrate/)** to port a Charmhub-hosted library. See [CONTRIBUTING.md](./CONTRIBUTING.md) for the developer quick-reference. + +For the details of how the monorepo works: + +- [Types of tests](https://documentation.ubuntu.com/charmlibs/explanation/charmlibs-tests/): unit, functional, and Juju integration tests +- [Publishing from the monorepo](https://documentation.ubuntu.com/charmlibs/explanation/charmlibs-publishing/): semantic versioning, dev versions, trusted publishing +- Customizing your [functional](https://documentation.ubuntu.com/charmlibs/how-to/customize-functional-tests/) and [integration](https://documentation.ubuntu.com/charmlibs/how-to/customize-integration-tests/) tests. + +## Writing charm libraries + +[Distribute charm libraries](https://documentation.ubuntu.com/charmlibs/how-to/python-package/) walks through the distribution options for a new library — git dependencies, PyPI, and this monorepo — and how to choose between them. + +For interface libraries specifically: + +- [Design relation interfaces](https://documentation.ubuntu.com/charmlibs/how-to/design-relation-interfaces/) — rules and patterns for backwards-compatible relation data formats +- [Provide relation data for charm tests](https://documentation.ubuntu.com/charmlibs/how-to/provide-relation-data-for-charm-tests/) — how to write the testing subpackage for your interface library + +## Using libraries in a charm + +Browse the library listings to find what you need: + +- [General library listing](https://documentation.ubuntu.com/charmlibs/reference/general-libs/) +- [Interface library listing](https://documentation.ubuntu.com/charmlibs/reference/interface-libs/) + +The [Manage charm libraries](https://documentation.ubuntu.com/charmlibs/how-to/manage-libraries/) guide covers adding a library to your charm, version constraints, git dependencies, and using legacy Charmhub-hosted libraries. + +We also host the library reference docs: + +- [charmlibs package reference](https://documentation.ubuntu.com/charmlibs/reference/charmlibs/) — `apt`, `pathops`, `snap`, and more +- [charmlibs.interfaces package reference](https://documentation.ubuntu.com/charmlibs/reference/charmlibs-interfaces/) — `tls_certificates`, `tracing`, and more +- [Interface specifications](https://documentation.ubuntu.com/charmlibs/reference/interfaces/) — relation data schemas and docs for each interface + +Read more in the docs: [Charm libraries explained](https://documentation.ubuntu.com/charmlibs/explanation/charm-libs/) + +--- + +New to charming? The [ops documentation](https://documentation.ubuntu.com/ops/) is the best place to start. From 6b920669f7e633b4b820461ab90a0decd015864a Mon Sep 17 00:00:00 2001 From: James Garner Date: Tue, 26 May 2026 16:39:13 +1200 Subject: [PATCH 21/65] docs: charmcraft fetch-libs deprecation (#491) This PR adds a docs page about the upcoming deprecation of the Charmhub-hosted library machinery. This page will be pointed to from all `charmcraft` library operations in an upcoming Charmcraft release (https://github.com/canonical/charmcraft/pull/2691). Due to the upcoming docs migration, I suggest we stick with the `ubu.link` approach suggested by Alex. So Charmcraft will show `https://ubu.link/charmhub-libraries-deprecation`, linking to `https://documentation.ubuntu.com/charmlibs/explanation/charmhub-libraries-deprecation/`. When we migrate the docs, we can delete the `ubu.link` and recreate it pointing to the new location. --------- Co-authored-by: Dave Wilding --- .../charmhub-libraries-deprecation.md | 68 +++++++++++++++++++ .docs/explanation/index.md | 1 + 2 files changed, 69 insertions(+) create mode 100644 .docs/explanation/charmhub-libraries-deprecation.md diff --git a/.docs/explanation/charmhub-libraries-deprecation.md b/.docs/explanation/charmhub-libraries-deprecation.md new file mode 100644 index 000000000..9995af710 --- /dev/null +++ b/.docs/explanation/charmhub-libraries-deprecation.md @@ -0,0 +1,68 @@ +--- +myst: + html_meta: + description: Charmhub-hosted charm libraries fetched with charmcraft fetch-libs are being phased out in favour of standard Python packages. +--- + +(charmhub-libraries-deprecation)= +# Charmhub-hosted libraries are being phased out + +Charmhub-hosted charm libraries -- single-file Python modules fetched with `charmcraft fetch-libs` -- are being phased out in favour of standard Python packages distributed on PyPI. + +## Background + +Charms originally shared code by distributing single-file Python modules on Charmhub. Each module was associated with a specific charm. Charms fetched libraries using the `charmcraft fetch-lib` and `charmcraft fetch-libs` commands, and typically checked them into their version control. These libraries were stored under `lib/`, which was added to `PYTHONPATH` at runtime, allowing the module to be imported by charm name and API version. For example `from operator_libs_linux.v2 import snap`. + +## Timeline + +- `charmcraft` emits a deprecation warning on library operations (we are here). +- Charmhub disables uploading *new* libraries (26.10 cycle). +- Charmhub disables updates to existing libraries. + +## Why the change? + +### No dependency resolution + +Charmhub-hosted libraries have no mechanism for resolving dependencies on other Python packages. If a library needs a dependency, it lists the package name in a `PYDEPS` variable, and the charm author must manually add each dependency to their charm's own requirements. That is, transitive dependencies of Charmhub-hosted libraries are tracked manually, and the charm author is also responsible for resolving a compatible version for the different libraries that might require that dependency. + +Standard Python packages, by contrast, declare their dependencies in metadata that tools like `pip` and `uv` resolve automatically. + +### Vendored code is hard to maintain + +Because Charmhub libraries are committed into each charm's repository, updating a library means re-running `charmcraft fetch-libs` and committing the result. Across many charms, this creates a maintenance burden and increases the risk of running outdated code. + +### Limited tooling integration + +Charmhub libraries do not participate in standard Python tooling. This makes it trickier to ensure their visibility for dependency scanners and vulnerability checkers, leads to the library itself not being visible in the charm's lock file, and requires extra work to make the library visible in IDEs. + +## What replaces it? + +Charm libraries should be distributed as regular Python packages, typically on PyPI. +Libraries of broad interest are published under the `charmlibs` namespace from the [`charmlibs` monorepo](https://github.com/canonical/charmlibs). Team-specific libraries can be distributed on PyPI under their own namespace, as Git dependencies, or as local packages in a charm monorepo. + +Read more: {ref}`how-to-python-package` + +## What should I do? + +### If you maintain a Charmhub-hosted library + +Migrate it to a Python package. If it implements a widely-used interface or provides broadly useful functionality, it belongs in the `charmlibs` monorepo. + +Read more: {ref}`how-to-migrate` + +### If you use Charmhub-hosted libraries in your charm + +When a Python package replacement is available, switch to it: + +1. Remove the library from the `charm-libs` section of your `charmcraft.yaml` and delete the vendored directory from `lib/`. +2. Remove any manually added transitive dependencies that are no longer needed. +3. Add the replacement package to your charm's dependencies. +4. Update your imports to use the new package's import path. + +Read more: {ref}`how-to-manage-charm-libraries` + +### If you are writing a new library + +Create it as a Python package from the start. Do not publish new libraries on Charmhub. + +Read more: {ref}`how-to-python-package` diff --git a/.docs/explanation/index.md b/.docs/explanation/index.md index 4e36f194e..3a96f9533 100644 --- a/.docs/explanation/index.md +++ b/.docs/explanation/index.md @@ -4,6 +4,7 @@ :maxdepth: 1 charm-libs +charmhub-libraries-deprecation charmlibs: Types of tests charmlibs: Publishing packages ``` From 86f8b61bc21d7b3dfff3b481abb34c6525d6b076 Mon Sep 17 00:00:00 2001 From: James Garner Date: Tue, 26 May 2026 23:53:08 +1200 Subject: [PATCH 22/65] docs: include RHS TOCs in custom build process (#495) This PR fixes the custom extension that we use to combine the individually built package reference docs into the full docs site. It now correctly includes the right hand side table of contents in the rendered pages, thanks to our good friend Claude. Resolves #496 --- .docs/extensions/package_docs.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.docs/extensions/package_docs.py b/.docs/extensions/package_docs.py index d821c4f4f..5b0f385a0 100644 --- a/.docs/extensions/package_docs.py +++ b/.docs/extensions/package_docs.py @@ -58,7 +58,7 @@ def _load_on_doctree_read(app: sphinx.application.Sphinx, doctree: docutils.node return if not (source := pathlib.Path('.save', f'{app.env.docname}.pickle')).exists(): return - saved, objects, modules = pickle.loads(source.read_bytes()) # noqa: S301 + saved, objects, modules, toc, toc_num_entries = pickle.loads(source.read_bytes()) # noqa: S301 # restore saved doctree doctree.clear() for node in saved.children: @@ -66,6 +66,10 @@ def _load_on_doctree_read(app: sphinx.application.Sphinx, doctree: docutils.node # restore domain inventory for cross-refs app.env.domains['py'].data['objects'].update(objects) app.env.domains['py'].data['modules'].update(modules) + # restore TOC so the RHS table of contents is populated + docname = app.env.docname + app.env.tocs[docname] = toc + app.env.toc_num_entries[docname] = toc_num_entries def _save_on_doctree_resolved( @@ -79,9 +83,11 @@ def _save_on_doctree_resolved( return objects = app.env.domains['py'].data['objects'] modules = app.env.domains['py'].data['modules'] + toc = app.env.tocs[docname] + toc_num_entries = app.env.toc_num_entries[docname] target = pathlib.Path('.save', f'{docname}.pickle') target.parent.mkdir(exist_ok=True, parents=True) - target.write_bytes(pickle.dumps((doctree, objects, modules))) + target.write_bytes(pickle.dumps((doctree, objects, modules, toc, toc_num_entries))) #################### From 4ddd3ca7763a5bb9492748931a0f75a3c926eaf3 Mon Sep 17 00:00:00 2001 From: James Garner Date: Wed, 27 May 2026 16:33:19 +1200 Subject: [PATCH 23/65] test(pathops): fix functional test breakage when running locally (#488) The `charmlibs.pathops` functional tests are failing locally on my system due to users / groups being managed by NSS. This won't be an issue in production, as `pathops` is only used to access file info via Pebble for charms managing K8s workload containers (where Pebble runs as root rather than as an NSS-managed user), so this PR updates the tests to pass locally. Resolves #487. --- pathops/tests/functional/conftest.py | 12 +++++++ .../tests/functional/test_container_path.py | 16 +++++++-- pathops/tests/functional/test_functions.py | 13 +++++-- pathops/tests/functional/test_local_path.py | 34 +++++++++++++++---- 4 files changed, 65 insertions(+), 10 deletions(-) diff --git a/pathops/tests/functional/conftest.py b/pathops/tests/functional/conftest.py index 62088a4d1..178ca07ba 100644 --- a/pathops/tests/functional/conftest.py +++ b/pathops/tests/functional/conftest.py @@ -40,6 +40,18 @@ def container() -> ops.Container: return _make_container('test1') +@pytest.fixture(scope='session') +def pebble_resolves_groups(container: ops.Container, session_dir: pathlib.Path) -> bool: + """Whether Pebble can resolve GIDs to group names on this system. + + Pebble uses Go's os/user package which doesn't support NSS/SSSD, so group + resolution fails when groups come from LDAP rather than /etc/group. + See: https://github.com/canonical/pebble/issues/868 + """ + info = container.list_files(str(session_dir), itself=True)[0] + return info.group != '' + + @pytest.fixture(scope='session') def another_container() -> ops.Container: return _make_container('test2') diff --git a/pathops/tests/functional/test_container_path.py b/pathops/tests/functional/test_container_path.py index 7cdd48dc3..d351bf292 100644 --- a/pathops/tests/functional/test_container_path.py +++ b/pathops/tests/functional/test_container_path.py @@ -262,8 +262,14 @@ def test_bad_empty_pattern(self, container: ops.Container, session_dir: pathlib. @pytest.mark.parametrize('filename', utils.FILENAMES_PLUS) @pytest.mark.parametrize('method', ['owner', 'group']) def test_owner_and_group( - container: ops.Container, session_dir: pathlib.Path, method: str, filename: str + container: ops.Container, + session_dir: pathlib.Path, + pebble_resolves_groups: bool, + method: str, + filename: str, ): + if method == 'group' and not pebble_resolves_groups: + pytest.xfail('Pebble cannot resolve LDAP/SSSD groups') path = session_dir / filename pathlib_method = getattr(path, method) container_path = ContainerPath(path, container=container) @@ -493,8 +499,14 @@ def test_user_only_ok( assert (path.owner(), path.group()) == (user, group) def test_user_and_group_ok( - self, container: ops.Container, tmp_path: pathlib.Path, method: Callable[..., None] + self, + container: ops.Container, + tmp_path: pathlib.Path, + pebble_resolves_groups: bool, + method: Callable[..., None], ): + if not pebble_resolves_groups: + pytest.xfail('Pebble cannot resolve LDAP/SSSD groups') user = getpass.getuser() group = grp.getgrgid(pwd.getpwnam(user).pw_gid).gr_name path = tmp_path / 'filename' diff --git a/pathops/tests/functional/test_functions.py b/pathops/tests/functional/test_functions.py index b76c7e870..75a486382 100644 --- a/pathops/tests/functional/test_functions.py +++ b/pathops/tests/functional/test_functions.py @@ -83,8 +83,15 @@ def test_ensure_contents( @pytest.mark.parametrize('follow_symlinks', [True, False]) @pytest.mark.parametrize('filename', utils.FILENAMES_PLUS) def test_get_fileinfo( - container: ops.Container, session_dir: pathlib.Path, filename: str, follow_symlinks: bool + container: ops.Container, + session_dir: pathlib.Path, + pebble_resolves_groups: bool, + filename: str, + follow_symlinks: bool, ): + exclude: list[str] = [] + if not pebble_resolves_groups: + exclude.append('group') path = session_dir / filename try: pebble_result = _get_fileinfo( @@ -95,4 +102,6 @@ def test_get_fileinfo( _get_fileinfo(path, follow_symlinks=follow_symlinks) else: synthetic_result = _get_fileinfo(path, follow_symlinks=follow_symlinks) - assert utils.info_to_dict(synthetic_result) == utils.info_to_dict(pebble_result) + synthetic_dict = utils.info_to_dict(synthetic_result, exclude=exclude) + pebble_dict = utils.info_to_dict(pebble_result, exclude=exclude) + assert synthetic_dict == pebble_dict diff --git a/pathops/tests/functional/test_local_path.py b/pathops/tests/functional/test_local_path.py index 763bc6a02..3db59ce07 100644 --- a/pathops/tests/functional/test_local_path.py +++ b/pathops/tests/functional/test_local_path.py @@ -142,6 +142,7 @@ def test_unknown_group_raises_before_other_errors( def test_write_methods_chmod( container: ops.Container, tmp_path: pathlib.Path, + pebble_resolves_groups: bool, method: str, data: str | bytes, mode_str: str | None, @@ -175,7 +176,9 @@ def test_write_methods_chmod( local_info = _get_fileinfo(local_path) # cleanup _unlink(path) - exclude = 'last_modified' + exclude = ['last_modified'] + if not pebble_resolves_groups: + exclude.append('group') container_dict = utils.info_to_dict(container_info, exclude=exclude) local_dict = utils.info_to_dict(local_info, exclude=exclude) assert local_dict == container_dict @@ -189,7 +192,13 @@ def test_write_methods_chmod( @pytest.mark.parametrize('mode_str', [*ALL_MODES, None]) class TestMkdirChmod: - def test_ok(self, container: ops.Container, tmp_path: pathlib.Path, mode_str: str | None): + def test_ok( + self, + container: ops.Container, + tmp_path: pathlib.Path, + pebble_resolves_groups: bool, + mode_str: str | None, + ): mode = int(mode_str, base=8) if mode_str is not None else None path = tmp_path / 'directory' # container @@ -217,13 +226,21 @@ def test_ok(self, container: ops.Container, tmp_path: pathlib.Path, mode_str: st # cleanup -- pytest is bad at cleaning up when permissions are funky _rmdirs(path) # comparison - exclude = ('last_modified', 'permissions') + exclude = ['last_modified', 'permissions'] + if not pebble_resolves_groups: + exclude.append('group') container_dict = utils.info_to_dict(container_info, exclude=exclude) local_dict = utils.info_to_dict(local_info, exclude=exclude) assert local_dict == container_dict assert _oct(local_info.permissions) == _oct(container_info.permissions) - def test_exists(self, container: ops.Container, tmp_path: pathlib.Path, mode_str: str | None): + def test_exists( + self, + container: ops.Container, + tmp_path: pathlib.Path, + pebble_resolves_groups: bool, + mode_str: str | None, + ): mode = int(mode_str, base=8) if mode_str is not None else None path = tmp_path / 'directory' path.mkdir() @@ -244,7 +261,9 @@ def test_exists(self, container: ops.Container, tmp_path: pathlib.Path, mode_str # cleanup -- pytest is bad at cleaning up when permissions are funky _rmdirs(path) # comparison - exclude = ('last_modified', 'permissions') + exclude = ['last_modified', 'permissions'] + if not pebble_resolves_groups: + exclude.append('group') container_dict = utils.info_to_dict(container_info, exclude=exclude) local_dict = utils.info_to_dict(local_info, exclude=exclude) assert local_dict == container_dict @@ -255,6 +274,7 @@ def test_parents( self, container: ops.Container, tmp_path: pathlib.Path, + pebble_resolves_groups: bool, mode_str: str | None, subdir_path: str, ): @@ -298,7 +318,9 @@ def test_parents( # cleanup -- pytest is bad at cleaning up when permissions are funky _rmdirs(path, *parents) # comparison - exclude = ('last_modified', 'permissions') + exclude = ['last_modified', 'permissions'] + if not pebble_resolves_groups: + exclude.append('group') container_dict = utils.info_to_dict(container_info, exclude=exclude) local_dict = utils.info_to_dict(local_info, exclude=exclude) assert local_dict == container_dict From 72f63dab0767ac0ce129bf3f750c2c7c298b924f Mon Sep 17 00:00:00 2001 From: Patricia Reinoso Date: Wed, 27 May 2026 14:24:52 +0200 Subject: [PATCH 24/65] fix(rollingops): dynamic cluster id integration with etcd and sync lock (#481) ## Description This PR fixes a couple of issues found in the rollingops library: 1. Dynamic cluster ID integration with etcd does not work well. Turns out that the certificates needed to connect to etcd were not created in case the cluster ID is not provided since deployment. Now they are created when the _EtcdRollingOpsBackend is instantiated. Fix https://github.com/issues/assigned?issue=canonical%7Ccharmlibs%7C480 2. The sync lock had a weird behavior in case of exception raised during the execution of the critical path. Now: - we separate the exception handling between lock acquisition and execution of critical path - the certificates to communicate to etcd are not removed during relation broken with etcd, and we do not fall back to peer relation back end on relation broken. This allows the unit to still use etcd during the teardown phase. If the relation was removed from the application and we are not in a scale down situation, the rollingops manager will automatically fallback to peer backend later because the relation would be missing --- rollingops/CHANGELOG.md | 6 + .../charmlibs/rollingops/_etcd/_backend.py | 17 -- .../charmlibs/rollingops/_etcd/_relations.py | 2 +- .../rollingops/_rollingops_manager.py | 38 ++--- .../src/charmlibs/rollingops/_version.py | 2 +- rollingops/tests/unit/conftest.py | 53 +++++- .../unit/test_etcd_rollingops_in_charm.py | 151 +++++++++++++++++- 7 files changed, 229 insertions(+), 40 deletions(-) diff --git a/rollingops/CHANGELOG.md b/rollingops/CHANGELOG.md index a9c7ae807..2f0aa3c75 100644 --- a/rollingops/CHANGELOG.md +++ b/rollingops/CHANGELOG.md @@ -1,3 +1,9 @@ +# 1.1.1 - 22 May 2026 + +Fix: +- Dynamic cluster ID integration with etcd +- Sync lock exception handling during critical path execution + # 1.1.0 - 20 May 2026 Extend the `RollingOpsManager` is_waiting_* helpers to receive a unit name. diff --git a/rollingops/src/charmlibs/rollingops/_etcd/_backend.py b/rollingops/src/charmlibs/rollingops/_etcd/_backend.py index 0de311879..2815381fe 100644 --- a/rollingops/src/charmlibs/rollingops/_etcd/_backend.py +++ b/rollingops/src/charmlibs/rollingops/_etcd/_backend.py @@ -20,7 +20,6 @@ from ops.charm import ( CharmBase, RelationCreatedEvent, - RelationDepartedEvent, ) from charmlibs import pathops @@ -129,9 +128,6 @@ def __init__( self.keys, owner, base_dir=self._base_dir, charm_dir=charm_dir ) - self.framework.observe( - charm.on[self.peer_relation_name].relation_departed, self._on_peer_relation_departed - ) self.framework.observe( charm.on[self.etcd_relation_name].relation_created, self._on_etcd_relation_created ) @@ -214,19 +210,6 @@ def _on_etcd_relation_created(self, event: RelationCreatedEvent) -> None: if not self.etcdctl.is_etcdctl_installed(): logger.error('%s is not installed.', ETCDCTL_CMD) - def _on_peer_relation_departed(self, event: RelationDepartedEvent) -> None: - """Handle removal of a unit from the peer relation. - - If the current unit is departing, the etcd worker process is stopped - to ensure a clean shutdown and avoid leaving a stale worker running. - - Args: - event: The peer relation departed event. - """ - unit = event.departing_unit - if unit == self.model.unit: - self.worker.stop() - def request_async_lock( self, callback_id: str, diff --git a/rollingops/src/charmlibs/rollingops/_etcd/_relations.py b/rollingops/src/charmlibs/rollingops/_etcd/_relations.py index 38f104b6e..1dff6a4d2 100644 --- a/rollingops/src/charmlibs/rollingops/_etcd/_relations.py +++ b/rollingops/src/charmlibs/rollingops/_etcd/_relations.py @@ -112,7 +112,7 @@ def create_and_share_certificate(self) -> None: app_data = relation.data[self.model.app] if app_data.get(CERT_SECRET_FIELD): - logger.info( + logger.debug( 'Shared certificate already exists in the databag. No new certificate is created.' ) return diff --git a/rollingops/src/charmlibs/rollingops/_rollingops_manager.py b/rollingops/src/charmlibs/rollingops/_rollingops_manager.py index 5a21a2e41..e786b2d57 100644 --- a/rollingops/src/charmlibs/rollingops/_rollingops_manager.py +++ b/rollingops/src/charmlibs/rollingops/_rollingops_manager.py @@ -18,7 +18,7 @@ from contextlib import contextmanager from typing import Any -from ops import CharmBase, Object, Relation, RelationBrokenEvent, Unit +from ops import CharmBase, Object, Relation, Unit from ops.framework import EventBase from charmlibs import pathops @@ -168,10 +168,7 @@ def __init__( callback_targets=callback_targets, base_dir=base_dir, ) - self.framework.observe( - charm.on[etcd_relation_name].relation_broken, - self._on_etcd_relation_broken, - ) + self._etcd_backend.shared_certificates.create_and_share_certificate() self.framework.observe(charm.on.rollingops_lock_granted, self._on_rollingops_lock_granted) self.framework.observe(charm.on.rollingops_etcd_failed, self._on_rollingops_etcd_failed) @@ -192,14 +189,6 @@ def _backend_state(self) -> _UnitBackendState: """ return _UnitBackendState(self.model, self.peer_relation_name, self.model.unit) - def _on_etcd_relation_broken(self, event: RelationBrokenEvent) -> None: - """Handle the etcd relation being fully removed. - - This method stops the etcd worker process since the required - relation is no longer available. - """ - self._fallback_current_unit_to_peer() - def _select_processing_backend(self) -> ProcessingBackend: """Choose which backend should handle new operations for this unit. @@ -442,13 +431,13 @@ def acquire_sync_lock(self, backend_id: str, timeout: int): TimeoutError: If lock acquisition through etcd or the peer backend times out. RollingOpsSyncLockError: if there is an error when acquiring the lock. + Any exception raised within the protected block is propagated, and the + lock is released before propagation. """ if self._etcd_backend is not None and self._etcd_backend.is_available(): logger.info('Acquiring sync lock on etcd.') try: self._etcd_backend.acquire_sync_lock(timeout) - yield - return except TimeoutError: raise except Exception as e: @@ -457,12 +446,20 @@ def acquire_sync_lock(self, backend_id: str, timeout: int): 'Failed to request etcd sync lock; falling back to peer: %s', e, ) - finally: + else: try: - self._etcd_backend.release_sync_lock() - logger.info('etcd lock released.') + # Separate lock acquisition errors from errors raised while holding the lock. + yield except Exception as e: - logger.exception('Failed to release sync lock: %s', e) + logger.exception('Error while holding etcd sync lock: %s', e) + raise + finally: + try: + self._etcd_backend.release_sync_lock() + logger.info('etcd lock released.') + except Exception as e: + logger.exception('Failed to release sync lock: %s', e) + return backend = self._get_sync_lock_backend(backend_id) logger.info('Acquiring sync lock backend %s.', backend_id) @@ -475,6 +472,9 @@ def acquire_sync_lock(self, backend_id: str, timeout: int): try: yield + except Exception as e: + logger.exception('Error while holding sync lock backend %s: %s', backend_id, e) + raise finally: try: backend.release() diff --git a/rollingops/src/charmlibs/rollingops/_version.py b/rollingops/src/charmlibs/rollingops/_version.py index f9e828ee1..82bead933 100644 --- a/rollingops/src/charmlibs/rollingops/_version.py +++ b/rollingops/src/charmlibs/rollingops/_version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = '1.1.0' +__version__ = '1.1.1' diff --git a/rollingops/tests/unit/conftest.py b/rollingops/tests/unit/conftest.py index 9741b918d..4dba3b98b 100644 --- a/rollingops/tests/unit/conftest.py +++ b/rollingops/tests/unit/conftest.py @@ -14,6 +14,7 @@ """Fixtures for unit tests, typically mocking out parts of the external system.""" +import time from collections.abc import Generator from pathlib import Path from typing import Any @@ -31,7 +32,7 @@ Certificate, PrivateKey, ) -from charmlibs.rollingops import RollingOpsManager +from charmlibs.rollingops import RollingOpsManager, SyncLockBackend from charmlibs.rollingops._common._models import OperationResult from charmlibs.rollingops._etcd._models import SharedCertificate @@ -144,6 +145,16 @@ def certificates_manager_patches() -> Generator[dict[str, MagicMock], None, None } +class DummySyncLockBackend(SyncLockBackend): + STOP_REPLSET_MEMBER = 'stop-replset-member' + + def acquire(self, timeout: int | None) -> None: + return None + + def release(self) -> None: + return None + + class RollingOpsCharm(ops.CharmBase): def __init__(self, framework: ops.Framework): super().__init__(framework) @@ -160,10 +171,15 @@ def __init__(self, framework: ops.Framework): etcd_relation_name='etcd', cluster_id='cluster-12345', callback_targets=callback_targets, + sync_lock_targets={DummySyncLockBackend.STOP_REPLSET_MEMBER: DummySyncLockBackend()}, ) self.framework.observe(self.on.restart_action, self._on_restart_action) self.framework.observe(self.on.failed_restart_action, self._on_failed_restart_action) self.framework.observe(self.on.deferred_restart_action, self._on_deferred_restart_action) + self.framework.observe( + self.on.failed_sync_restart_action, self._on_failed_sync_restart_action + ) + self.framework.observe(self.on.sync_restart_action, self.on_sync_restart_action) def _on_restart_action(self, event: ActionEvent) -> None: delay = event.params.get('delay') @@ -187,6 +203,23 @@ def _on_deferred_restart_action(self, event: ActionEvent) -> None: max_retry=max_retry, ) + def _on_failed_sync_restart_action(self, event: ActionEvent) -> None: + timeout = int(event.params.get('timeout', 30)) + with self.restart_manager.acquire_sync_lock( + backend_id=DummySyncLockBackend.STOP_REPLSET_MEMBER, + timeout=timeout, + ): + raise ValueError('Simulated failure in sync lock callback') + + def on_sync_restart_action(self, event: ActionEvent) -> None: + timeout = int(event.params.get('timeout', 30)) + delay = int(event.params.get('delay', 0)) + with self.restart_manager.acquire_sync_lock( + backend_id=DummySyncLockBackend.STOP_REPLSET_MEMBER, + timeout=timeout, + ): + time.sleep(delay) + def _restart(self) -> None: pass @@ -255,6 +288,24 @@ def charm_test() -> type[RollingOpsCharm]: }, }, }, + 'failed-sync-restart': { + 'description': 'Example restart that simulates a failure in a sync lock callback.', + }, + 'sync-restart': { + 'description': 'Example restart that acquires a sync lock. Used in testing', + 'params': { + 'delay': { + 'description': 'Amount of time in seconds to sleep while holding the sync lock.', + 'type': 'integer', + 'default': 0, + }, + 'timeout': { + 'description': 'Timeout in seconds for sync lock acquisition.', + 'type': 'integer', + 'default': 30, + }, + }, + }, } diff --git a/rollingops/tests/unit/test_etcd_rollingops_in_charm.py b/rollingops/tests/unit/test_etcd_rollingops_in_charm.py index 4b7003184..d2f3420cd 100644 --- a/rollingops/tests/unit/test_etcd_rollingops_in_charm.py +++ b/rollingops/tests/unit/test_etcd_rollingops_in_charm.py @@ -17,7 +17,7 @@ from unittest.mock import MagicMock, patch import pytest -from ops.testing import Context, PeerRelation, Secret, State +from ops.testing import Context, PeerRelation, Relation, Secret, State from scenario import RawDataBagContents from scenario.errors import UncaughtCharmError from tests.unit.conftest import ( @@ -497,3 +497,152 @@ def test_is_waiting_returns_false_when_no_operations_in_unit( with ctx(ctx.on.update_status(), state) as mgr: assert mgr.charm.restart_manager.is_waiting_callback('restart', 'charm/1') is False assert mgr.charm.restart_manager.is_waiting('charm/1') is False + + +def test_sync_lock_request_failed_critical_path_using_etcd_lock( + ctx: Context[RollingOpsCharm], +): + peer = PeerRelation(endpoint='restart') + etcd_relation = Relation( + endpoint='etcd', + interface='rollingops', + ) + state_in = State(leader=False, relations={peer, etcd_relation}) + + with ( + patch( + 'charmlibs.rollingops._etcd._backend._EtcdRollingOpsBackend.is_available', + return_value=True, + ) as mock_is_available, + patch( + 'charmlibs.rollingops._etcd._backend._EtcdRollingOpsBackend.acquire_sync_lock', + ) as mock_acquire_sync_lock, + patch( + 'charmlibs.rollingops._etcd._backend._EtcdRollingOpsBackend.release_sync_lock', + ) as mock_release_sync_lock, + ): + with pytest.raises(UncaughtCharmError) as exc_info: + ctx.run( + ctx.on.action('failed-sync-restart'), + state_in, + ) + + assert isinstance(exc_info.value.__cause__, ValueError) + mock_is_available.assert_called_once() + mock_acquire_sync_lock.assert_called_once_with(30) + mock_release_sync_lock.assert_called_once() + + +def test_sync_lock_fallbacks_to_peer_backend_on_etcd_error( + ctx: Context[RollingOpsCharm], +): + peer = PeerRelation(endpoint='restart') + etcd_relation = Relation( + endpoint='etcd', + interface='rollingops', + ) + state_in = State(leader=False, relations={peer, etcd_relation}) + + mock_backend = MagicMock() + mock_backend.acquire = MagicMock() + mock_backend.release = MagicMock() + + with ( + patch( + 'charmlibs.rollingops._etcd._backend._EtcdRollingOpsBackend.is_available', + return_value=True, + ) as mock_is_available, + patch( + 'charmlibs.rollingops._etcd._backend._EtcdRollingOpsBackend.acquire_sync_lock', + side_effect=Exception('etcd failure'), + ) as mock_etcd_acquire, + patch( + 'charmlibs.rollingops._rollingops_manager.RollingOpsManager._get_sync_lock_backend', + return_value=mock_backend, + ), + ): + ctx.run( + ctx.on.action('sync-restart'), + state_in, + ) + + mock_is_available.assert_called_once() + mock_etcd_acquire.assert_called_once() + mock_backend.acquire.assert_called_once_with(timeout=30) + mock_backend.release.assert_called_once() + + +def test_sync_lock_peer_backend_and_failure_on_critical_path_is_propagated( + ctx: Context[RollingOpsCharm], +): + peer = PeerRelation(endpoint='restart') + etcd_relation = Relation( + endpoint='etcd', + interface='rollingops', + ) + state_in = State(leader=False, relations={peer, etcd_relation}) + + mock_backend = MagicMock() + mock_backend.acquire = MagicMock() + mock_backend.release = MagicMock() + + with ( + patch( + 'charmlibs.rollingops._etcd._backend._EtcdRollingOpsBackend.is_available', + return_value=True, + ) as mock_is_available, + patch( + 'charmlibs.rollingops._etcd._backend._EtcdRollingOpsBackend.acquire_sync_lock', + side_effect=Exception('etcd failure'), + ) as mock_etcd_acquire, + patch( + 'charmlibs.rollingops._rollingops_manager.RollingOpsManager._get_sync_lock_backend', + return_value=mock_backend, + ), + ): + with pytest.raises(UncaughtCharmError) as exc_info: + ctx.run( + ctx.on.action('failed-sync-restart'), + state_in, + ) + + assert isinstance(exc_info.value.__cause__, ValueError) + mock_is_available.assert_called_once() + mock_etcd_acquire.assert_called_once() + mock_backend.acquire.assert_called_once_with(timeout=30) + mock_backend.release.assert_called_once() + + +def test_sync_lock_request_timeout_raises( + ctx: Context[RollingOpsCharm], +): + peer = PeerRelation(endpoint='restart') + etcd_relation = Relation( + endpoint='etcd', + interface='rollingops', + ) + state_in = State(leader=False, relations={peer, etcd_relation}) + + with ( + patch( + 'charmlibs.rollingops._etcd._backend._EtcdRollingOpsBackend.is_available', + return_value=True, + ) as mock_is_available, + patch( + 'charmlibs.rollingops._etcd._backend._EtcdRollingOpsBackend.acquire_sync_lock', + side_effect=TimeoutError, + ) as mock_acquire_sync_lock, + patch( + 'charmlibs.rollingops._etcd._backend._EtcdRollingOpsBackend.release_sync_lock', + ) as mock_release_sync_lock, + ): + with pytest.raises(UncaughtCharmError) as exc_info: + ctx.run( + ctx.on.action('sync-restart', params={'timeout': 2}), + state_in, + ) + + assert isinstance(exc_info.value.__cause__, TimeoutError) + mock_is_available.assert_called_once() + mock_acquire_sync_lock.assert_called_once_with(2) + mock_release_sync_lock.assert_not_called() From c591c3582d53e995322695f08b9f4519ba32ffc3 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Thu, 28 May 2026 17:05:00 +1200 Subject: [PATCH 25/65] chore: add 7-day Dependabot cooldown to version updates (#499) Add a 7-day cooldown to the monthly github-actions and pip version updates, matching the rest of the Charm Tech estate. The cooldown gives a window for a freshly published (then potentially yanked) release to be caught before Dependabot opens an update PR. The security-only pip entry (open-pull-requests-limit: 0) intentionally has no cooldown, since security fixes should land promptly. Co-authored-by: Claude Opus 4.7 (1M context) --- .github/dependabot.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 905dd7cef..48e83b4c7 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -9,6 +9,8 @@ updates: interval: "monthly" # This is hard-coded to the 1st of the month labels: - "dependencies" + cooldown: + default-days: 7 # Monthly update to repo wide Python testing dependencies # These are defined in /pyproject.toml and /test-requirements.txt - package-ecosystem: "pip" @@ -17,6 +19,8 @@ updates: interval: "monthly" labels: - "dependencies" + cooldown: + default-days: 7 groups: test-deps: patterns: From 030327cb3b50d992bf062da1af494c456839c5ca Mon Sep 17 00:00:00 2001 From: James Garner Date: Fri, 29 May 2026 00:09:37 +1200 Subject: [PATCH 26/65] test: add workshop support to make functional tests easier to run locally (#500) This PR adds workshop config (and instructions) to make it easier to run functional tests locally. --- .gitignore | 3 +++ .workshop/charmlibs/hooks/setup-base | 12 ++++++++++++ .workshop/charmlibs/sdk.yaml | 3 +++ .workshop/jammy.yaml | 8 ++++++++ .workshop/noble.yaml | 8 ++++++++ .workshop/resolute.yaml | 8 ++++++++ AGENTS.md | 18 +++++++++++++++++- CONTRIBUTING.md | 11 ++++++++++- 8 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 .workshop/charmlibs/hooks/setup-base create mode 100644 .workshop/charmlibs/sdk.yaml create mode 100644 .workshop/jammy.yaml create mode 100644 .workshop/noble.yaml create mode 100644 .workshop/resolute.yaml diff --git a/.gitignore b/.gitignore index dcb59288a..7477985e0 100644 --- a/.gitignore +++ b/.gitignore @@ -182,3 +182,6 @@ cython_debug/ .example/**/uv.lock .tutorial/**/uv.lock interfaces/.example/**/uv.lock + +# Workshop lock file (machine-local state) +.workshop.lock diff --git a/.workshop/charmlibs/hooks/setup-base b/.workshop/charmlibs/hooks/setup-base new file mode 100644 index 000000000..0cc4e7ad2 --- /dev/null +++ b/.workshop/charmlibs/hooks/setup-base @@ -0,0 +1,12 @@ +# Install development tools. +snap install --classic astral-uv +snap install --classic just +snap install --classic pebble + +# uv hardlinks by default, which fails across the workshop mount boundary. +echo UV_LINK_MODE=copy >> /etc/environment +# Avoid clashes with the host's .venv in the mounted project directory. +echo UV_PROJECT_ENVIRONMENT=/tmp/workshop-uv-venv >> /etc/environment + +# Install fish shell for interactive use. +apt-get update -qq && apt-get install -yqq fish || echo "Couldn't install fish for interactive use." diff --git a/.workshop/charmlibs/sdk.yaml b/.workshop/charmlibs/sdk.yaml new file mode 100644 index 000000000..69e5b156b --- /dev/null +++ b/.workshop/charmlibs/sdk.yaml @@ -0,0 +1,3 @@ +name: charmlibs +version: "0.1" +summary: Development tools for the charmlibs monorepo. diff --git a/.workshop/jammy.yaml b/.workshop/jammy.yaml new file mode 100644 index 000000000..b471a2687 --- /dev/null +++ b/.workshop/jammy.yaml @@ -0,0 +1,8 @@ +name: jammy +base: ubuntu@22.04 +sdks: + - name: project-charmlibs +actions: + functional: | + # Override Python with: workshop run --env PYTHON=python3.12 jammy -- functional ... + sudo just python="${PYTHON:-python3}" functional "$@" diff --git a/.workshop/noble.yaml b/.workshop/noble.yaml new file mode 100644 index 000000000..783f40579 --- /dev/null +++ b/.workshop/noble.yaml @@ -0,0 +1,8 @@ +name: noble +base: ubuntu@24.04 +sdks: + - name: project-charmlibs +actions: + functional: | + # Override Python with: workshop run --env PYTHON=python3.12 noble -- functional ... + sudo just python="${PYTHON:-python3}" functional "$@" diff --git a/.workshop/resolute.yaml b/.workshop/resolute.yaml new file mode 100644 index 000000000..35cb8b094 --- /dev/null +++ b/.workshop/resolute.yaml @@ -0,0 +1,8 @@ +name: resolute +base: ubuntu@26.04 +sdks: + - name: project-charmlibs +actions: + functional: | + # Override Python with: workshop run --env PYTHON=python3.13 resolute -- functional ... + sudo just python="${PYTHON:-python3}" functional "$@" diff --git a/AGENTS.md b/AGENTS.md index 70083fd0a..af197f8b7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -91,7 +91,7 @@ The `` argument is the path from the repo root, e.g. `pathops` or `inte | `just format [package]` | Auto-fix ruff and formatting errors | | `just static ` | pyright only | | `just unit ` | Run unit tests | -| `just functional ` | Run functional tests (may need external software available, or to be run with sudo -- and note that this probably indicates a test that's destructive to the local environment (e.g. adding or removing packages)) | +| `just functional ` | Run functional tests (may need external software available, or to be run with sudo -- and note that this probably indicates a test that's destructive to the local environment (e.g. adding or removing packages)). **Do not run functional tests directly on the host.** Use Workshop instead (see below). | | `just pack-k8s ` | Pack K8s test charm(s) for integration tests, running the libraries `tests/integration/pack.sh` script with environment variables set | | `just pack-machine ` | Pack machine test charm(s) for integration tests, as above | | `just integration-k8s ` | Run Juju integration tests, excluding `pytest.mark.machine_only` tests | @@ -129,6 +129,22 @@ A test type is only executed if the corresponding `tests/` subdirectory exists. Read more: [types of tests in the charmlibs monorepo](https://documentation.ubuntu.com/charmlibs/explanation/charmlibs-tests/). +### Running functional tests with Workshop + +Functional tests often require `sudo` and may be destructive to the local environment (e.g. installing or removing system packages). **Never run functional tests directly on the host machine.** Always use [Workshop](https://snapcraft.io/workshop) to run them in an isolated container: + +```bash +workshop run resolute -- functional # Ubuntu 26.04 +workshop run noble -- functional # Ubuntu 24.04 +workshop run jammy -- functional # Ubuntu 22.04 +``` + +Workshop configs are defined in `.workshop/`. The `functional` action runs `sudo just functional "$@"` inside the VM. Extra pytest flags are passed through: + +```bash +workshop run noble -- functional snap -x -k test_install +``` + ## Commit and PR conventions **PRs are squash-merged.** The PR title becomes the single commit message on `main`. Branch commit messages are for local reference, and should follow conventional commits for clarity. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 46e7df479..be9b852bf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,7 +45,16 @@ just add pathops 'pydantic>=2' `functional` and `integration` tests are also executed in CI, and can be run locally too. They're excluded from `just check` as they may require additional setup: -- `just functional ` runs functional tests, which interact with real external processes (but not Juju itself). Some functional test suites may require additional software installed locally, like `pebble` or `sudo` access. +- `just functional ` runs functional tests, which interact with real external processes (but not Juju itself). Some functional test suites may require `sudo` access and may be destructive to the local environment (e.g. installing or removing system packages). Use [Workshop](https://snapcraft.io/workshop) to run them in an isolated container instead of running them directly on your host: + + ```bash + workshop run resolute -- functional # Ubuntu 26.04 + workshop run noble -- functional # Ubuntu 24.04 + workshop run jammy -- functional # Ubuntu 22.04 + ``` + + Extra pytest flags are passed through, e.g. `workshop run noble -- functional snap -x -k test_install`. Workshop configs are in `.workshop/`. + - Integration tests involve packing real test charms and deploying them on a Juju cloud. Pack first with `just pack-k8s ` or `just pack-machine `, then run `just integration-k8s ` or `just integration-machine `. Read more: [the different types of tests](https://documentation.ubuntu.com/charmlibs/explanation/charmlibs-tests/). From 1e0192cb7083b461a41f4fda6a91499a4f2584a0 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Tue, 2 Jun 2026 17:20:23 +1200 Subject: [PATCH 27/65] chore: use SPDX license expression in maintainer-owned pyproject.toml (#502) Migrate the Apache-2.0 license from the deprecated trove classifier ('License :: OSI Approved :: Apache Software License') to a PEP 639 license expression ('license = "Apache-2.0"'). Scope: maintainer-owned packages (apt, passwd, pathops, snap, sysctl, systemd) and the .example / interfaces/.example templates. Other libraries are owned by separate teams (see CODEOWNERS) and can follow in per-team PRs as the issue suggests. The four tests/integration/charms/*/library/pyproject.toml files under .example are symlinks to the parent template, so they pick up the change transparently. Fixes #293 --------- Co-authored-by: Claude Opus 4.7 (1M context) --- .example/pyproject.toml | 2 +- .template/{{ cookiecutter.project_slug }}/pyproject.toml | 2 +- apt/pyproject.toml | 2 +- interfaces/.example/pyproject.toml | 2 +- passwd/pyproject.toml | 2 +- pathops/pyproject.toml | 2 +- snap/pyproject.toml | 2 +- sysctl/pyproject.toml | 2 +- systemd/pyproject.toml | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.example/pyproject.toml b/.example/pyproject.toml index 65a7a0b7e..fa6389b66 100644 --- a/.example/pyproject.toml +++ b/.example/pyproject.toml @@ -6,9 +6,9 @@ requires-python = ">=3.10" authors = [ {name="The Charm Tech team at Canonical"}, ] +license = "Apache-2.0" classifiers = [ "Programming Language :: Python :: 3", - "License :: OSI Approved :: Apache Software License", "Intended Audience :: Developers", "Operating System :: POSIX :: Linux", "Development Status :: 5 - Production/Stable", diff --git a/.template/{{ cookiecutter.project_slug }}/pyproject.toml b/.template/{{ cookiecutter.project_slug }}/pyproject.toml index 2e5a21647..dbfc159a9 100644 --- a/.template/{{ cookiecutter.project_slug }}/pyproject.toml +++ b/.template/{{ cookiecutter.project_slug }}/pyproject.toml @@ -6,9 +6,9 @@ requires-python = ">={{ cookiecutter.min_python_version }}" authors = [ {name="{{ cookiecutter.author }}"}, ] +license = "Apache-2.0" classifiers = [ "Programming Language :: Python :: 3", - "License :: OSI Approved :: Apache Software License", "Intended Audience :: Developers", "Operating System :: POSIX :: Linux", "Development Status :: 5 - Production/Stable", diff --git a/apt/pyproject.toml b/apt/pyproject.toml index 8b179496a..ccfefe9ca 100644 --- a/apt/pyproject.toml +++ b/apt/pyproject.toml @@ -6,9 +6,9 @@ requires-python = ">=3.10" authors = [ {name="The Charm Tech team at Canonical Ltd."}, ] +license = "Apache-2.0" classifiers = [ "Programming Language :: Python :: 3", - "License :: OSI Approved :: Apache Software License", "Intended Audience :: Developers", "Operating System :: POSIX :: Linux", "Development Status :: 5 - Production/Stable", diff --git a/interfaces/.example/pyproject.toml b/interfaces/.example/pyproject.toml index 907b5ddfb..7edbd1224 100644 --- a/interfaces/.example/pyproject.toml +++ b/interfaces/.example/pyproject.toml @@ -6,9 +6,9 @@ requires-python = ">=3.10" authors = [ {name="The Charm Tech team at Canonical"}, ] +license = "Apache-2.0" classifiers = [ "Programming Language :: Python :: 3", - "License :: OSI Approved :: Apache Software License", "Intended Audience :: Developers", "Operating System :: POSIX :: Linux", "Development Status :: 5 - Production/Stable", diff --git a/passwd/pyproject.toml b/passwd/pyproject.toml index 2862e2bb1..ac49d28b8 100644 --- a/passwd/pyproject.toml +++ b/passwd/pyproject.toml @@ -6,9 +6,9 @@ requires-python = ">=3.10" authors = [ {name="charmlibs-maintainers"}, ] +license = "Apache-2.0" classifiers = [ "Programming Language :: Python :: 3", - "License :: OSI Approved :: Apache Software License", "Intended Audience :: Developers", "Operating System :: POSIX :: Linux", "Development Status :: 5 - Production/Stable", diff --git a/pathops/pyproject.toml b/pathops/pyproject.toml index 4e38e3a43..5783af3f1 100644 --- a/pathops/pyproject.toml +++ b/pathops/pyproject.toml @@ -6,9 +6,9 @@ requires-python = ">=3.10" authors = [ {name="The Charm Tech team at Canonical Ltd."}, ] +license = "Apache-2.0" classifiers = [ "Programming Language :: Python :: 3", - "License :: OSI Approved :: Apache Software License", "Intended Audience :: Developers", "Operating System :: POSIX :: Linux", "Development Status :: 5 - Production/Stable", diff --git a/snap/pyproject.toml b/snap/pyproject.toml index 1fb1b0381..dbab1ce79 100644 --- a/snap/pyproject.toml +++ b/snap/pyproject.toml @@ -6,9 +6,9 @@ requires-python = ">=3.10" authors = [ {name="The Charm Tech team at Canonical"}, ] +license = "Apache-2.0" classifiers = [ "Programming Language :: Python :: 3", - "License :: OSI Approved :: Apache Software License", "Intended Audience :: Developers", "Operating System :: POSIX :: Linux", "Development Status :: 5 - Production/Stable", diff --git a/sysctl/pyproject.toml b/sysctl/pyproject.toml index f55aea455..1aa2a960e 100644 --- a/sysctl/pyproject.toml +++ b/sysctl/pyproject.toml @@ -6,9 +6,9 @@ requires-python = ">=3.10" authors = [ {name="The Charm Tech team at Canonical"}, ] +license = "Apache-2.0" classifiers = [ "Programming Language :: Python :: 3", - "License :: OSI Approved :: Apache Software License", "Intended Audience :: Developers", "Operating System :: POSIX :: Linux", "Development Status :: 5 - Production/Stable", diff --git a/systemd/pyproject.toml b/systemd/pyproject.toml index b4176e0a5..036770494 100644 --- a/systemd/pyproject.toml +++ b/systemd/pyproject.toml @@ -6,9 +6,9 @@ requires-python = ">=3.10" authors = [ {name="The Charm Tech team at Canonical"}, ] +license = "Apache-2.0" classifiers = [ "Programming Language :: Python :: 3", - "License :: OSI Approved :: Apache Software License", "Intended Audience :: Developers", "Operating System :: POSIX :: Linux", "Development Status :: 5 - Production/Stable", From 3836ff884cecd139def11d9e3752f25c4373349e Mon Sep 17 00:00:00 2001 From: James Garner Date: Tue, 2 Jun 2026 17:55:59 +1200 Subject: [PATCH 28/65] docs: update lib metadata (#509) This PR adds `lib` entries to the interface metadata for some interfaces that were missing them (those without more specific codeowners). It also corrects two library name errors in the interface libs CSV. --- .docs/reference/interface-libs.csv | 4 +-- .../interface/v0/interface.yaml | 1 + .../fiveg_n2/interface/v0/interface.yaml | 1 + .../fiveg_n4/interface/v0/interface.yaml | 1 + .../fiveg_nrf/interface/v0/interface.yaml | 1 + interfaces/index.json | 36 +++++++++---------- .../ingress/interface/v1/interface.yaml | 1 + .../ingress/interface/v2/interface.yaml | 1 + .../interface/v0/interface.yaml | 1 + .../sdcore_config/interface/v0/interface.yaml | 1 + .../interface/v0/interface.yaml | 1 + 11 files changed, 29 insertions(+), 20 deletions(-) diff --git a/.docs/reference/interface-libs.csv b/.docs/reference/interface-libs.csv index 9d2cb7705..966fd3d08 100644 --- a/.docs/reference/interface-libs.csv +++ b/.docs/reference/interface-libs.csv @@ -1,6 +1,6 @@ name,status,url,docs,src,kind,rel_name,rel_url_charmhub,rel_url_schema,description charms.opencti.opencti_connector,,https://charmhub.io/opencti/libraries/opencti_connector,,https://github.com/canonical/opencti-operator/,Charmhub,opencti-connector,https://charmhub.io/integrations/opencti_connector,https://documentation.ubuntu.com/charmlibs/reference/interfaces/opencti_connector/v0/, -charms.filesystem_manager.filesystem_info,,https://charmhub.io/filesystem-client/libraries/filesystem_info,,https://github.com/charmed-hpc/filesystem-charms,Charmhub,filesystem_info,https://charmhub.io/integrations/filesystem_info,, +charms.filesystem_client.filesystem_info,,https://charmhub.io/filesystem-client/libraries/filesystem_info,,https://github.com/charmed-hpc/filesystem-charms,Charmhub,filesystem_info,https://charmhub.io/integrations/filesystem_info,, charms.storage_libs.nfs_interfaces,legacy,https://charmhub.io/storage-libs/libraries/nfs_interfaces,,,Charmhub,nfs_share,https://charmhub.io/integrations/nfs_share,,This interface is deprecated in favour of ``filesystem_info``. charms.alertmanager_k8s.alertmanager_dispatch,,https://charmhub.io/alertmanager-k8s/libraries/alertmanager_dispatch,,https://github.com/canonical/alertmanager-k8s-operator,Charmhub,alertmanager_dispatch,https://charmhub.io/integrations/alertmanager_dispatch,, charms.alertmanager_k8s.alertmanager_remote_configuration,,https://charmhub.io/alertmanager-k8s/libraries/alertmanager_remote_configuration,,https://github.com/canonical/alertmanager-k8s-operator,Charmhub,alertmanager_remote_configuration,https://charmhub.io/integrations/alertmanager_remote_configuration,, @@ -85,6 +85,6 @@ charmlibs.interfaces.istio_request_auth,recommended,https://pypi.org/project/cha charmed_service_mesh_helpers.interfaces.request_auth,legacy,https://pypi.org/project/charmed-service-mesh-helpers/,,https://github.com/canonical/charmed-service-mesh-helpers,PyPI,istio-request-auth,,,Deprecated in favour of charmlibs.interfaces.istio_request_auth. charmlibs.interfaces.gateway_metadata,recommended,https://pypi.org/project/charmlibs-interfaces-gateway-metadata,https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/gateway-metadata,https://github.com/canonical/charmlibs/tree/main/interfaces/gateway-metadata,PyPI,gateway-metadata,,,Share Kubernetes Gateway API workload metadata between charms. charmed_service_mesh_helpers.interfaces.gateway_metadata,legacy,https://pypi.org/project/charmed-service-mesh-helpers/,,https://github.com/canonical/charmed-service-mesh-helpers,PyPI,gateway-metadata,,,Deprecated in favour of ``charmlibs.interfaces.gateway_metadata``. -dpcharmlibs-interfaces,,https://pypi.org/project/dpcharmlibs-interfaces/,,https://github.com/canonical/data-platform-charmlibs,PyPI,valkey_client,https://charmhub.io/integrations/valkey_client,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/valkey_client,Database charms from the Data Platform Team recommend using the generic ``data_platform_libs.data_interfaces`` library to implement the interface. +dpcharmlibs.interfaces,,https://pypi.org/project/dpcharmlibs-interfaces,,https://github.com/canonical/data-platform-charmlibs,PyPI,valkey_client,https://charmhub.io/integrations/valkey_client,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/valkey_client,Database charms from the Data Platform Team recommend using the generic ``data_platform_libs.data_interfaces`` library to implement the interface. charmlibs.interfaces.istio_ingress_route,recommended,https://pypi.org/project/charmlibs-interfaces-istio-ingress-route,https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio-ingress-route,https://github.com/canonical/charmlibs/tree/main/interfaces/istio-ingress-route,PyPI,istio-ingress-route,,,Advanced ingress routing through the istio-ingress-k8s charm with multi-port listeners and gRPC support. charms.istio_ingress_k8s.istio_ingress_route,legacy,https://charmhub.io/istio-ingress-k8s/libraries/istio_ingress_route,,https://github.com/canonical/istio-ingress-k8s-operator,Charmhub,istio-ingress-route,,,Deprecated in favour of ``charmlibs.interfaces.istio_ingress_route``. diff --git a/interfaces/fiveg_core_gnb/interface/v0/interface.yaml b/interfaces/fiveg_core_gnb/interface/v0/interface.yaml index 6edceeb23..a658e0396 100644 --- a/interfaces/fiveg_core_gnb/interface/v0/interface.yaml +++ b/interfaces/fiveg_core_gnb/interface/v0/interface.yaml @@ -3,6 +3,7 @@ internal: true version: 0 status: draft +lib: charms.sdcore_nms_k8s.fiveg_core_gnb providers: - name: sdcore-nms-k8s-operator diff --git a/interfaces/fiveg_n2/interface/v0/interface.yaml b/interfaces/fiveg_n2/interface/v0/interface.yaml index 0cebce3b3..658a679f3 100644 --- a/interfaces/fiveg_n2/interface/v0/interface.yaml +++ b/interfaces/fiveg_n2/interface/v0/interface.yaml @@ -3,6 +3,7 @@ internal: true version: 0 status: draft +lib: charms.sdcore_amf_k8s.fiveg_n2 providers: - name: sdcore-amf-operator diff --git a/interfaces/fiveg_n4/interface/v0/interface.yaml b/interfaces/fiveg_n4/interface/v0/interface.yaml index 9f1a5ffe5..203ae5c8a 100644 --- a/interfaces/fiveg_n4/interface/v0/interface.yaml +++ b/interfaces/fiveg_n4/interface/v0/interface.yaml @@ -3,6 +3,7 @@ internal: true version: 0 status: draft +lib: charms.sdcore_upf_k8s.fiveg_n4 providers: [] requirers: [] diff --git a/interfaces/fiveg_nrf/interface/v0/interface.yaml b/interfaces/fiveg_nrf/interface/v0/interface.yaml index 870d9c1f8..cad469990 100644 --- a/interfaces/fiveg_nrf/interface/v0/interface.yaml +++ b/interfaces/fiveg_nrf/interface/v0/interface.yaml @@ -3,6 +3,7 @@ internal: true version: 0 status: draft +lib: charms.sdcore_nrf_k8s.fiveg_nrf providers: [] requirers: [] diff --git a/interfaces/index.json b/interfaces/index.json index 93eb428a3..9125f829d 100644 --- a/interfaces/index.json +++ b/interfaces/index.json @@ -103,7 +103,7 @@ "name": "filesystem_info", "version": "0", "lib": "charms.filesystem_client.filesystem_info", - "lib_url": "", + "lib_url": "https://charmhub.io/filesystem-client/libraries/filesystem_info", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/filesystem_info/", "summary": "Export mount information about shared filesystems.", "description": "The `filesystem_info` interface allows charms that export shared filesystems to expose the required mount information.\nThe provider charm exports mount information for the requirer charm.", @@ -112,8 +112,8 @@ { "name": "fiveg_core_gnb", "version": "0", - "lib": "", - "lib_url": "", + "lib": "charms.sdcore_nms_k8s.fiveg_core_gnb", + "lib_url": "https://charmhub.io/sdcore-nms-k8s/libraries/fiveg_core_gnb", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/fiveg_core_gnb/", "summary": "", "description": "", @@ -142,8 +142,8 @@ { "name": "fiveg_n2", "version": "0", - "lib": "", - "lib_url": "", + "lib": "charms.sdcore_amf_k8s.fiveg_n2", + "lib_url": "https://charmhub.io/sdcore-amf-k8s/libraries/fiveg_n2", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/fiveg_n2/", "summary": "", "description": "", @@ -162,8 +162,8 @@ { "name": "fiveg_n4", "version": "0", - "lib": "", - "lib_url": "", + "lib": "charms.sdcore_upf_k8s.fiveg_n4", + "lib_url": "https://charmhub.io/sdcore-upf-k8s/libraries/fiveg_n4", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/fiveg_n4/", "summary": "", "description": "", @@ -172,8 +172,8 @@ { "name": "fiveg_nrf", "version": "0", - "lib": "", - "lib_url": "", + "lib": "charms.sdcore_nrf_k8s.fiveg_nrf", + "lib_url": "https://charmhub.io/sdcore-nrf-k8s/libraries/fiveg_nrf", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/fiveg_nrf/", "summary": "", "description": "", @@ -242,8 +242,8 @@ { "name": "ingress", "version": "2", - "lib": "", - "lib_url": "", + "lib": "charms.traefik_k8s.ingress", + "lib_url": "https://charmhub.io/traefik-k8s/libraries/ingress", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/ingress/", "summary": "", "description": "", @@ -252,8 +252,8 @@ { "name": "ingress_per_unit", "version": "0", - "lib": "", - "lib_url": "", + "lib": "charms.traefik_k8s.ingress_per_unit", + "lib_url": "https://charmhub.io/traefik-k8s/libraries/ingress_per_unit", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/ingress_per_unit/", "summary": "", "description": "", @@ -542,8 +542,8 @@ { "name": "sdcore_config", "version": "0", - "lib": "", - "lib_url": "", + "lib": "charms.sdcore_nms_k8s.sdcore_config", + "lib_url": "https://charmhub.io/sdcore-nms-k8s/libraries/sdcore_config", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/sdcore_config/", "summary": "", "description": "", @@ -552,8 +552,8 @@ { "name": "sdcore_management", "version": "0", - "lib": "", - "lib_url": "", + "lib": "charms.sdcore_webui_k8s.sdcore_management", + "lib_url": "https://charmhub.io/sdcore-webui-k8s/libraries/sdcore_management", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/sdcore_management/", "summary": "", "description": "", @@ -613,7 +613,7 @@ "name": "valkey_client", "version": "1", "lib": "dpcharmlibs.interfaces", - "lib_url": "", + "lib_url": "https://pypi.org/project/dpcharmlibs-interfaces", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/valkey_client/", "summary": "Access Valkey databases with credentials and TLS configuration.", "description": "The `valkey_client` interface allows charms to access Valkey databases with user-specific credentials and TLS configuration.\nThe provider charm creates Valkey users and permissions based on requirer requests, while the requirer charm shares the desired key access prefix.", diff --git a/interfaces/ingress/interface/v1/interface.yaml b/interfaces/ingress/interface/v1/interface.yaml index bad9a847e..3941b4cb0 100644 --- a/interfaces/ingress/interface/v1/interface.yaml +++ b/interfaces/ingress/interface/v1/interface.yaml @@ -1,6 +1,7 @@ name: ingress version: 1 status: retired +lib: charms.traefik_k8s.ingress providers: - name: traefik-k8s diff --git a/interfaces/ingress/interface/v2/interface.yaml b/interfaces/ingress/interface/v2/interface.yaml index 5c6706536..fe4b524b0 100644 --- a/interfaces/ingress/interface/v2/interface.yaml +++ b/interfaces/ingress/interface/v2/interface.yaml @@ -1,6 +1,7 @@ name: ingress version: 2 status: published +lib: charms.traefik_k8s.ingress providers: - name: traefik-k8s diff --git a/interfaces/ingress_per_unit/interface/v0/interface.yaml b/interfaces/ingress_per_unit/interface/v0/interface.yaml index 68759a710..690d465f2 100644 --- a/interfaces/ingress_per_unit/interface/v0/interface.yaml +++ b/interfaces/ingress_per_unit/interface/v0/interface.yaml @@ -1,6 +1,7 @@ name: ingress_per_unit version: 0 status: published +lib: charms.traefik_k8s.ingress_per_unit providers: - name: traefik-k8s diff --git a/interfaces/sdcore_config/interface/v0/interface.yaml b/interfaces/sdcore_config/interface/v0/interface.yaml index 248dcf149..8451bfae5 100644 --- a/interfaces/sdcore_config/interface/v0/interface.yaml +++ b/interfaces/sdcore_config/interface/v0/interface.yaml @@ -3,6 +3,7 @@ internal: true version: 0 status: draft +lib: charms.sdcore_nms_k8s.sdcore_config providers: - name: sdcore-nms-k8s diff --git a/interfaces/sdcore_management/interface/v0/interface.yaml b/interfaces/sdcore_management/interface/v0/interface.yaml index 81af3bb02..93871dd26 100644 --- a/interfaces/sdcore_management/interface/v0/interface.yaml +++ b/interfaces/sdcore_management/interface/v0/interface.yaml @@ -3,6 +3,7 @@ internal: true version: 0 status: draft +lib: charms.sdcore_webui_k8s.sdcore_management providers: [] requirers: [] From 7d55178a0a887b4501339851b7d6209969e0d883 Mon Sep 17 00:00:00 2001 From: James Garner Date: Tue, 2 Jun 2026 18:09:02 +1200 Subject: [PATCH 29/65] ci: replace library csvs with yaml (#510) This PR migrates the contents of `general-libs.csv` and `interface-libs.csv` to a YAML file instead. I went with CSV initially because I thought it would be nice for contributors to be able to edit and explore library metadata visually in a spreadsheet program. But it turns out that 90% of the time we're looking at them in an IDE, on Github, or (worst of all) in a diff. A YAML file should be a lot friendlier for all three cases. This will also make adding composite fields easier, for example the tags for #501. The contents of the CSV files were migrated to the YAML file with a deterministic Python script (not checked in). During PR review, some existing typos in the information were caught and fixed. --------- Co-authored-by: Dave Wilding --- .docs/extensions/generate_tables.py | 53 +- .docs/reference/general-libs.csv | 68 -- .docs/reference/interface-libs.csv | 90 -- .docs/reference/libs.yaml | 1537 +++++++++++++++++++++++++++ .scripts/ls.py | 24 +- 5 files changed, 1576 insertions(+), 196 deletions(-) delete mode 100644 .docs/reference/general-libs.csv delete mode 100644 .docs/reference/interface-libs.csv create mode 100644 .docs/reference/libs.yaml diff --git a/.docs/extensions/generate_tables.py b/.docs/extensions/generate_tables.py index e80cc1b3f..3c10991ea 100644 --- a/.docs/extensions/generate_tables.py +++ b/.docs/extensions/generate_tables.py @@ -14,15 +14,16 @@ # ruff: noqa: S405 (suspicious-xml-etree-import ) -"""Generate source .rst files for lib tables, from CSV files in the reference directory.""" +"""Generate source .rst files for lib tables, from libs.yaml in the reference directory.""" from __future__ import annotations -import csv import pathlib import typing from xml.etree import ElementTree +import yaml + #################### # Sphinx extension # #################### @@ -81,7 +82,7 @@ def _generate(app: sphinx.application.Sphinx): _FILE_HEADER = """.. This file was automatically generated. It should not be manually edited! - Instead, edit the corresponding -raw.csv file and then rebuild the docs. + Instead, edit libs.yaml and then rebuild the docs. """ _INTERFACE_LIBS_TABLE_HEADER = """.. list-table:: @@ -117,7 +118,7 @@ def _generate(app: sphinx.application.Sphinx): """ -class _CSVRow(typing.TypedDict, total=True): +class _LibEntry(typing.TypedDict, total=True): name: str status: str url: str @@ -127,15 +128,20 @@ class _CSVRow(typing.TypedDict, total=True): description: str -class _InterfaceCSVRow(_CSVRow, total=True): +class _InterfaceLibEntry(_LibEntry, total=True): rel_name: str rel_url_charmhub: str rel_url_schema: str -class _GeneralCSVRow(_CSVRow, total=True): - machine: str - K8s: str +class _GeneralLibEntry(_LibEntry, total=True): + machine: bool + K8s: bool + + +class _LibsYaml(typing.TypedDict): + general: list[_GeneralLibEntry] + interfaces: list[_InterfaceLibEntry] class _TableRow(typing.NamedTuple): @@ -149,9 +155,9 @@ def _generate_libs_tables(docs_dir: str | pathlib.Path) -> None: reference_dir = pathlib.Path(docs_dir) / 'reference' generated_dir = reference_dir / 'generated' generated_dir.mkdir(exist_ok=True) - # interface libs - with (reference_dir / 'interface-libs.csv').open() as f: - interface_entries: list[_InterfaceCSVRow] = list(csv.DictReader(f)) # type: ignore + data: _LibsYaml = yaml.safe_load((reference_dir / 'libs.yaml').read_text()) + interface_entries = data['interfaces'] + general_entries = data['general'] _write_if_needed( path=(generated_dir / 'interface-libs-table.rst'), content=_get_interface_libs_table(interface_entries), @@ -160,9 +166,6 @@ def _generate_libs_tables(docs_dir: str | pathlib.Path) -> None: path=(generated_dir / 'interface-libs-status-key-table.rst'), content=_get_status_key_table_dropdown(interface_entries), ) - # general libs - with (reference_dir / 'general-libs.csv').open() as f: - general_entries: list[_GeneralCSVRow] = list(csv.DictReader(f)) # type: ignore _write_if_needed( path=(generated_dir / 'general-libs-table.rst'), content=_get_general_libs_table(general_entries), @@ -189,7 +192,7 @@ def _write_if_needed(path: pathlib.Path, content: str) -> None: ########## -def _get_interface_libs_table(entries: Iterable[_InterfaceCSVRow]) -> str: +def _get_interface_libs_table(entries: Iterable[_InterfaceLibEntry]) -> str: def key(row: tuple[str, ...]) -> tuple[str, ...]: status, _name, _kind, desc = row return status, desc @@ -202,7 +205,7 @@ def key(row: tuple[str, ...]) -> tuple[str, ...]: return _INTERFACE_LIBS_TABLE_HEADER + _rst_rows(sorted(rows, key=key)) -def _get_general_libs_table(entries: Iterable[_GeneralCSVRow]) -> str: +def _get_general_libs_table(entries: Iterable[_GeneralLibEntry]) -> str: def key(row: _TableRow) -> tuple[str, ...]: return row.status, row.kind, row.name, row.description @@ -214,7 +217,7 @@ def key(row: _TableRow) -> tuple[str, ...]: return _GENERAL_LIBS_TABLE_HEADER + _rst_rows(sorted(rows, key=key)) -def _get_status_key_table_dropdown(entries: Iterable[_CSVRow]) -> str: +def _get_status_key_table_dropdown(entries: Iterable[_LibEntry]) -> str: used_statuses = {entry['status'] for entry in entries} rows = [ (_status({'status': s}), _STATUS_TOOLTIPS[s]) # type: ignore @@ -226,8 +229,8 @@ def _get_status_key_table_dropdown(entries: Iterable[_CSVRow]) -> str: return _KEY_DROPDOWN_HEADER + _indent_lines(table, level=3) -def _is_listed(row: _CSVRow) -> bool: - return row['status'] != 'unlisted' +def _is_listed(entry: _LibEntry) -> bool: + return entry['status'] != 'unlisted' ########## @@ -235,7 +238,7 @@ def _is_listed(row: _CSVRow) -> bool: ########## -def _status(entry: _CSVRow) -> str: +def _status(entry: _LibEntry) -> str: status = entry['status'] html_lines = [_html_hidden_span(_STATUS_SORTKEYS[status])] if status in _EMOJIS: @@ -243,7 +246,7 @@ def _status(entry: _CSVRow) -> str: return _rst_table_indent(_rst_raw_html('\n'.join(html_lines))) -def _name(entry: _CSVRow) -> str: +def _name(entry: _LibEntry) -> str: name = entry['name'] if entry['kind'] != 'Charmhub' else entry['name'].removeprefix('charms.') link = _html_link(name, entry['url']) extras = ', '.join(_html_link(s, url) for s in ('docs', 'src') if (url := entry[s])) @@ -252,7 +255,7 @@ def _name(entry: _CSVRow) -> str: return _rst_table_indent(_rst_raw_html('\n'.join(html_lines))) -def _kind(entry: _CSVRow) -> str: +def _kind(entry: _LibEntry) -> str: kind = entry['kind'] content = [_rst_raw_html(_html_hidden_span(_KIND_SORTKEYS[kind]))] if kind_str := _EMOJIS.get(kind, '') + kind: @@ -260,7 +263,7 @@ def _kind(entry: _CSVRow) -> str: return _rst_table_indent('\n'.join(content)) -def _interface_description(entry: _InterfaceCSVRow) -> str: +def _interface_description(entry: _InterfaceLibEntry) -> str: sortkeys = [ entry['rel_name'].ljust(64, 'z'), str(_STATUS_SORTKEYS[entry['status']]), @@ -276,7 +279,7 @@ def _interface_description(entry: _InterfaceCSVRow) -> str: return _rst_table_indent('\n'.join(content)) -def _rel_links(entry: _InterfaceCSVRow) -> str: +def _rel_links(entry: _InterfaceLibEntry) -> str: if not (name := entry['rel_name']): return '' if not (main_url := entry['rel_url_charmhub']): @@ -288,7 +291,7 @@ def _rel_links(entry: _InterfaceCSVRow) -> str: return f'{main_link} ({schema_link})' -def _general_description(entry: _GeneralCSVRow) -> str: +def _general_description(entry: _GeneralLibEntry) -> str: substrates = ('machine', 'K8s') sortkeys = [ *('0' if entry[s] else '1' for s in substrates), diff --git a/.docs/reference/general-libs.csv b/.docs/reference/general-libs.csv deleted file mode 100644 index ccbf5623c..000000000 --- a/.docs/reference/general-libs.csv +++ /dev/null @@ -1,68 +0,0 @@ -name,status,url,docs,src,kind,machine,K8s,description -charms.operator_libs_linux.apt,legacy,https://charmhub.io/operator-libs-linux/libraries/apt,,https://github.com/canonical/operator-libs-linux,Charmhub,y,,Use ``apt`` to install and manage packages. Deprecated in favor of ``charmlibs.apt``. -charms.mysql.architecture,team,https://charmhub.io/mysql/libraries/architecture,,https://github.com/canonical/mysql-operator,Charmhub,y,y,Shared code between the ``mysql`` and ``mysql-k8s`` charms. -charms.mysql.async_replication,team,https://charmhub.io/mysql/libraries/async_replication,,https://github.com/canonical/mysql-operator,Charmhub,y,y,Shared code between the ``mysql`` and ``mysql-k8s`` charms. -awsjuju,unlisted,https://pypi.org/project/awsjuju/,,https://github.com/kapilt/awsjuju,PyPI,,,Legacy library for managing AWS resources. -charms.mysql.backups,team,https://charmhub.io/mysql/libraries/backups,,https://github.com/canonical/mysql-operator,Charmhub,y,y,Shared code between the ``mysql`` and ``mysql-k8s`` charms. -charms.harness_extensions.capture_events,legacy,https://charmhub.io/harness-extensions/libraries/capture_events,,https://github.com/PietroPasotti/harness-extensions,Charmhub,y,y,Helper for legacy harness tests. New charms should write state-transition tests with ``ops[testing]`` instead. -charms.observability_libs.cert_handler,,https://charmhub.io/observability-libs/libraries/cert_handler,,https://github.com/canonical/observability-libs,Charmhub,y,y,Wraps the requirer side of the ``tls-certificates-interface`` charm's ``tls_certificates`` lib. -charms.loki_k8s.charm_logging,recommended,https://charmhub.io/loki-k8s/libraries/charm_logging,,https://github.com/canonical/loki-k8s-operator,Charmhub,y,y,Add charm code logging to logs sent via ``loki_push_api``. -charms.tempo_coordinator_k8s.charm_tracing,legacy,https://charmhub.io/tempo-coordinator-k8s/libraries/charm_tracing,,https://github.com/canonical/tempo-coordinator-k8s-operator,Charmhub,y,y,Provided by the ``tempo-coordinator-k8s`` charm. Consider using ``ops[tracing]`` instead. -charms.tempo_k8s.charm_tracing,legacy,https://charmhub.io/tempo-k8s/libraries/charm_tracing,,https://github.com/canonical/tempo-k8s-operator,Charmhub,y,y,Deprecated in favor of the ``tempo-coordinator-k8s`` charm's libs. New charms should use ``ops[tracing]`` for tracing charm code instead. -charm-api,experimental,https://pypi.org/project/charm-api/,,https://github.com/canonical/charm-api,PyPI,y,y,Experimental API for writing charms. -charm-helpers,legacy,https://pypi.org/project/charmhelpers/,https://charm-helpers.readthedocs.io/,https://github.com/juju/charm-helpers,PyPI,y,y,Pre-Ops library used by reactive charms. -charm-json,legacy,https://pypi.org/project/charm-json/,,https://github.com/canonical/charm-json,PyPI,y,y,JSON typed relation data – unnecessary with Ops 2.23+ due to the addition of typed relation data. -charm-refresh,recommended,https://pypi.org/project/charm-refresh/,https://canonical-charm-refresh.readthedocs-hosted.com/latest/,https://github.com/canonical/charm-refresh,PyPI,y,y,In-place rolling refreshes of stateful charmed applications. -charm-refresh-build-version,unlisted,https://pypi.org/project/charm-refresh-build-version/,,https://github.com/canonical/charm-refresh-build-version,PyPI,y,y,Workaround for a Charmcraft issue. -charm-upgrade,unlisted,https://pypi.org/project/charm-upgrade/,,https://github.com/canonical/charm-upgrade,PyPI,y,y,Alias for charm-refresh? (Repo URL redirects.) -charmed-kubeflow-chisme,team,https://pypi.org/project/charmed-kubeflow-chisme/,,https://github.com/canonical/charmed-kubeflow-chisme,PyPI,y,y,Used internally by the Charmed Kubeflow team. -charmed-service-mesh-helpers,team,https://pypi.org/project/charmed-service-mesh-helpers/,,https://github.com/canonical/charmed-service-mesh-helpers/,PyPI,,y,Used internally by the Service Mesh team. -charmlibs.apt,recommended,https://pypi.org/project/charmlibs-apt,https://documentation.ubuntu.com/charmlibs/reference/charmlibs/apt,https://github.com/canonical/charmlibs/tree/main/apt,PyPI,y,,Use ``apt`` to install and manage packages. -charmlibs.passwd,recommended,https://pypi.org/project/charmlibs-passwd,https://documentation.ubuntu.com/charmlibs/reference/charmlibs/passwd,https://github.com/canonical/charmlibs/tree/main/passwd,PyPI,y,,Manage Linux users and groups. -charmlibs.pathops,recommended,https://pypi.org/project/charmlibs-pathops,https://documentation.ubuntu.com/charmlibs/reference/charmlibs/pathops,https://github.com/canonical/charmlibs/tree/main/pathops,PyPI,y,y,Substrate agnostic file operations. -charmlibs.rollingops,recommended,https://pypi.org/project/charmlibs-rollingops,https://documentation.ubuntu.com/charmlibs/reference/charmlibs/rollingops,https://github.com/canonical/charmlibs/tree/main/rollingops,PyPI,y,y,"Coordinate rolling operations for charms, including across applications or clusters." -charmlibs.snap,recommended,https://pypi.org/project/charmlibs-snap,https://documentation.ubuntu.com/charmlibs/reference/charmlibs/snap,https://github.com/canonical/charmlibs/tree/main/snap,PyPI,y,,Use ``snapd`` to install and manage packages. -charmlibs.sysctl,recommended,https://pypi.org/project/charmlibs-sysctl,https://documentation.ubuntu.com/charmlibs/reference/charmlibs/sysctl,https://github.com/canonical/charmlibs/tree/main/sysctl,PyPI,y,,Create and configure ``sysctl`` options. -charmlibs.systemd,recommended,https://pypi.org/project/charmlibs-systemd,https://documentation.ubuntu.com/charmlibs/reference/charmlibs/systemd,https://github.com/canonical/charmlibs/tree/main/systemd,PyPI,y,,"Use ``systemd`` to start, stop, and manage system services." -charms.contextual_status,experimental,https://pypi.org/project/charms.contextual-status/,,https://github.com/charmed-kubernetes/charm-lib-contextual-status,PyPI,y,y,Context manager based library for setting charm statuses. -charms.docker,legacy,https://pypi.org/project/charms.docker/,,https://github.com/juju-solutions/charms.docker,PyPI,,,Legacy library used by the ``docker-layer`` reactive charm. -charms.proxylib,,https://github.com/canonical/charms.proxylib,,https://github.com/canonical/charms.proxylib,PyPI,y,y,A library that helps charms direct HTTP requests and subprocess calls through the model-configured proxy environment. -charms.reactive,legacy,https://pypi.org/project/charms.reactive/,https://charmsreactive.readthedocs.io/en/latest/,https://github.com/canonical/charms.reactive,PyPI,y,y,Legacy library used to implement reactive charms. -charms.reconciler,experimental,https://pypi.org/project/charms.reconciler/,,https://github.com/charmed-kubernetes/charm-lib-reconciler,PyPI,y,y,Handle all Juju events in an Ops-based charm with a single method. -charms.templating.jinja2,legacy,https://pypi.org/project/charms.templating.jinja2/,https://pythonhosted.org/charms.templating.jinja2/,https://github.com/juju-solutions/charms.templating.jinja2,PyPI,,,Legacy library for Jinja templating in reactive charms. -charms.zookeeper.client,,https://charmhub.io/zookeeper/libraries/client,,http://github.com/canonical/zookeeper-operator,Charmhub,y,y,Perform ``zookeeper`` operations. -coordinated-workers,team,https://pypi.org/project/coordinated-workers/,,https://github.com/canonical/cos-coordinated-workers,PyPI,y,y,"Abstractions for charms following the coordinator-worker pattern, used by Observability Team." -cosl,team,https://pypi.org/project/cosl/,,https://github.com/canonical/cos-lib,PyPI,y,y,Used internally by the Observability Charm Engineering Team. A dependency (via ``PYDEPS``) of popular charm libs such as ``loki_push_api``. -charms.data_platform_libs.data_models,legacy,https://charmhub.io/data-platform-libs/libraries/data_models,,https://github.com/canonical/data-platform-libs,Charmhub,y,y,``pydantic``-based typed relation data – unnecessary with Ops 2.23+ due to the addition of typed relation data. -charms.data_platform_libs.data_secrets,legacy,https://charmhub.io/data-platform-libs/libraries/data_secrets,,https://github.com/canonical/data-platform-libs,Charmhub,y,y,Secrets-related helpers for interfaces. Deprecated in favor of the ``data-platform-libs.data_inferfaces`` interface lib. -data-platform-helpers,team,https://pypi.org/project/data-platform-helpers/,,https://github.com/canonical/data-platform-helpers,PyPI,y,y,Used internally by the Data Charm Engineering team. -charms.operator_libs_linux.dnf,legacy,https://charmhub.io/operator-libs-linux/libraries/dnf,,https://github.com/canonical/operator-libs-linux,Charmhub,y,,"Use ``dnf`` to install and manage packages – modern charms all run on Ubuntu, so this shouldn't be needed." -charms.harness_extensions.evt_sequences,legacy,https://charmhub.io/harness-extensions/libraries/evt_sequences,,https://github.com/PietroPasotti/harness-extensions,Charmhub,y,y,Helper for legacy harness tests. New charms should write state-transition tests with ``ops[testing]`` instead. -charms.operator_libs_linux.grub,legacy,https://charmhub.io/operator-libs-linux/libraries/grub,,https://github.com/canonical/operator-libs-linux,Charmhub,y,,Use GRUB to manage kernel configuration. This library has fallen out of use and is no longer actively maintained. Contact Charm Tech if you have a need for this functionality in your charm. -charms.opensearch.helper_cos,team,https://charmhub.io/opensearch/libraries/helper_cos,,https://github.com/canonical/opensearch-operator,Charmhub,,,Relies on ``data_platform_helpers``. -hpc-libs,team,https://github.com/charmed-hpc/hpc-libs,,,git,y,y,Used internally by HPC charms. -charms.operator_libs_linux.juju_systemd_notices,legacy,https://charmhub.io/operator-libs-linux/libraries/juju_systemd_notices,,https://github.com/canonical/operator-libs-linux,Charmhub,y,,Use ``systemd`` to observe and emit notices when services change state. This library has fallen out of use and is no longer actively maintained. Contact Charm Tech if you have a need for this functionality in your charm. -charms.observability_libs.juju_topology,legacy,https://charmhub.io/observability-libs/libraries/juju_topology,,https://github.com/canonical/observability-libs,Charmhub,y,y,Deprecated in favor of ``cosl.juju_topology.JujuTopology``. -jujubigdata,legacy,https://pypi.org/project/jujubigdata/,http://pythonhosted.org/jujubigdata/,https://github.com/juju-solutions/jujubigdata,PyPI,,,Legacy library for developing Big Data charms. -jujuresources,legacy,https://pypi.org/project/jujuresources/,http://pythonhosted.org/jujuresources/,https://github.com/juju-solutions/jujuresources,PyPI,y,y,Legacy library for loading binary resources. New charms should use ``ops.Model.resources``. -charms.observability_libs.kubernetes_compute_resources_patch,,https://charmhub.io/observability-libs/libraries/kubernetes_compute_resources_patch,,https://github.com/canonical/observability-libs,Charmhub,,y,Patch Kubernetes compute resource limits. -charms.observability_libs.kubernetes_service_patch,legacy,https://charmhub.io/observability-libs/libraries/kubernetes_service_patch,,https://github.com/canonical/observability-libs,Charmhub,,y,Deprecated in favor of ``ops.Unit.set_ports``. -charms.observability_libs.metrics_endpoint_discovery,,https://charmhub.io/observability-libs/libraries/metrics_endpoint_discovery,,https://github.com/canonical/observability-libs,Charmhub,,y,Discover metric endpoints exposed by applications deployed to a K8s cluster. -mongo-charms-single-kernel,team,https://pypi.org/project/mongo-charms-single-kernel/,,https://github.com/canonical/mongo-single-kernel-library,PyPI,y,y,Used internally by the Data Charm Engineering team. -charms.kubernetes_charm_libraries.multus,,https://charmhub.io/kubernetes-charm-libraries/libraries/multus,,https://github.com/canonical/kubernetes-charm-libraries,Charmhub,,y,Use the Multus Kubernetes Container Network Interface. -charms.mysql.mysql,team,https://charmhub.io/mysql/libraries/mysql,,https://github.com/canonical/mysql-operator,Charmhub,y,y,Shared code between the ``mysql`` and ``mysql-k8s`` charms. -oci-image,legacy,https://pypi.org/project/oci-image/,,https://github.com/juju-solutions/resource-oci-image,PyPI,,y,Work with OCI images in podspec charms. New Kubernetes charms should use the sidecar pattern. Repo archived in March 2024. -ops_reactive_interface,legacy,https://pypi.org/project/ops-reactive-interface/,https://github.com/canonical/ops-reactive-interface/tree/main/docs,https://github.com/juju-solutions/ops-reactive-interface,PyPI,y,y,"Helper for interface library developers, to allow an Ops-based interface library to interact with legacy, reactive charms." -ops.manifest,,https://pypi.org/project/ops.manifest/,,https://github.com/canonical/ops-lib-manifest,PyPI,,y,Work with Kubernetes manifests. -charms.operator_libs_linux.passwd,legacy,https://charmhub.io/operator-libs-linux/libraries/passwd,,https://github.com/canonical/operator-libs-linux,Charmhub,y,,Manage Linux users and groups. Deprecated in favor of ``charmlibs.passwd``. -charms.pgbouncer_k8s.pgb,team,https://charmhub.io/pgbouncer-k8s/libraries/pgb,,https://github.com/canonical/pgbouncer-k8s-operator,Charmhub,y,y,Shared code between ``pgbouncer`` and ``pgbouncer-k8s`` charms. -charms.postgresql_k8s.postgresql,team,https://charmhub.io/postgresql-k8s/libraries/postgresql,,https://github.com/canonical/postgresql-k8s-operator,Charmhub,y,y,Shared code between the ``posgresql`` and ``postgresql-k8s`` charms. -charms.rolling_ops.rollingops,legacy,https://charmhub.io/rolling-ops/libraries/rollingops,,https://github.com/canonical/charm-rolling-ops,Charmhub,y,y,"Legacy library for performing ""rolling"" operations across units, for example rolling restarts. New charms should use ``charmlibs.rollingops``." -charms.mysql.s3_helpers,team,https://charmhub.io/mysql/libraries/s3_helpers,,https://github.com/canonical/mysql-operator,Charmhub,y,y,Shared code between the ``mysql`` and ``mysql-k8s`` charms. -sborl,legacy,https://pypi.org/project/sborl/,,https://github.com/canonical/sborl,PyPI,y,y,Legacy library for implementing interface libraries. New interface libraries can use the typed relation data feature available in Ops 2.23+. -serialized-data-interface,legacy,https://pypi.org/project/serialized-data-interface/,,https://github.com/canonical/serialized-data-interface,PyPI,y,y,Relation data validation – use the features available in Ops instead. -charms.operator_libs_linux.snap,legacy,https://charmhub.io/operator-libs-linux/libraries/snap,,https://github.com/canonical/operator-libs-linux,Charmhub,y,,Use ``snapd`` to install and manage packages. Deprecated in favor of ``charmlibs.snap``. -charms.operator_libs_linux.sysctl,legacy,https://charmhub.io/operator-libs-linux/libraries/sysctl,,https://github.com/canonical/operator-libs-linux,Charmhub,y,,Create and configure ``sysctl`` options. Deprecated in favor of ``charmlibs.sysctl``. -charms.operator_libs_linux.systemd,legacy,https://charmhub.io/operator-libs-linux/libraries/systemd,,https://github.com/canonical/operator-libs-linux,Charmhub,y,,"Use ``systemd`` to start, stop, and manage system services. Deprecated in favor of ``charmlibs.systemd``." -charms.mysql.tls,team,https://charmhub.io/mysql/libraries/tls,,https://github.com/canonical/mysql-operator,Charmhub,y,y,Shared code between the ``mysql`` and ``mysql-k8s`` charms. -charms.data_platform_libs.upgrade,legacy,https://charmhub.io/data-platform-libs/libraries/upgrade,,https://github.com/canonical/data-platform-libs,Charmhub,y,y,Manage in-place upgrades. Deprecated in favor of ``charm-refresh``. diff --git a/.docs/reference/interface-libs.csv b/.docs/reference/interface-libs.csv deleted file mode 100644 index 966fd3d08..000000000 --- a/.docs/reference/interface-libs.csv +++ /dev/null @@ -1,90 +0,0 @@ -name,status,url,docs,src,kind,rel_name,rel_url_charmhub,rel_url_schema,description -charms.opencti.opencti_connector,,https://charmhub.io/opencti/libraries/opencti_connector,,https://github.com/canonical/opencti-operator/,Charmhub,opencti-connector,https://charmhub.io/integrations/opencti_connector,https://documentation.ubuntu.com/charmlibs/reference/interfaces/opencti_connector/v0/, -charms.filesystem_client.filesystem_info,,https://charmhub.io/filesystem-client/libraries/filesystem_info,,https://github.com/charmed-hpc/filesystem-charms,Charmhub,filesystem_info,https://charmhub.io/integrations/filesystem_info,, -charms.storage_libs.nfs_interfaces,legacy,https://charmhub.io/storage-libs/libraries/nfs_interfaces,,,Charmhub,nfs_share,https://charmhub.io/integrations/nfs_share,,This interface is deprecated in favour of ``filesystem_info``. -charms.alertmanager_k8s.alertmanager_dispatch,,https://charmhub.io/alertmanager-k8s/libraries/alertmanager_dispatch,,https://github.com/canonical/alertmanager-k8s-operator,Charmhub,alertmanager_dispatch,https://charmhub.io/integrations/alertmanager_dispatch,, -charms.alertmanager_k8s.alertmanager_remote_configuration,,https://charmhub.io/alertmanager-k8s/libraries/alertmanager_remote_configuration,,https://github.com/canonical/alertmanager-k8s-operator,Charmhub,alertmanager_remote_configuration,https://charmhub.io/integrations/alertmanager_remote_configuration,, -charms.oathkeeper.auth_proxy,,https://charmhub.io/oathkeeper/libraries/auth_proxy,,https://github.com/canonical/oathkeeper-operator,Charmhub,auth_proxy,https://charmhub.io/integrations/auth_proxy,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/auth_proxy/v0, -charms.oauth2_proxy_k8s.auth_proxy,,https://charmhub.io/oauth2-proxy-k8s/libraries/auth_proxy,,https://github.com/canonical/oauth2-proxy-k8s-operator,Charmhub,auth_proxy,https://charmhub.io/integrations/auth_proxy,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/auth_proxy/v0, -charms.blackbox_exporter_k8s.blackbox_probes,,https://charmhub.io/blackbox-exporter-k8s/libraries/blackbox_probes,,https://github.com/canonical/blackbox-exporter-k8s-operator,Charmhub,blackbox_exporter_probes,https://charmhub.io/blackbox-exporter-k8s/integrations#probes,,"(The Charmhub interfaces page doesn't load, this link is to a charm that uses the interface.)" -charms.catalogue_k8s.catalogue,,https://charmhub.io/catalogue-k8s/libraries/catalogue,,https://github.com/canonical/catalogue-k8s-operator,Charmhub,catalogue,https://charmhub.io/integrations/catalogue,,Transfer catalogue data for display by the provider. -charms.certificate_transfer_interface.certificate_transfer,,https://charmhub.io/certificate-transfer-interface/libraries/certificate_transfer,,https://github.com/canonical/certificate-transfer-interface,Charmhub,certificate_transfer,https://charmhub.io/integrations/certificate_transfer,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/certificate_transfer/v1,Transfer certificates between charms. -charms.grafana_cloud_integrator.cloud_config_provider,,https://charmhub.io/grafana-cloud-integrator/libraries/cloud_config_provider,,https://github.com/canonical/grafana-cloud-integrator,Charmhub,grafana_cloud_config,https://charmhub.io/integrations/grafana_cloud_config,, -charms.grafana_cloud_integrator.cloud_config_requirer,,https://charmhub.io/grafana-cloud-integrator/libraries/cloud_config_requirer,,https://github.com/canonical/grafana-cloud-integrator,Charmhub,grafana_cloud_config,https://charmhub.io/integrations/grafana_cloud_config,, -charms.cloudflare_configurator.cloudflared_route,,https://charmhub.io/cloudflare-configurator/libraries/cloudflared_route,,https://github.com/canonical/cloudflare-configurator-operator,Charmhub,cloudflared_route,https://charmhub.io/integrations/cloudflared_route,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/cloudflared_route/v0, -charms.grafana_agent.cos_agent,,https://charmhub.io/grafana-agent/libraries/cos_agent,,https://github.com/canonical/grafana-agent-operator,Charmhub,cos_agent,https://charmhub.io/integrations/cos_agent,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/cos_agent/v0,Machine charm specific interface for exchanging observability related data. -charms.data_platform_libs.data_interfaces,,https://charmhub.io/data-platform-libs/libraries/data_interfaces,,https://github.com/canonical/data-platform-libs,Charmhub,etcd_client,https://charmhub.io/integrations/etcd_client,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/etcd_client,Database charms from the Data Platform Team recommend using the generic ``data_platform_libs.data_interfaces`` library to implement the interface. -charms.data_platform_libs.data_interfaces,,https://charmhub.io/data-platform-libs/libraries/data_interfaces,,https://github.com/canonical/data-platform-libs,Charmhub,mysql_client,https://charmhub.io/integrations/mysql_client,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/mysql_client,Database charms from the Data Platform Team recommend using the generic ``data_platform_libs.data_interfaces`` library to implement the interface. -charms.data_platform_libs.data_interfaces,,https://charmhub.io/data-platform-libs/libraries/data_interfaces,,https://github.com/canonical/data-platform-libs,Charmhub,mongodb_client,https://charmhub.io/integrations/mongodb_client,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/mongodb_client,Database charms from the Data Platform Team recommend using the generic ``data_platform_libs.data_interfaces`` library to implement the interface. -charms.data_platform_libs.data_interfaces,,https://charmhub.io/data-platform-libs/libraries/data_interfaces,,https://github.com/canonical/data-platform-libs,Charmhub,opensearch_client,https://charmhub.io/integrations/opensearch_client,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/opensearch_client,Database charms from the Data Platform Team recommend using the generic ``data_platform_libs.data_interfaces`` library to implement the interface. -charms.data_platform_libs.data_interfaces,,https://charmhub.io/data-platform-libs/libraries/data_interfaces,,https://github.com/canonical/data-platform-libs,Charmhub,postgresql_client,https://charmhub.io/integrations/postgresql_client,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/postgresql_client,Database charms from the Data Platform Team recommend using the generic ``data_platform_libs.data_interfaces`` library to implement the interface. -charms.data_platform_libs.database_provides,legacy,https://charmhub.io/data-platform-libs/libraries/database_provides,,https://github.com/canonical/data-platform-libs,Charmhub,,,,Deprecated in favour of ``data_platform_helpers.data_interfaces``. -charms.data_platform_libs.database_requires,legacy,https://charmhub.io/data-platform-libs/libraries/database_requires,,https://github.com/canonical/data-platform-libs,Charmhub,,,,Deprecated in favour of ``data_platform_helpers.data_interfaces``. -charms.dex_auth.dex_oidc_config,,https://charmhub.io/dex-auth/libraries/dex_oidc_config,,https://github.com/canonical/dex-auth-operator,Charmhub,dex-oidc-config,https://charmhub.io/integrations/dex-oidc-config,, -charms.sdcore_nms_k8s.fiveg_core_gnb,,https://charmhub.io/sdcore-nms-k8s/libraries/fiveg_core_gnb,,https://github.com/canonical/sdcore-nms-k8s-operator,Charmhub,fiveg_core_gnb,https://charmhub.io/integrations/fiveg_core_gnb,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/fiveg_core_gnb/v0, -charms.sdcore_amf_k8s.fiveg_n2,,https://charmhub.io/sdcore-amf-k8s/libraries/fiveg_n2,,https://github.com/canonical/sdcore-amf-k8s-operator,Charmhub,fiveg_n2,https://charmhub.io/integrations/fiveg_n2,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/fiveg_n2/v0, -charms.sdcore_upf_k8s.fiveg_n4,,https://charmhub.io/sdcore-upf-k8s/libraries/fiveg_n4,,https://github.com/canonical/sdcore-upf-k8s-operator,Charmhub,fiveg_n4,https://charmhub.io/integrations/fiveg_n4,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/fiveg_n4/v0, -charms.sdcore_nrf_k8s.fiveg_nrf,,https://charmhub.io/sdcore-nrf-k8s/libraries/fiveg_nrf,,https://github.com/canonical/sdcore-nrf-k8s-operator,Charmhub,fiveg_nrf,https://charmhub.io/integrations/fiveg_nrf,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/fiveg_nrf/v0,Transfer Network Repository Function information from one charm to another. -charms.oathkeeper.forward_auth,,https://charmhub.io/oathkeeper/libraries/forward_auth,,https://github.com/canonical/oathkeeper-operator,Charmhub,forward_auth,https://charmhub.io/integrations/forward_auth,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/forward_auth/v0, -charms.oauth2_proxy_k8s.forward_auth,,https://charmhub.io/oauth2-proxy-k8s/libraries/forward_auth,,https://github.com/canonical/oauth2-proxy-k8s-operator,Charmhub,forward_auth,https://charmhub.io/integrations/forward_auth,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/forward_auth/v0, -charms.glauth_utils.glauth_auxiliary,,https://charmhub.io/glauth-utils/libraries/glauth_auxiliary,,https://github.com/canonical/glauth-utils,Charmhub,glauth_auxilary,https://charmhub.io/integrations/glauth_auxiliary,, -charms.grafana_k8s.grafana_auth,,https://charmhub.io/grafana-k8s/libraries/grafana_auth,,https://github.com/canonical/grafana-k8s-operator,Charmhub,grafana_auth,https://charmhub.io/integrations/grafana_auth,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/grafana_auth/v0, -charms.grafana_k8s.grafana_dashboard,,https://charmhub.io/grafana-k8s/libraries/grafana_dashboard,,https://github.com/canonical/grafana-k8s-operator,Charmhub,grafana_dashboard,https://charmhub.io/integrations/grafana_dashboard,,Manage dashboards for Grafana. -charms.grafana_k8s.grafana_metadata,,https://charmhub.io/grafana-k8s/libraries/grafana_metadata,,https://github.com/canonical/grafana-k8s-operator,Charmhub,grafana_metadata,https://charmhub.io/integrations/grafana_metadata,, -charms.grafana_k8s.grafana_source,,https://charmhub.io/grafana-k8s/libraries/grafana_source,,https://github.com/canonical/grafana-k8s-operator,Charmhub,grafana-source,https://charmhub.io/integrations/grafana-source,,Provide a data source for Grafana dashboards. -charms.hydra.hydra_endpoints,,https://charmhub.io/hydra/libraries/hydra_endpoints,,https://github.com/canonical/hydra-operator,Charmhub,hydra_endpoints,https://charmhub.io/integrations/hydra_endpoints,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/hydra_endpoints/v0, -charms.traefik_k8s.ingress,,https://charmhub.io/traefik-k8s/libraries/ingress,,https://github.com/canonical/traefik-k8s-operator,Charmhub,ingress,https://charmhub.io/integrations/ingress,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/ingress/v2,Manage per-application ingress and and load-balancing. -charms.nginx_ingress_integrator.ingress,,https://charmhub.io/nginx-ingress-integrator/libraries/ingress,,https://github.com/canonical/nginx-ingress-integrator-operator,Charmhub,ingress,https://charmhub.io/integrations/ingress,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/ingress/v2,Manage ingress. -charms.traefik_k8s.ingress_per_unit,,https://charmhub.io/traefik-k8s/libraries/ingress_per_unit,,https://github.com/canonical/traefik-k8s-operator,Charmhub,ingress_per_unit,https://charmhub.io/integrations/ingress_per_unit,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/ingress_per_unit/v0, -charms.istio_pilot.istio_gateway_info,,https://charmhub.io/istio-pilot/libraries/istio_gateway_info,,https://github.com/canonical/istio-operators,Charmhub,istio-gateway-info,https://charmhub.io/integrations/istio-gateway-info,, -charms.mlops_libs.k8s_service_info,,https://charmhub.io/mlops-libs/libraries/k8s_service_info,,https://github.com/canonical/mlops-libs,Charmhub,k8s-service,https://charmhub.io/integrations/k8s-service,, -charms.karma_k8s.karma_dashboard,,https://charmhub.io/karma-k8s/libraries/karma_dashboard,,https://github.com/canonical/karma-k8s-operator,Charmhub,karma_dashboard,https://charmhub.io/integrations/karma_dashboard,, -charms.kratos_external_idp_integrator.kratos_external_provider,,https://charmhub.io/kratos-external-idp-integrator/libraries/kratos_external_provider,,https://github.com/canonical/kratos-external-idp-integrator,Charmhub,external_provider,https://charmhub.io/integrations/external_provider,, -charms.kratos.kratos_info,,https://charmhub.io/kratos/libraries/kratos_info,,https://github.com/canonical/kratos-operator,Charmhub,kratos_info,https://charmhub.io/integrations/kratos_info,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/kratos_info/v0, -charms.kratos.kratos_registration_webhook,unlisted,https://charmhub.io/kratos/libraries/kratos_registration_webhook,,https://github.com/canonical/kratos-operator,Charmhub,,,, -charms.kubeflow_dashboard.kubeflow_dashboard_links,,https://charmhub.io/kubeflow-dashboard/libraries/kubeflow_dashboard_links,,https://github.com/canonical/kubeflow-dashboard-operator,Charmhub,kubeflow_dashboard_links,https://charmhub.io/integrations/kubeflow_dashboard_links,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/kubeflow_dashboard_links/v0, -charms.resource_dispatcher.kubernetes_manifests,,https://charmhub.io/resource-dispatcher/libraries/kubernetes_manifests,,https://github.com/canonical/resource-dispatcher,Charmhub,kubernetes_manifest,https://charmhub.io/integrations/kubernetes_manifest,, -charms.glauth_k8s.ldap,,https://charmhub.io/glauth-k8s/libraries/ldap,,https://github.com/canonical/glauth-k8s-operator,Charmhub,ldap,https://charmhub.io/integrations/ldap,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/ldap/v0, -charms.lego_base_k8s.lego_client,legacy,https://charmhub.io/lego-base-k8s/libraries/lego_client,,https://github.com/canonical/lego-base-k8s-operator,Charmhub,tls_certificates,https://charmhub.io/integrations/tls_certificates,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/tls_certificates/v1,Legacy charm library used to implement the provider side of this interface. -loadbalancer_interface,,https://pypi.org/project/loadbalancer-interface/,https://github.com/juju-solutions/loadbalancer-interface/blob/main/docs/api.md,https://github.com/juju-solutions/loadbalancer-interface,PyPI,loadbalancer,https://charmhub.io/integrations/loadbalancer,, -charms.identity_platform_login_ui_operator.login_ui_endpoints,,https://charmhub.io/identity-platform-login-ui-operator/libraries/login_ui_endpoints,,https://github.com/canonical/identity-platform-login-ui-operator,Charmhub,login_ui_endpoints,https://charmhub.io/integrations/login_ui_endpoints,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/login_ui_endpoints/v0, -charms.loki_k8s.loki_push_api,,https://charmhub.io/loki-k8s/libraries/loki_push_api,,https://github.com/canonical/loki-k8s-operator,Charmhub,loki_push_api,https://charmhub.io/integrations/loki_push_api,,Manage logs for Loki. -charms.synapse.matrix_auth,,https://charmhub.io/synapse/libraries/matrix_auth,,https://github.com/canonical/synapse-operator,Charmhub,matrix_auth,https://charmhub.io/maubot/integrations#matrix-auth,,"(The Charmhub interfaces page doesn't load, this link is to a charm that uses the interface.)" -charms.nginx_ingress_integrator.nginx_route,,https://charmhub.io/nginx-ingress-integrator/libraries/nginx_route,,https://github.com/canonical/nginx-ingress-integrator-operator,Charmhub,nginx_route,https://charmhub.io/integrations/nginx_route,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/nginx_route/v0,Manage external HTTP access to Kubernetes workloads using Nginx. -charms.oathkeeper.oathkeeper_info,,https://charmhub.io/oathkeeper/libraries/oathkeeper_info,,https://github.com/canonical/oathkeeper-operator,Charmhub,oathkeeper_info,https://charmhub.io/integrations/oathkeeper_info,, -charms.hydra.oauth,,https://charmhub.io/hydra/libraries/oauth,,https://github.com/canonical/hydra-operator,Charmhub,oauth,https://charmhub.io/integrations/oauth,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/oauth/v0, -charms.s3proxy_k8s.object_storage,,https://charmhub.io/s3proxy-k8s/libraries/object_storage,,https://github.com/canonical/s3proxy-k8s-operator,Charmhub,s3,https://charmhub.io/integrations/s3,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/s3/v0, -charms.openfga_k8s.openfga,,https://charmhub.io/openfga-k8s/libraries/openfga,,https://github.com/canonical/openfga-operator,Charmhub,openfga,https://charmhub.io/integrations/openfga,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/openfga/v1, -ops-lib-mysql,legacy,https://pypi.org/project/ops-lib-mysql/,,https://github.com/canonical/ops-lib-mysql,PyPI,mysql,,,"Modern charms should use the ``mysql_client`` interface, using the ``data_interfaces`` lib from ``data-platform-helpers``." -ops-lib-pgsql,legacy,https://pypi.org/project/ops-lib-pgsql/,,https://github.com/canonical/ops-lib-pgsql,PyPI,pgsql,,,"Modern charms should use the ``mysql_client`` interface, using the ``data_interfaces`` lib from ``data-platform-helpers``." -ops.interface_kube_control,,https://pypi.org/project/ops.interface-kube-control/,,https://github.com/charmed-kubernetes/interface-kube-control,PyPI,kube-control,https://charmhub.io/integrations/kube-control,, -charms.parca.parca_scrape,legacy,https://charmhub.io/parca/libraries/parca_scrape,,https://github.com/canonical/parca-operator,Charmhub,parca_scrape,https://charmhub.io/integrations/parca_scrape,,Deprecated in favour of the ``parca-k8s`` charm's ``parka_scrape`` lib. -charms.parca_k8s.parca_scrape,,https://charmhub.io/parca-k8s/libraries/parca_scrape,,https://github.com/canonical/parca-k8s-operator,Charmhub,parca_scrape,https://charmhub.io/integrations/parca_scrape,, -charms.mimir_coordinator_k8s.prometheus_api,,https://charmhub.io/mimir-coordinator-k8s/libraries/prometheus_api,,https://github.com/canonical/mimir-coordinator-k8s-operator,Charmhub,prometheus_api,https://charmhub.io/integrations/prometheus_api,, -charms.prometheus_k8s.prometheus_remote_write,,https://charmhub.io/prometheus-k8s/libraries/prometheus_remote_write,,https://github.com/canonical/prometheus-k8s-operator,Charmhub,prometheus_remote_write,https://charmhub.io/integrations/prometheus_remote_write,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/prometheus_remote_write/v0,Transfer Prometheus metrics data between charms. -charms.prometheus_k8s.prometheus_scrape,,https://charmhub.io/prometheus-k8s/libraries/prometheus_scrape,,https://github.com/canonical/prometheus-k8s-operator,Charmhub,prometheus_scrape,https://charmhub.io/integrations/prometheus_scrape,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/prometheus_scrape/v0,Manage metrics for Prometheus. -charms.prometheus_pushgateway_k8s.pushgateway,,https://charmhub.io/prometheus-pushgateway-k8s/libraries/pushgateway,,https://github.com/canonical/prometheus-pushgateway-k8s-operator,Charmhub,pushgateway,https://charmhub.io/prometheus-pushgateway-k8s/integrations#push-endpoint,,"(The Charmhub interfaces page doesn't load, this link is to a charm that uses the interface.)" -charms.redis_k8s.redis,,https://charmhub.io/redis-k8s/libraries/redis,,https://github.com/canonical/redis-k8s-operator,Charmhub,redis,https://charmhub.io/integrations/redis,, -charms.data_platform_libs.s3,,https://charmhub.io/data-platform-libs/libraries/s3,,https://github.com/canonical/data-platform-libs,Charmhub,s3,https://charmhub.io/integrations/s3,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/s3/v0,Manage s3 credentials and metadata. -charms.saml_integrator.saml,,https://charmhub.io/saml-integrator/libraries/saml,,https://github.com/canonical/saml-integrator-operator,Charmhub,saml,https://charmhub.io/integrations/saml,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/saml/v0, -charms.sdcore_nms_k8s.sdcore_config,,https://charmhub.io/sdcore-nms-k8s/libraries/sdcore_config,,https://github.com/canonical/sdcore-nms-k8s-operator,Charmhub,sdcore_config,https://charmhub.io/integrations/sdcore_config,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/sdcore_config/v0,Provide or require the web UI gRPC address for SD-Core configuration. -charms.sdcore_webui_k8s.sdcore_management,,https://charmhub.io/sdcore-webui-k8s/libraries/sdcore_management,,https://github.com/canonical/sdcore-webui-k8s-operator,Charmhub,sdcore_management,https://charmhub.io/integrations/sdcore_management,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/sdcore_management/v0, -charmlibs.interfaces.service_mesh,recommended,https://pypi.org/project/charmlibs-interfaces-service-mesh,https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/service-mesh,https://github.com/canonical/charmlibs/tree/main/interfaces/service-mesh,PyPI,service_mesh,https://charmhub.io/integrations/service_mesh,,Enroll charms onto a service mesh and provision network policies. -charms.istio_beacon_k8s.service_mesh,legacy,https://charmhub.io/istio-beacon-k8s/libraries/service_mesh,,https://github.com/canonical/istio-beacon-k8s-operator,Charmhub,service_mesh,https://charmhub.io/integrations/service_mesh,,Deprecated in favour of ``charmlibs.interfaces.service_mesh``. -charms.smtp_integrator.smtp,,https://charmhub.io/smtp-integrator/libraries/smtp,,https://github.com/canonical/smtp-integrator-operator,Charmhub,smtp,https://charmhub.io/integrations/smtp,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/smtp/v0, -charms.tempo_coordinator_k8s.tempo_api,,https://charmhub.io/tempo-coordinator-k8s/libraries/tempo_api,,https://github.com/canonical/tempo-coordinator-k8s-operator,Charmhub,tempo_api,https://charmhub.io/integrations/tempo_api,, -charmlibs.interfaces.tls_certificates,recommended,https://pypi.org/project/charmlibs-interfaces-tls-certificates,https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/tls_certificates,https://github.com/canonical/charmlibs/tree/main/interfaces/tls_certificates,PyPI,tls_certificates,https://charmhub.io/integrations/tls_certificates,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/tls_certificates/v1,Manage TLS certificates. -charms.tls_certificates_interface.tls_certificates,legacy,https://charmhub.io/tls-certificates-interface/libraries/tls_certificates,https://charmhub.io/tls-certificates-interface/,https://github.com/canonical/tls-certificates-interface,Charmhub,tls_certificates,https://charmhub.io/integrations/tls_certificates,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/tls_certificates/v1,Manage TLS certificates. Deprecated in favor of ``charmlibs.interfaces.tls_certificates``. -charms.tempo_k8s.tracing,legacy,https://charmhub.io/tempo-k8s/libraries/tracing,,https://github.com/canonical/tempo-coordinator-k8s-operator,Charmhub,tracing,https://charmhub.io/integrations/tracing,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/tracing/v2,Deprecated in favour of ``tempo_coordinator_k8s .tracing``. -charms.tempo_coordinator_k8s.tracing,,https://charmhub.io/tempo-coordinator-k8s/libraries/tracing,,https://github.com/canonical/tempo-coordinator-k8s-operator,Charmhub,tracing,https://charmhub.io/integrations/tracing,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/tracing/v2,Provide and consume tracing data. -charms.traefik_route_k8s.traefik_route,legacy,https://charmhub.io/traefik-route-k8s/libraries/traefik_route,,https://github.com/canonical/traefik-k8s-operator,Charmhub,traefik_route,https://charmhub.io/integrations/traefik_route,,Deprecated in favour of the ``traefik-k8s`` charm's ``traefik_route`` lib. -charms.traefik_k8s.traefik_route,,https://charmhub.io/traefik-k8s/libraries/traefik_route,,https://github.com/canonical/traefik-k8s-operator,Charmhub,traefik_route,https://charmhub.io/integrations/traefik_route,, -charms.vault_k8s.vault_kv,,https://charmhub.io/vault-k8s/libraries/vault_kv,,https://github.com/canonical/vault-k8s-operator,Charmhub,vault_kv,https://charmhub.io/integrations/vault_kv,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/vault_kv/v0, -litmus-libs,,https://pypi.org/project/litmus-libs/,,https://github.com/canonical/litmus-operators,PyPI,litmus_auth,https://charmhub.io/integrations/litmus_auth,https://documentation.ubuntu.com/charmlibs/reference/interfaces/litmus_auth/v0/, -charmlibs.interfaces.istio_request_auth,recommended,https://pypi.org/project/charmlibs-interfaces-istio-request-auth,https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio-request-auth,https://github.com/canonical/charmlibs/tree/main/interfaces/istio-request-auth,PyPI,istio-request-auth,,,Configure Istio RequestAuthentication resources via relation data. -charmed_service_mesh_helpers.interfaces.request_auth,legacy,https://pypi.org/project/charmed-service-mesh-helpers/,,https://github.com/canonical/charmed-service-mesh-helpers,PyPI,istio-request-auth,,,Deprecated in favour of charmlibs.interfaces.istio_request_auth. -charmlibs.interfaces.gateway_metadata,recommended,https://pypi.org/project/charmlibs-interfaces-gateway-metadata,https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/gateway-metadata,https://github.com/canonical/charmlibs/tree/main/interfaces/gateway-metadata,PyPI,gateway-metadata,,,Share Kubernetes Gateway API workload metadata between charms. -charmed_service_mesh_helpers.interfaces.gateway_metadata,legacy,https://pypi.org/project/charmed-service-mesh-helpers/,,https://github.com/canonical/charmed-service-mesh-helpers,PyPI,gateway-metadata,,,Deprecated in favour of ``charmlibs.interfaces.gateway_metadata``. -dpcharmlibs.interfaces,,https://pypi.org/project/dpcharmlibs-interfaces,,https://github.com/canonical/data-platform-charmlibs,PyPI,valkey_client,https://charmhub.io/integrations/valkey_client,https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/valkey_client,Database charms from the Data Platform Team recommend using the generic ``data_platform_libs.data_interfaces`` library to implement the interface. -charmlibs.interfaces.istio_ingress_route,recommended,https://pypi.org/project/charmlibs-interfaces-istio-ingress-route,https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio-ingress-route,https://github.com/canonical/charmlibs/tree/main/interfaces/istio-ingress-route,PyPI,istio-ingress-route,,,Advanced ingress routing through the istio-ingress-k8s charm with multi-port listeners and gRPC support. -charms.istio_ingress_k8s.istio_ingress_route,legacy,https://charmhub.io/istio-ingress-k8s/libraries/istio_ingress_route,,https://github.com/canonical/istio-ingress-k8s-operator,Charmhub,istio-ingress-route,,,Deprecated in favour of ``charmlibs.interfaces.istio_ingress_route``. diff --git a/.docs/reference/libs.yaml b/.docs/reference/libs.yaml new file mode 100644 index 000000000..0ac36dd6c --- /dev/null +++ b/.docs/reference/libs.yaml @@ -0,0 +1,1537 @@ +general: +- name: charms.operator_libs_linux.apt + status: legacy + url: https://charmhub.io/operator-libs-linux/libraries/apt + docs: '' + src: https://github.com/canonical/operator-libs-linux + kind: Charmhub + machine: true + K8s: false + description: Use ``apt`` to install and manage packages. Deprecated in favor of + ``charmlibs.apt``. +- name: charms.mysql.architecture + status: team + url: https://charmhub.io/mysql/libraries/architecture + docs: '' + src: https://github.com/canonical/mysql-operator + kind: Charmhub + machine: true + K8s: true + description: Shared code between the ``mysql`` and ``mysql-k8s`` charms. +- name: charms.mysql.async_replication + status: team + url: https://charmhub.io/mysql/libraries/async_replication + docs: '' + src: https://github.com/canonical/mysql-operator + kind: Charmhub + machine: true + K8s: true + description: Shared code between the ``mysql`` and ``mysql-k8s`` charms. +- name: awsjuju + status: unlisted + url: https://pypi.org/project/awsjuju/ + docs: '' + src: https://github.com/kapilt/awsjuju + kind: PyPI + machine: false + K8s: false + description: Legacy library for managing AWS resources. +- name: charms.mysql.backups + status: team + url: https://charmhub.io/mysql/libraries/backups + docs: '' + src: https://github.com/canonical/mysql-operator + kind: Charmhub + machine: true + K8s: true + description: Shared code between the ``mysql`` and ``mysql-k8s`` charms. +- name: charms.harness_extensions.capture_events + status: legacy + url: https://charmhub.io/harness-extensions/libraries/capture_events + docs: '' + src: https://github.com/PietroPasotti/harness-extensions + kind: Charmhub + machine: true + K8s: true + description: Helper for legacy harness tests. New charms should write state-transition + tests with ``ops[testing]`` instead. +- name: charms.observability_libs.cert_handler + status: '' + url: https://charmhub.io/observability-libs/libraries/cert_handler + docs: '' + src: https://github.com/canonical/observability-libs + kind: Charmhub + machine: true + K8s: true + description: Wraps the requirer side of the ``tls-certificates-interface`` charm's + ``tls_certificates`` lib. +- name: charms.loki_k8s.charm_logging + status: recommended + url: https://charmhub.io/loki-k8s/libraries/charm_logging + docs: '' + src: https://github.com/canonical/loki-k8s-operator + kind: Charmhub + machine: true + K8s: true + description: Add charm code logging to logs sent via ``loki_push_api``. +- name: charms.tempo_coordinator_k8s.charm_tracing + status: legacy + url: https://charmhub.io/tempo-coordinator-k8s/libraries/charm_tracing + docs: '' + src: https://github.com/canonical/tempo-coordinator-k8s-operator + kind: Charmhub + machine: true + K8s: true + description: Provided by the ``tempo-coordinator-k8s`` charm. Consider using ``ops[tracing]`` + instead. +- name: charms.tempo_k8s.charm_tracing + status: legacy + url: https://charmhub.io/tempo-k8s/libraries/charm_tracing + docs: '' + src: https://github.com/canonical/tempo-k8s-operator + kind: Charmhub + machine: true + K8s: true + description: Deprecated in favor of the ``tempo-coordinator-k8s`` charm's libs. + New charms should use ``ops[tracing]`` for tracing charm code instead. +- name: charm-api + status: experimental + url: https://pypi.org/project/charm-api/ + docs: '' + src: https://github.com/canonical/charm-api + kind: PyPI + machine: true + K8s: true + description: Experimental API for writing charms. +- name: charm-helpers + status: legacy + url: https://pypi.org/project/charmhelpers/ + docs: https://charm-helpers.readthedocs.io/ + src: https://github.com/juju/charm-helpers + kind: PyPI + machine: true + K8s: true + description: Pre-Ops library used by reactive charms. +- name: charm-json + status: legacy + url: https://pypi.org/project/charm-json/ + docs: '' + src: https://github.com/canonical/charm-json + kind: PyPI + machine: true + K8s: true + description: JSON typed relation data – unnecessary with Ops 2.23+ due to the addition + of typed relation data. +- name: charm-refresh + status: recommended + url: https://pypi.org/project/charm-refresh/ + docs: https://canonical-charm-refresh.readthedocs-hosted.com/latest/ + src: https://github.com/canonical/charm-refresh + kind: PyPI + machine: true + K8s: true + description: In-place rolling refreshes of stateful charmed applications. +- name: charm-refresh-build-version + status: unlisted + url: https://pypi.org/project/charm-refresh-build-version/ + docs: '' + src: https://github.com/canonical/charm-refresh-build-version + kind: PyPI + machine: true + K8s: true + description: Workaround for a Charmcraft issue. +- name: charm-upgrade + status: unlisted + url: https://pypi.org/project/charm-upgrade/ + docs: '' + src: https://github.com/canonical/charm-upgrade + kind: PyPI + machine: true + K8s: true + description: Alias for charm-refresh? (Repo URL redirects.) +- name: charmed-kubeflow-chisme + status: team + url: https://pypi.org/project/charmed-kubeflow-chisme/ + docs: '' + src: https://github.com/canonical/charmed-kubeflow-chisme + kind: PyPI + machine: true + K8s: true + description: Used internally by the Charmed Kubeflow team. +- name: charmed-service-mesh-helpers + status: team + url: https://pypi.org/project/charmed-service-mesh-helpers/ + docs: '' + src: https://github.com/canonical/charmed-service-mesh-helpers/ + kind: PyPI + machine: false + K8s: true + description: Used internally by the Service Mesh team. +- name: charmlibs.apt + status: recommended + url: https://pypi.org/project/charmlibs-apt + docs: https://documentation.ubuntu.com/charmlibs/reference/charmlibs/apt + src: https://github.com/canonical/charmlibs/tree/main/apt + kind: PyPI + machine: true + K8s: false + description: Use ``apt`` to install and manage packages. +- name: charmlibs.passwd + status: recommended + url: https://pypi.org/project/charmlibs-passwd + docs: https://documentation.ubuntu.com/charmlibs/reference/charmlibs/passwd + src: https://github.com/canonical/charmlibs/tree/main/passwd + kind: PyPI + machine: true + K8s: false + description: Manage Linux users and groups. +- name: charmlibs.pathops + status: recommended + url: https://pypi.org/project/charmlibs-pathops + docs: https://documentation.ubuntu.com/charmlibs/reference/charmlibs/pathops + src: https://github.com/canonical/charmlibs/tree/main/pathops + kind: PyPI + machine: true + K8s: true + description: Substrate agnostic file operations. +- name: charmlibs.rollingops + status: recommended + url: https://pypi.org/project/charmlibs-rollingops + docs: https://documentation.ubuntu.com/charmlibs/reference/charmlibs/rollingops + src: https://github.com/canonical/charmlibs/tree/main/rollingops + kind: PyPI + machine: true + K8s: true + description: Coordinate rolling operations for charms, including across applications + or clusters. +- name: charmlibs.snap + status: recommended + url: https://pypi.org/project/charmlibs-snap + docs: https://documentation.ubuntu.com/charmlibs/reference/charmlibs/snap + src: https://github.com/canonical/charmlibs/tree/main/snap + kind: PyPI + machine: true + K8s: false + description: Use ``snapd`` to install and manage packages. +- name: charmlibs.sysctl + status: recommended + url: https://pypi.org/project/charmlibs-sysctl + docs: https://documentation.ubuntu.com/charmlibs/reference/charmlibs/sysctl + src: https://github.com/canonical/charmlibs/tree/main/sysctl + kind: PyPI + machine: true + K8s: false + description: Create and configure ``sysctl`` options. +- name: charmlibs.systemd + status: recommended + url: https://pypi.org/project/charmlibs-systemd + docs: https://documentation.ubuntu.com/charmlibs/reference/charmlibs/systemd + src: https://github.com/canonical/charmlibs/tree/main/systemd + kind: PyPI + machine: true + K8s: false + description: Use ``systemd`` to start, stop, and manage system services. +- name: charms.contextual_status + status: experimental + url: https://pypi.org/project/charms.contextual-status/ + docs: '' + src: https://github.com/charmed-kubernetes/charm-lib-contextual-status + kind: PyPI + machine: true + K8s: true + description: Context manager based library for setting charm statuses. +- name: charms.docker + status: legacy + url: https://pypi.org/project/charms.docker/ + docs: '' + src: https://github.com/juju-solutions/charms.docker + kind: PyPI + machine: false + K8s: false + description: Legacy library used by the ``docker-layer`` reactive charm. +- name: charms.proxylib + status: '' + url: https://github.com/canonical/charms.proxylib + docs: '' + src: https://github.com/canonical/charms.proxylib + kind: PyPI + machine: true + K8s: true + description: A library that helps charms direct HTTP requests and subprocess calls + through the model-configured proxy environment. +- name: charms.reactive + status: legacy + url: https://pypi.org/project/charms.reactive/ + docs: https://charmsreactive.readthedocs.io/en/latest/ + src: https://github.com/canonical/charms.reactive + kind: PyPI + machine: true + K8s: true + description: Legacy library used to implement reactive charms. +- name: charms.reconciler + status: experimental + url: https://pypi.org/project/charms.reconciler/ + docs: '' + src: https://github.com/charmed-kubernetes/charm-lib-reconciler + kind: PyPI + machine: true + K8s: true + description: Handle all Juju events in an Ops-based charm with a single method. +- name: charms.templating.jinja2 + status: legacy + url: https://pypi.org/project/charms.templating.jinja2/ + docs: https://pythonhosted.org/charms.templating.jinja2/ + src: https://github.com/juju-solutions/charms.templating.jinja2 + kind: PyPI + machine: false + K8s: false + description: Legacy library for Jinja templating in reactive charms. +- name: charms.zookeeper.client + status: '' + url: https://charmhub.io/zookeeper/libraries/client + docs: '' + src: https://github.com/canonical/zookeeper-operator + kind: Charmhub + machine: true + K8s: true + description: Perform ``zookeeper`` operations. +- name: coordinated-workers + status: team + url: https://pypi.org/project/coordinated-workers/ + docs: '' + src: https://github.com/canonical/cos-coordinated-workers + kind: PyPI + machine: true + K8s: true + description: Abstractions for charms following the coordinator-worker pattern, used + by Observability Team. +- name: cosl + status: team + url: https://pypi.org/project/cosl/ + docs: '' + src: https://github.com/canonical/cos-lib + kind: PyPI + machine: true + K8s: true + description: Used internally by the Observability Charm Engineering Team. A dependency + (via ``PYDEPS``) of popular charm libs such as ``loki_push_api``. +- name: charms.data_platform_libs.data_models + status: legacy + url: https://charmhub.io/data-platform-libs/libraries/data_models + docs: '' + src: https://github.com/canonical/data-platform-libs + kind: Charmhub + machine: true + K8s: true + description: '``pydantic``-based typed relation data – unnecessary with Ops 2.23+ + due to the addition of typed relation data.' +- name: charms.data_platform_libs.data_secrets + status: legacy + url: https://charmhub.io/data-platform-libs/libraries/data_secrets + docs: '' + src: https://github.com/canonical/data-platform-libs + kind: Charmhub + machine: true + K8s: true + description: Secrets-related helpers for interfaces. Deprecated in favor of the + ``data-platform-libs.data_inferfaces`` interface lib. +- name: data-platform-helpers + status: team + url: https://pypi.org/project/data-platform-helpers/ + docs: '' + src: https://github.com/canonical/data-platform-helpers + kind: PyPI + machine: true + K8s: true + description: Used internally by the Data Charm Engineering team. +- name: charms.operator_libs_linux.dnf + status: legacy + url: https://charmhub.io/operator-libs-linux/libraries/dnf + docs: '' + src: https://github.com/canonical/operator-libs-linux + kind: Charmhub + machine: true + K8s: false + description: Use ``dnf`` to install and manage packages – modern charms all run + on Ubuntu, so this shouldn't be needed. +- name: charms.harness_extensions.evt_sequences + status: legacy + url: https://charmhub.io/harness-extensions/libraries/evt_sequences + docs: '' + src: https://github.com/PietroPasotti/harness-extensions + kind: Charmhub + machine: true + K8s: true + description: Helper for legacy harness tests. New charms should write state-transition + tests with ``ops[testing]`` instead. +- name: charms.operator_libs_linux.grub + status: legacy + url: https://charmhub.io/operator-libs-linux/libraries/grub + docs: '' + src: https://github.com/canonical/operator-libs-linux + kind: Charmhub + machine: true + K8s: false + description: Use GRUB to manage kernel configuration. This library has fallen out + of use and is no longer actively maintained. Contact Charm Tech if you have a + need for this functionality in your charm. +- name: charms.opensearch.helper_cos + status: team + url: https://charmhub.io/opensearch/libraries/helper_cos + docs: '' + src: https://github.com/canonical/opensearch-operator + kind: Charmhub + machine: false + K8s: false + description: Relies on ``data_platform_helpers``. +- name: hpc-libs + status: team + url: https://github.com/charmed-hpc/hpc-libs + docs: '' + src: '' + kind: git + machine: true + K8s: true + description: Used internally by HPC charms. +- name: charms.operator_libs_linux.juju_systemd_notices + status: legacy + url: https://charmhub.io/operator-libs-linux/libraries/juju_systemd_notices + docs: '' + src: https://github.com/canonical/operator-libs-linux + kind: Charmhub + machine: true + K8s: false + description: Use ``systemd`` to observe and emit notices when services change state. + This library has fallen out of use and is no longer actively maintained. Contact + Charm Tech if you have a need for this functionality in your charm. +- name: charms.observability_libs.juju_topology + status: legacy + url: https://charmhub.io/observability-libs/libraries/juju_topology + docs: '' + src: https://github.com/canonical/observability-libs + kind: Charmhub + machine: true + K8s: true + description: Deprecated in favor of ``cosl.juju_topology.JujuTopology``. +- name: jujubigdata + status: legacy + url: https://pypi.org/project/jujubigdata/ + docs: http://pythonhosted.org/jujubigdata/ + src: https://github.com/juju-solutions/jujubigdata + kind: PyPI + machine: false + K8s: false + description: Legacy library for developing Big Data charms. +- name: jujuresources + status: legacy + url: https://pypi.org/project/jujuresources/ + docs: http://pythonhosted.org/jujuresources/ + src: https://github.com/juju-solutions/jujuresources + kind: PyPI + machine: true + K8s: true + description: Legacy library for loading binary resources. New charms should use + ``ops.Model.resources``. +- name: charms.observability_libs.kubernetes_compute_resources_patch + status: '' + url: https://charmhub.io/observability-libs/libraries/kubernetes_compute_resources_patch + docs: '' + src: https://github.com/canonical/observability-libs + kind: Charmhub + machine: false + K8s: true + description: Patch Kubernetes compute resource limits. +- name: charms.observability_libs.kubernetes_service_patch + status: legacy + url: https://charmhub.io/observability-libs/libraries/kubernetes_service_patch + docs: '' + src: https://github.com/canonical/observability-libs + kind: Charmhub + machine: false + K8s: true + description: Deprecated in favor of ``ops.Unit.set_ports``. +- name: charms.observability_libs.metrics_endpoint_discovery + status: '' + url: https://charmhub.io/observability-libs/libraries/metrics_endpoint_discovery + docs: '' + src: https://github.com/canonical/observability-libs + kind: Charmhub + machine: false + K8s: true + description: Discover metric endpoints exposed by applications deployed to a K8s + cluster. +- name: mongo-charms-single-kernel + status: team + url: https://pypi.org/project/mongo-charms-single-kernel/ + docs: '' + src: https://github.com/canonical/mongo-single-kernel-library + kind: PyPI + machine: true + K8s: true + description: Used internally by the Data Charm Engineering team. +- name: charms.kubernetes_charm_libraries.multus + status: '' + url: https://charmhub.io/kubernetes-charm-libraries/libraries/multus + docs: '' + src: https://github.com/canonical/kubernetes-charm-libraries + kind: Charmhub + machine: false + K8s: true + description: Use the Multus Kubernetes Container Network Interface. +- name: charms.mysql.mysql + status: team + url: https://charmhub.io/mysql/libraries/mysql + docs: '' + src: https://github.com/canonical/mysql-operator + kind: Charmhub + machine: true + K8s: true + description: Shared code between the ``mysql`` and ``mysql-k8s`` charms. +- name: oci-image + status: legacy + url: https://pypi.org/project/oci-image/ + docs: '' + src: https://github.com/juju-solutions/resource-oci-image + kind: PyPI + machine: false + K8s: true + description: Work with OCI images in podspec charms. New Kubernetes charms should + use the sidecar pattern. Repo archived in March 2024. +- name: ops_reactive_interface + status: legacy + url: https://pypi.org/project/ops-reactive-interface/ + docs: https://github.com/canonical/ops-reactive-interface/tree/main/docs + src: https://github.com/juju-solutions/ops-reactive-interface + kind: PyPI + machine: true + K8s: true + description: Helper for interface library developers, to allow an Ops-based interface + library to interact with legacy, reactive charms. +- name: ops.manifest + status: '' + url: https://pypi.org/project/ops.manifest/ + docs: '' + src: https://github.com/canonical/ops-lib-manifest + kind: PyPI + machine: false + K8s: true + description: Work with Kubernetes manifests. +- name: charms.operator_libs_linux.passwd + status: legacy + url: https://charmhub.io/operator-libs-linux/libraries/passwd + docs: '' + src: https://github.com/canonical/operator-libs-linux + kind: Charmhub + machine: true + K8s: false + description: Manage Linux users and groups. Deprecated in favor of ``charmlibs.passwd``. +- name: charms.pgbouncer_k8s.pgb + status: team + url: https://charmhub.io/pgbouncer-k8s/libraries/pgb + docs: '' + src: https://github.com/canonical/pgbouncer-k8s-operator + kind: Charmhub + machine: true + K8s: true + description: Shared code between ``pgbouncer`` and ``pgbouncer-k8s`` charms. +- name: charms.postgresql_k8s.postgresql + status: team + url: https://charmhub.io/postgresql-k8s/libraries/postgresql + docs: '' + src: https://github.com/canonical/postgresql-k8s-operator + kind: Charmhub + machine: true + K8s: true + description: Shared code between the ``postgresql`` and ``postgresql-k8s`` charms. +- name: charms.rolling_ops.rollingops + status: legacy + url: https://charmhub.io/rolling-ops/libraries/rollingops + docs: '' + src: https://github.com/canonical/charm-rolling-ops + kind: Charmhub + machine: true + K8s: true + description: Legacy library for performing "rolling" operations across units, for + example rolling restarts. New charms should use ``charmlibs.rollingops``. +- name: charms.mysql.s3_helpers + status: team + url: https://charmhub.io/mysql/libraries/s3_helpers + docs: '' + src: https://github.com/canonical/mysql-operator + kind: Charmhub + machine: true + K8s: true + description: Shared code between the ``mysql`` and ``mysql-k8s`` charms. +- name: sborl + status: legacy + url: https://pypi.org/project/sborl/ + docs: '' + src: https://github.com/canonical/sborl + kind: PyPI + machine: true + K8s: true + description: Legacy library for implementing interface libraries. New interface + libraries can use the typed relation data feature available in Ops 2.23+. +- name: serialized-data-interface + status: legacy + url: https://pypi.org/project/serialized-data-interface/ + docs: '' + src: https://github.com/canonical/serialized-data-interface + kind: PyPI + machine: true + K8s: true + description: Relation data validation – use the features available in Ops instead. +- name: charms.operator_libs_linux.snap + status: legacy + url: https://charmhub.io/operator-libs-linux/libraries/snap + docs: '' + src: https://github.com/canonical/operator-libs-linux + kind: Charmhub + machine: true + K8s: false + description: Use ``snapd`` to install and manage packages. Deprecated in favor of + ``charmlibs.snap``. +- name: charms.operator_libs_linux.sysctl + status: legacy + url: https://charmhub.io/operator-libs-linux/libraries/sysctl + docs: '' + src: https://github.com/canonical/operator-libs-linux + kind: Charmhub + machine: true + K8s: false + description: Create and configure ``sysctl`` options. Deprecated in favor of ``charmlibs.sysctl``. +- name: charms.operator_libs_linux.systemd + status: legacy + url: https://charmhub.io/operator-libs-linux/libraries/systemd + docs: '' + src: https://github.com/canonical/operator-libs-linux + kind: Charmhub + machine: true + K8s: false + description: Use ``systemd`` to start, stop, and manage system services. Deprecated + in favor of ``charmlibs.systemd``. +- name: charms.mysql.tls + status: team + url: https://charmhub.io/mysql/libraries/tls + docs: '' + src: https://github.com/canonical/mysql-operator + kind: Charmhub + machine: true + K8s: true + description: Shared code between the ``mysql`` and ``mysql-k8s`` charms. +- name: charms.data_platform_libs.upgrade + status: legacy + url: https://charmhub.io/data-platform-libs/libraries/upgrade + docs: '' + src: https://github.com/canonical/data-platform-libs + kind: Charmhub + machine: true + K8s: true + description: Manage in-place upgrades. Deprecated in favor of ``charm-refresh``. +interfaces: +- name: charms.opencti.opencti_connector + status: '' + url: https://charmhub.io/opencti/libraries/opencti_connector + docs: '' + src: https://github.com/canonical/opencti-operator/ + kind: Charmhub + rel_name: opencti-connector + rel_url_charmhub: https://charmhub.io/integrations/opencti_connector + rel_url_schema: https://documentation.ubuntu.com/charmlibs/reference/interfaces/opencti_connector/v0/ + description: '' +- name: charms.filesystem_client.filesystem_info + status: '' + url: https://charmhub.io/filesystem-client/libraries/filesystem_info + docs: '' + src: https://github.com/charmed-hpc/filesystem-charms + kind: Charmhub + rel_name: filesystem_info + rel_url_charmhub: https://charmhub.io/integrations/filesystem_info + rel_url_schema: '' + description: '' +- name: charms.storage_libs.nfs_interfaces + status: legacy + url: https://charmhub.io/storage-libs/libraries/nfs_interfaces + docs: '' + src: '' + kind: Charmhub + rel_name: nfs_share + rel_url_charmhub: https://charmhub.io/integrations/nfs_share + rel_url_schema: '' + description: This interface is deprecated in favour of ``filesystem_info``. +- name: charms.alertmanager_k8s.alertmanager_dispatch + status: '' + url: https://charmhub.io/alertmanager-k8s/libraries/alertmanager_dispatch + docs: '' + src: https://github.com/canonical/alertmanager-k8s-operator + kind: Charmhub + rel_name: alertmanager_dispatch + rel_url_charmhub: https://charmhub.io/integrations/alertmanager_dispatch + rel_url_schema: '' + description: '' +- name: charms.alertmanager_k8s.alertmanager_remote_configuration + status: '' + url: https://charmhub.io/alertmanager-k8s/libraries/alertmanager_remote_configuration + docs: '' + src: https://github.com/canonical/alertmanager-k8s-operator + kind: Charmhub + rel_name: alertmanager_remote_configuration + rel_url_charmhub: https://charmhub.io/integrations/alertmanager_remote_configuration + rel_url_schema: '' + description: '' +- name: charms.oathkeeper.auth_proxy + status: '' + url: https://charmhub.io/oathkeeper/libraries/auth_proxy + docs: '' + src: https://github.com/canonical/oathkeeper-operator + kind: Charmhub + rel_name: auth_proxy + rel_url_charmhub: https://charmhub.io/integrations/auth_proxy + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/auth_proxy/v0 + description: '' +- name: charms.oauth2_proxy_k8s.auth_proxy + status: '' + url: https://charmhub.io/oauth2-proxy-k8s/libraries/auth_proxy + docs: '' + src: https://github.com/canonical/oauth2-proxy-k8s-operator + kind: Charmhub + rel_name: auth_proxy + rel_url_charmhub: https://charmhub.io/integrations/auth_proxy + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/auth_proxy/v0 + description: '' +- name: charms.blackbox_exporter_k8s.blackbox_probes + status: '' + url: https://charmhub.io/blackbox-exporter-k8s/libraries/blackbox_probes + docs: '' + src: https://github.com/canonical/blackbox-exporter-k8s-operator + kind: Charmhub + rel_name: blackbox_exporter_probes + rel_url_charmhub: https://charmhub.io/blackbox-exporter-k8s/integrations#probes + rel_url_schema: '' + description: (The Charmhub interfaces page doesn't load, this link is to a charm + that uses the interface.) +- name: charms.catalogue_k8s.catalogue + status: '' + url: https://charmhub.io/catalogue-k8s/libraries/catalogue + docs: '' + src: https://github.com/canonical/catalogue-k8s-operator + kind: Charmhub + rel_name: catalogue + rel_url_charmhub: https://charmhub.io/integrations/catalogue + rel_url_schema: '' + description: Transfer catalogue data for display by the provider. +- name: charms.certificate_transfer_interface.certificate_transfer + status: '' + url: https://charmhub.io/certificate-transfer-interface/libraries/certificate_transfer + docs: '' + src: https://github.com/canonical/certificate-transfer-interface + kind: Charmhub + rel_name: certificate_transfer + rel_url_charmhub: https://charmhub.io/integrations/certificate_transfer + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/certificate_transfer/v1 + description: Transfer certificates between charms. +- name: charms.grafana_cloud_integrator.cloud_config_provider + status: '' + url: https://charmhub.io/grafana-cloud-integrator/libraries/cloud_config_provider + docs: '' + src: https://github.com/canonical/grafana-cloud-integrator + kind: Charmhub + rel_name: grafana_cloud_config + rel_url_charmhub: https://charmhub.io/integrations/grafana_cloud_config + rel_url_schema: '' + description: '' +- name: charms.grafana_cloud_integrator.cloud_config_requirer + status: '' + url: https://charmhub.io/grafana-cloud-integrator/libraries/cloud_config_requirer + docs: '' + src: https://github.com/canonical/grafana-cloud-integrator + kind: Charmhub + rel_name: grafana_cloud_config + rel_url_charmhub: https://charmhub.io/integrations/grafana_cloud_config + rel_url_schema: '' + description: '' +- name: charms.cloudflare_configurator.cloudflared_route + status: '' + url: https://charmhub.io/cloudflare-configurator/libraries/cloudflared_route + docs: '' + src: https://github.com/canonical/cloudflare-configurator-operator + kind: Charmhub + rel_name: cloudflared_route + rel_url_charmhub: https://charmhub.io/integrations/cloudflared_route + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/cloudflared_route/v0 + description: '' +- name: charms.grafana_agent.cos_agent + status: '' + url: https://charmhub.io/grafana-agent/libraries/cos_agent + docs: '' + src: https://github.com/canonical/grafana-agent-operator + kind: Charmhub + rel_name: cos_agent + rel_url_charmhub: https://charmhub.io/integrations/cos_agent + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/cos_agent/v0 + description: Machine charm specific interface for exchanging observability related + data. +- name: charms.data_platform_libs.data_interfaces + status: '' + url: https://charmhub.io/data-platform-libs/libraries/data_interfaces + docs: '' + src: https://github.com/canonical/data-platform-libs + kind: Charmhub + rel_name: etcd_client + rel_url_charmhub: https://charmhub.io/integrations/etcd_client + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/etcd_client + description: Database charms from the Data Platform Team recommend using the generic + ``data_platform_libs.data_interfaces`` library to implement the interface. +- name: charms.data_platform_libs.data_interfaces + status: '' + url: https://charmhub.io/data-platform-libs/libraries/data_interfaces + docs: '' + src: https://github.com/canonical/data-platform-libs + kind: Charmhub + rel_name: mysql_client + rel_url_charmhub: https://charmhub.io/integrations/mysql_client + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/mysql_client + description: Database charms from the Data Platform Team recommend using the generic + ``data_platform_libs.data_interfaces`` library to implement the interface. +- name: charms.data_platform_libs.data_interfaces + status: '' + url: https://charmhub.io/data-platform-libs/libraries/data_interfaces + docs: '' + src: https://github.com/canonical/data-platform-libs + kind: Charmhub + rel_name: mongodb_client + rel_url_charmhub: https://charmhub.io/integrations/mongodb_client + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/mongodb_client + description: Database charms from the Data Platform Team recommend using the generic + ``data_platform_libs.data_interfaces`` library to implement the interface. +- name: charms.data_platform_libs.data_interfaces + status: '' + url: https://charmhub.io/data-platform-libs/libraries/data_interfaces + docs: '' + src: https://github.com/canonical/data-platform-libs + kind: Charmhub + rel_name: opensearch_client + rel_url_charmhub: https://charmhub.io/integrations/opensearch_client + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/opensearch_client + description: Database charms from the Data Platform Team recommend using the generic + ``data_platform_libs.data_interfaces`` library to implement the interface. +- name: charms.data_platform_libs.data_interfaces + status: '' + url: https://charmhub.io/data-platform-libs/libraries/data_interfaces + docs: '' + src: https://github.com/canonical/data-platform-libs + kind: Charmhub + rel_name: postgresql_client + rel_url_charmhub: https://charmhub.io/integrations/postgresql_client + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/postgresql_client + description: Database charms from the Data Platform Team recommend using the generic + ``data_platform_libs.data_interfaces`` library to implement the interface. +- name: charms.data_platform_libs.database_provides + status: legacy + url: https://charmhub.io/data-platform-libs/libraries/database_provides + docs: '' + src: https://github.com/canonical/data-platform-libs + kind: Charmhub + rel_name: '' + rel_url_charmhub: '' + rel_url_schema: '' + description: Deprecated in favour of ``data_platform_helpers.data_interfaces``. +- name: charms.data_platform_libs.database_requires + status: legacy + url: https://charmhub.io/data-platform-libs/libraries/database_requires + docs: '' + src: https://github.com/canonical/data-platform-libs + kind: Charmhub + rel_name: '' + rel_url_charmhub: '' + rel_url_schema: '' + description: Deprecated in favour of ``data_platform_helpers.data_interfaces``. +- name: charms.dex_auth.dex_oidc_config + status: '' + url: https://charmhub.io/dex-auth/libraries/dex_oidc_config + docs: '' + src: https://github.com/canonical/dex-auth-operator + kind: Charmhub + rel_name: dex-oidc-config + rel_url_charmhub: https://charmhub.io/integrations/dex-oidc-config + rel_url_schema: '' + description: '' +- name: charms.sdcore_nms_k8s.fiveg_core_gnb + status: '' + url: https://charmhub.io/sdcore-nms-k8s/libraries/fiveg_core_gnb + docs: '' + src: https://github.com/canonical/sdcore-nms-k8s-operator + kind: Charmhub + rel_name: fiveg_core_gnb + rel_url_charmhub: https://charmhub.io/integrations/fiveg_core_gnb + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/fiveg_core_gnb/v0 + description: '' +- name: charms.sdcore_amf_k8s.fiveg_n2 + status: '' + url: https://charmhub.io/sdcore-amf-k8s/libraries/fiveg_n2 + docs: '' + src: https://github.com/canonical/sdcore-amf-k8s-operator + kind: Charmhub + rel_name: fiveg_n2 + rel_url_charmhub: https://charmhub.io/integrations/fiveg_n2 + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/fiveg_n2/v0 + description: '' +- name: charms.sdcore_upf_k8s.fiveg_n4 + status: '' + url: https://charmhub.io/sdcore-upf-k8s/libraries/fiveg_n4 + docs: '' + src: https://github.com/canonical/sdcore-upf-k8s-operator + kind: Charmhub + rel_name: fiveg_n4 + rel_url_charmhub: https://charmhub.io/integrations/fiveg_n4 + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/fiveg_n4/v0 + description: '' +- name: charms.sdcore_nrf_k8s.fiveg_nrf + status: '' + url: https://charmhub.io/sdcore-nrf-k8s/libraries/fiveg_nrf + docs: '' + src: https://github.com/canonical/sdcore-nrf-k8s-operator + kind: Charmhub + rel_name: fiveg_nrf + rel_url_charmhub: https://charmhub.io/integrations/fiveg_nrf + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/fiveg_nrf/v0 + description: Transfer Network Repository Function information from one charm to + another. +- name: charms.oathkeeper.forward_auth + status: '' + url: https://charmhub.io/oathkeeper/libraries/forward_auth + docs: '' + src: https://github.com/canonical/oathkeeper-operator + kind: Charmhub + rel_name: forward_auth + rel_url_charmhub: https://charmhub.io/integrations/forward_auth + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/forward_auth/v0 + description: '' +- name: charms.oauth2_proxy_k8s.forward_auth + status: '' + url: https://charmhub.io/oauth2-proxy-k8s/libraries/forward_auth + docs: '' + src: https://github.com/canonical/oauth2-proxy-k8s-operator + kind: Charmhub + rel_name: forward_auth + rel_url_charmhub: https://charmhub.io/integrations/forward_auth + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/forward_auth/v0 + description: '' +- name: charms.glauth_utils.glauth_auxiliary + status: '' + url: https://charmhub.io/glauth-utils/libraries/glauth_auxiliary + docs: '' + src: https://github.com/canonical/glauth-utils + kind: Charmhub + rel_name: glauth_auxiliary + rel_url_charmhub: https://charmhub.io/integrations/glauth_auxiliary + rel_url_schema: '' + description: '' +- name: charms.grafana_k8s.grafana_auth + status: '' + url: https://charmhub.io/grafana-k8s/libraries/grafana_auth + docs: '' + src: https://github.com/canonical/grafana-k8s-operator + kind: Charmhub + rel_name: grafana_auth + rel_url_charmhub: https://charmhub.io/integrations/grafana_auth + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/grafana_auth/v0 + description: '' +- name: charms.grafana_k8s.grafana_dashboard + status: '' + url: https://charmhub.io/grafana-k8s/libraries/grafana_dashboard + docs: '' + src: https://github.com/canonical/grafana-k8s-operator + kind: Charmhub + rel_name: grafana_dashboard + rel_url_charmhub: https://charmhub.io/integrations/grafana_dashboard + rel_url_schema: '' + description: Manage dashboards for Grafana. +- name: charms.grafana_k8s.grafana_metadata + status: '' + url: https://charmhub.io/grafana-k8s/libraries/grafana_metadata + docs: '' + src: https://github.com/canonical/grafana-k8s-operator + kind: Charmhub + rel_name: grafana_metadata + rel_url_charmhub: https://charmhub.io/integrations/grafana_metadata + rel_url_schema: '' + description: '' +- name: charms.grafana_k8s.grafana_source + status: '' + url: https://charmhub.io/grafana-k8s/libraries/grafana_source + docs: '' + src: https://github.com/canonical/grafana-k8s-operator + kind: Charmhub + rel_name: grafana-source + rel_url_charmhub: https://charmhub.io/integrations/grafana-source + rel_url_schema: '' + description: Provide a data source for Grafana dashboards. +- name: charms.hydra.hydra_endpoints + status: '' + url: https://charmhub.io/hydra/libraries/hydra_endpoints + docs: '' + src: https://github.com/canonical/hydra-operator + kind: Charmhub + rel_name: hydra_endpoints + rel_url_charmhub: https://charmhub.io/integrations/hydra_endpoints + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/hydra_endpoints/v0 + description: '' +- name: charms.traefik_k8s.ingress + status: '' + url: https://charmhub.io/traefik-k8s/libraries/ingress + docs: '' + src: https://github.com/canonical/traefik-k8s-operator + kind: Charmhub + rel_name: ingress + rel_url_charmhub: https://charmhub.io/integrations/ingress + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/ingress/v2 + description: Manage per-application ingress and and load-balancing. +- name: charms.nginx_ingress_integrator.ingress + status: '' + url: https://charmhub.io/nginx-ingress-integrator/libraries/ingress + docs: '' + src: https://github.com/canonical/nginx-ingress-integrator-operator + kind: Charmhub + rel_name: ingress + rel_url_charmhub: https://charmhub.io/integrations/ingress + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/ingress/v2 + description: Manage ingress. +- name: charms.traefik_k8s.ingress_per_unit + status: '' + url: https://charmhub.io/traefik-k8s/libraries/ingress_per_unit + docs: '' + src: https://github.com/canonical/traefik-k8s-operator + kind: Charmhub + rel_name: ingress_per_unit + rel_url_charmhub: https://charmhub.io/integrations/ingress_per_unit + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/ingress_per_unit/v0 + description: '' +- name: charms.istio_pilot.istio_gateway_info + status: '' + url: https://charmhub.io/istio-pilot/libraries/istio_gateway_info + docs: '' + src: https://github.com/canonical/istio-operators + kind: Charmhub + rel_name: istio-gateway-info + rel_url_charmhub: https://charmhub.io/integrations/istio-gateway-info + rel_url_schema: '' + description: '' +- name: charms.mlops_libs.k8s_service_info + status: '' + url: https://charmhub.io/mlops-libs/libraries/k8s_service_info + docs: '' + src: https://github.com/canonical/mlops-libs + kind: Charmhub + rel_name: k8s-service + rel_url_charmhub: https://charmhub.io/integrations/k8s-service + rel_url_schema: '' + description: '' +- name: charms.karma_k8s.karma_dashboard + status: '' + url: https://charmhub.io/karma-k8s/libraries/karma_dashboard + docs: '' + src: https://github.com/canonical/karma-k8s-operator + kind: Charmhub + rel_name: karma_dashboard + rel_url_charmhub: https://charmhub.io/integrations/karma_dashboard + rel_url_schema: '' + description: '' +- name: charms.kratos_external_idp_integrator.kratos_external_provider + status: '' + url: https://charmhub.io/kratos-external-idp-integrator/libraries/kratos_external_provider + docs: '' + src: https://github.com/canonical/kratos-external-idp-integrator + kind: Charmhub + rel_name: external_provider + rel_url_charmhub: https://charmhub.io/integrations/external_provider + rel_url_schema: '' + description: '' +- name: charms.kratos.kratos_info + status: '' + url: https://charmhub.io/kratos/libraries/kratos_info + docs: '' + src: https://github.com/canonical/kratos-operator + kind: Charmhub + rel_name: kratos_info + rel_url_charmhub: https://charmhub.io/integrations/kratos_info + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/kratos_info/v0 + description: '' +- name: charms.kratos.kratos_registration_webhook + status: unlisted + url: https://charmhub.io/kratos/libraries/kratos_registration_webhook + docs: '' + src: https://github.com/canonical/kratos-operator + kind: Charmhub + rel_name: '' + rel_url_charmhub: '' + rel_url_schema: '' + description: '' +- name: charms.kubeflow_dashboard.kubeflow_dashboard_links + status: '' + url: https://charmhub.io/kubeflow-dashboard/libraries/kubeflow_dashboard_links + docs: '' + src: https://github.com/canonical/kubeflow-dashboard-operator + kind: Charmhub + rel_name: kubeflow_dashboard_links + rel_url_charmhub: https://charmhub.io/integrations/kubeflow_dashboard_links + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/kubeflow_dashboard_links/v0 + description: '' +- name: charms.resource_dispatcher.kubernetes_manifests + status: '' + url: https://charmhub.io/resource-dispatcher/libraries/kubernetes_manifests + docs: '' + src: https://github.com/canonical/resource-dispatcher + kind: Charmhub + rel_name: kubernetes_manifest + rel_url_charmhub: https://charmhub.io/integrations/kubernetes_manifest + rel_url_schema: '' + description: '' +- name: charms.glauth_k8s.ldap + status: '' + url: https://charmhub.io/glauth-k8s/libraries/ldap + docs: '' + src: https://github.com/canonical/glauth-k8s-operator + kind: Charmhub + rel_name: ldap + rel_url_charmhub: https://charmhub.io/integrations/ldap + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/ldap/v0 + description: '' +- name: charms.lego_base_k8s.lego_client + status: legacy + url: https://charmhub.io/lego-base-k8s/libraries/lego_client + docs: '' + src: https://github.com/canonical/lego-base-k8s-operator + kind: Charmhub + rel_name: tls_certificates + rel_url_charmhub: https://charmhub.io/integrations/tls_certificates + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/tls_certificates/v1 + description: Legacy charm library used to implement the provider side of this interface. +- name: loadbalancer_interface + status: '' + url: https://pypi.org/project/loadbalancer-interface/ + docs: https://github.com/juju-solutions/loadbalancer-interface/blob/main/docs/api.md + src: https://github.com/juju-solutions/loadbalancer-interface + kind: PyPI + rel_name: loadbalancer + rel_url_charmhub: https://charmhub.io/integrations/loadbalancer + rel_url_schema: '' + description: '' +- name: charms.identity_platform_login_ui_operator.login_ui_endpoints + status: '' + url: https://charmhub.io/identity-platform-login-ui-operator/libraries/login_ui_endpoints + docs: '' + src: https://github.com/canonical/identity-platform-login-ui-operator + kind: Charmhub + rel_name: login_ui_endpoints + rel_url_charmhub: https://charmhub.io/integrations/login_ui_endpoints + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/login_ui_endpoints/v0 + description: '' +- name: charms.loki_k8s.loki_push_api + status: '' + url: https://charmhub.io/loki-k8s/libraries/loki_push_api + docs: '' + src: https://github.com/canonical/loki-k8s-operator + kind: Charmhub + rel_name: loki_push_api + rel_url_charmhub: https://charmhub.io/integrations/loki_push_api + rel_url_schema: '' + description: Manage logs for Loki. +- name: charms.synapse.matrix_auth + status: '' + url: https://charmhub.io/synapse/libraries/matrix_auth + docs: '' + src: https://github.com/canonical/synapse-operator + kind: Charmhub + rel_name: matrix_auth + rel_url_charmhub: https://charmhub.io/maubot/integrations#matrix-auth + rel_url_schema: '' + description: (The Charmhub interfaces page doesn't load, this link is to a charm + that uses the interface.) +- name: charms.nginx_ingress_integrator.nginx_route + status: '' + url: https://charmhub.io/nginx-ingress-integrator/libraries/nginx_route + docs: '' + src: https://github.com/canonical/nginx-ingress-integrator-operator + kind: Charmhub + rel_name: nginx_route + rel_url_charmhub: https://charmhub.io/integrations/nginx_route + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/nginx_route/v0 + description: Manage external HTTP access to Kubernetes workloads using Nginx. +- name: charms.oathkeeper.oathkeeper_info + status: '' + url: https://charmhub.io/oathkeeper/libraries/oathkeeper_info + docs: '' + src: https://github.com/canonical/oathkeeper-operator + kind: Charmhub + rel_name: oathkeeper_info + rel_url_charmhub: https://charmhub.io/integrations/oathkeeper_info + rel_url_schema: '' + description: '' +- name: charms.hydra.oauth + status: '' + url: https://charmhub.io/hydra/libraries/oauth + docs: '' + src: https://github.com/canonical/hydra-operator + kind: Charmhub + rel_name: oauth + rel_url_charmhub: https://charmhub.io/integrations/oauth + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/oauth/v0 + description: '' +- name: charms.s3proxy_k8s.object_storage + status: '' + url: https://charmhub.io/s3proxy-k8s/libraries/object_storage + docs: '' + src: https://github.com/canonical/s3proxy-k8s-operator + kind: Charmhub + rel_name: s3 + rel_url_charmhub: https://charmhub.io/integrations/s3 + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/s3/v0 + description: '' +- name: charms.openfga_k8s.openfga + status: '' + url: https://charmhub.io/openfga-k8s/libraries/openfga + docs: '' + src: https://github.com/canonical/openfga-operator + kind: Charmhub + rel_name: openfga + rel_url_charmhub: https://charmhub.io/integrations/openfga + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/openfga/v1 + description: '' +- name: ops-lib-mysql + status: legacy + url: https://pypi.org/project/ops-lib-mysql/ + docs: '' + src: https://github.com/canonical/ops-lib-mysql + kind: PyPI + rel_name: mysql + rel_url_charmhub: '' + rel_url_schema: '' + description: Modern charms should use the ``mysql_client`` interface, using the + ``data_interfaces`` lib from ``data-platform-helpers``. +- name: ops-lib-pgsql + status: legacy + url: https://pypi.org/project/ops-lib-pgsql/ + docs: '' + src: https://github.com/canonical/ops-lib-pgsql + kind: PyPI + rel_name: pgsql + rel_url_charmhub: '' + rel_url_schema: '' + description: Modern charms should use the ``postgresql_client`` interface, using the + ``data_interfaces`` lib from ``data-platform-helpers``. +- name: ops.interface_kube_control + status: '' + url: https://pypi.org/project/ops.interface-kube-control/ + docs: '' + src: https://github.com/charmed-kubernetes/interface-kube-control + kind: PyPI + rel_name: kube-control + rel_url_charmhub: https://charmhub.io/integrations/kube-control + rel_url_schema: '' + description: '' +- name: charms.parca.parca_scrape + status: legacy + url: https://charmhub.io/parca/libraries/parca_scrape + docs: '' + src: https://github.com/canonical/parca-operator + kind: Charmhub + rel_name: parca_scrape + rel_url_charmhub: https://charmhub.io/integrations/parca_scrape + rel_url_schema: '' + description: Deprecated in favour of the ``parca-k8s`` charm's ``parca_scrape`` + lib. +- name: charms.parca_k8s.parca_scrape + status: '' + url: https://charmhub.io/parca-k8s/libraries/parca_scrape + docs: '' + src: https://github.com/canonical/parca-k8s-operator + kind: Charmhub + rel_name: parca_scrape + rel_url_charmhub: https://charmhub.io/integrations/parca_scrape + rel_url_schema: '' + description: '' +- name: charms.mimir_coordinator_k8s.prometheus_api + status: '' + url: https://charmhub.io/mimir-coordinator-k8s/libraries/prometheus_api + docs: '' + src: https://github.com/canonical/mimir-coordinator-k8s-operator + kind: Charmhub + rel_name: prometheus_api + rel_url_charmhub: https://charmhub.io/integrations/prometheus_api + rel_url_schema: '' + description: '' +- name: charms.prometheus_k8s.prometheus_remote_write + status: '' + url: https://charmhub.io/prometheus-k8s/libraries/prometheus_remote_write + docs: '' + src: https://github.com/canonical/prometheus-k8s-operator + kind: Charmhub + rel_name: prometheus_remote_write + rel_url_charmhub: https://charmhub.io/integrations/prometheus_remote_write + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/prometheus_remote_write/v0 + description: Transfer Prometheus metrics data between charms. +- name: charms.prometheus_k8s.prometheus_scrape + status: '' + url: https://charmhub.io/prometheus-k8s/libraries/prometheus_scrape + docs: '' + src: https://github.com/canonical/prometheus-k8s-operator + kind: Charmhub + rel_name: prometheus_scrape + rel_url_charmhub: https://charmhub.io/integrations/prometheus_scrape + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/prometheus_scrape/v0 + description: Manage metrics for Prometheus. +- name: charms.prometheus_pushgateway_k8s.pushgateway + status: '' + url: https://charmhub.io/prometheus-pushgateway-k8s/libraries/pushgateway + docs: '' + src: https://github.com/canonical/prometheus-pushgateway-k8s-operator + kind: Charmhub + rel_name: pushgateway + rel_url_charmhub: https://charmhub.io/prometheus-pushgateway-k8s/integrations#push-endpoint + rel_url_schema: '' + description: (The Charmhub interfaces page doesn't load, this link is to a charm + that uses the interface.) +- name: charms.redis_k8s.redis + status: '' + url: https://charmhub.io/redis-k8s/libraries/redis + docs: '' + src: https://github.com/canonical/redis-k8s-operator + kind: Charmhub + rel_name: redis + rel_url_charmhub: https://charmhub.io/integrations/redis + rel_url_schema: '' + description: '' +- name: charms.data_platform_libs.s3 + status: '' + url: https://charmhub.io/data-platform-libs/libraries/s3 + docs: '' + src: https://github.com/canonical/data-platform-libs + kind: Charmhub + rel_name: s3 + rel_url_charmhub: https://charmhub.io/integrations/s3 + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/s3/v0 + description: Manage s3 credentials and metadata. +- name: charms.saml_integrator.saml + status: '' + url: https://charmhub.io/saml-integrator/libraries/saml + docs: '' + src: https://github.com/canonical/saml-integrator-operator + kind: Charmhub + rel_name: saml + rel_url_charmhub: https://charmhub.io/integrations/saml + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/saml/v0 + description: '' +- name: charms.sdcore_nms_k8s.sdcore_config + status: '' + url: https://charmhub.io/sdcore-nms-k8s/libraries/sdcore_config + docs: '' + src: https://github.com/canonical/sdcore-nms-k8s-operator + kind: Charmhub + rel_name: sdcore_config + rel_url_charmhub: https://charmhub.io/integrations/sdcore_config + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/sdcore_config/v0 + description: Provide or require the web UI gRPC address for SD-Core configuration. +- name: charms.sdcore_webui_k8s.sdcore_management + status: '' + url: https://charmhub.io/sdcore-webui-k8s/libraries/sdcore_management + docs: '' + src: https://github.com/canonical/sdcore-webui-k8s-operator + kind: Charmhub + rel_name: sdcore_management + rel_url_charmhub: https://charmhub.io/integrations/sdcore_management + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/sdcore_management/v0 + description: '' +- name: charmlibs.interfaces.service_mesh + status: recommended + url: https://pypi.org/project/charmlibs-interfaces-service-mesh + docs: https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/service-mesh + src: https://github.com/canonical/charmlibs/tree/main/interfaces/service-mesh + kind: PyPI + rel_name: service_mesh + rel_url_charmhub: https://charmhub.io/integrations/service_mesh + rel_url_schema: '' + description: Enroll charms onto a service mesh and provision network policies. +- name: charms.istio_beacon_k8s.service_mesh + status: legacy + url: https://charmhub.io/istio-beacon-k8s/libraries/service_mesh + docs: '' + src: https://github.com/canonical/istio-beacon-k8s-operator + kind: Charmhub + rel_name: service_mesh + rel_url_charmhub: https://charmhub.io/integrations/service_mesh + rel_url_schema: '' + description: Deprecated in favour of ``charmlibs.interfaces.service_mesh``. +- name: charms.smtp_integrator.smtp + status: '' + url: https://charmhub.io/smtp-integrator/libraries/smtp + docs: '' + src: https://github.com/canonical/smtp-integrator-operator + kind: Charmhub + rel_name: smtp + rel_url_charmhub: https://charmhub.io/integrations/smtp + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/smtp/v0 + description: '' +- name: charms.tempo_coordinator_k8s.tempo_api + status: '' + url: https://charmhub.io/tempo-coordinator-k8s/libraries/tempo_api + docs: '' + src: https://github.com/canonical/tempo-coordinator-k8s-operator + kind: Charmhub + rel_name: tempo_api + rel_url_charmhub: https://charmhub.io/integrations/tempo_api + rel_url_schema: '' + description: '' +- name: charmlibs.interfaces.tls_certificates + status: recommended + url: https://pypi.org/project/charmlibs-interfaces-tls-certificates + docs: https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/tls_certificates + src: https://github.com/canonical/charmlibs/tree/main/interfaces/tls_certificates + kind: PyPI + rel_name: tls_certificates + rel_url_charmhub: https://charmhub.io/integrations/tls_certificates + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/tls_certificates/v1 + description: Manage TLS certificates. +- name: charms.tls_certificates_interface.tls_certificates + status: legacy + url: https://charmhub.io/tls-certificates-interface/libraries/tls_certificates + docs: https://charmhub.io/tls-certificates-interface/ + src: https://github.com/canonical/tls-certificates-interface + kind: Charmhub + rel_name: tls_certificates + rel_url_charmhub: https://charmhub.io/integrations/tls_certificates + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/tls_certificates/v1 + description: Manage TLS certificates. Deprecated in favor of ``charmlibs.interfaces.tls_certificates``. +- name: charms.tempo_k8s.tracing + status: legacy + url: https://charmhub.io/tempo-k8s/libraries/tracing + docs: '' + src: https://github.com/canonical/tempo-coordinator-k8s-operator + kind: Charmhub + rel_name: tracing + rel_url_charmhub: https://charmhub.io/integrations/tracing + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/tracing/v2 + description: Deprecated in favour of ``tempo_coordinator_k8s .tracing``. +- name: charms.tempo_coordinator_k8s.tracing + status: '' + url: https://charmhub.io/tempo-coordinator-k8s/libraries/tracing + docs: '' + src: https://github.com/canonical/tempo-coordinator-k8s-operator + kind: Charmhub + rel_name: tracing + rel_url_charmhub: https://charmhub.io/integrations/tracing + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/tracing/v2 + description: Provide and consume tracing data. +- name: charms.traefik_route_k8s.traefik_route + status: legacy + url: https://charmhub.io/traefik-route-k8s/libraries/traefik_route + docs: '' + src: https://github.com/canonical/traefik-k8s-operator + kind: Charmhub + rel_name: traefik_route + rel_url_charmhub: https://charmhub.io/integrations/traefik_route + rel_url_schema: '' + description: Deprecated in favour of the ``traefik-k8s`` charm's ``traefik_route`` + lib. +- name: charms.traefik_k8s.traefik_route + status: '' + url: https://charmhub.io/traefik-k8s/libraries/traefik_route + docs: '' + src: https://github.com/canonical/traefik-k8s-operator + kind: Charmhub + rel_name: traefik_route + rel_url_charmhub: https://charmhub.io/integrations/traefik_route + rel_url_schema: '' + description: '' +- name: charms.vault_k8s.vault_kv + status: '' + url: https://charmhub.io/vault-k8s/libraries/vault_kv + docs: '' + src: https://github.com/canonical/vault-k8s-operator + kind: Charmhub + rel_name: vault_kv + rel_url_charmhub: https://charmhub.io/integrations/vault_kv + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/vault_kv/v0 + description: '' +- name: litmus-libs + status: '' + url: https://pypi.org/project/litmus-libs/ + docs: '' + src: https://github.com/canonical/litmus-operators + kind: PyPI + rel_name: litmus_auth + rel_url_charmhub: https://charmhub.io/integrations/litmus_auth + rel_url_schema: https://documentation.ubuntu.com/charmlibs/reference/interfaces/litmus_auth/v0/ + description: '' +- name: charmlibs.interfaces.istio_request_auth + status: recommended + url: https://pypi.org/project/charmlibs-interfaces-istio-request-auth + docs: https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio-request-auth + src: https://github.com/canonical/charmlibs/tree/main/interfaces/istio-request-auth + kind: PyPI + rel_name: istio-request-auth + rel_url_charmhub: '' + rel_url_schema: '' + description: Configure Istio RequestAuthentication resources via relation data. +- name: charmed_service_mesh_helpers.interfaces.request_auth + status: legacy + url: https://pypi.org/project/charmed-service-mesh-helpers/ + docs: '' + src: https://github.com/canonical/charmed-service-mesh-helpers + kind: PyPI + rel_name: istio-request-auth + rel_url_charmhub: '' + rel_url_schema: '' + description: Deprecated in favour of charmlibs.interfaces.istio_request_auth. +- name: charmlibs.interfaces.gateway_metadata + status: recommended + url: https://pypi.org/project/charmlibs-interfaces-gateway-metadata + docs: https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/gateway-metadata + src: https://github.com/canonical/charmlibs/tree/main/interfaces/gateway-metadata + kind: PyPI + rel_name: gateway-metadata + rel_url_charmhub: '' + rel_url_schema: '' + description: Share Kubernetes Gateway API workload metadata between charms. +- name: charmed_service_mesh_helpers.interfaces.gateway_metadata + status: legacy + url: https://pypi.org/project/charmed-service-mesh-helpers/ + docs: '' + src: https://github.com/canonical/charmed-service-mesh-helpers + kind: PyPI + rel_name: gateway-metadata + rel_url_charmhub: '' + rel_url_schema: '' + description: Deprecated in favour of ``charmlibs.interfaces.gateway_metadata``. +- name: dpcharmlibs.interfaces + status: '' + url: https://pypi.org/project/dpcharmlibs-interfaces + docs: '' + src: https://github.com/canonical/data-platform-charmlibs + kind: PyPI + rel_name: valkey_client + rel_url_charmhub: https://charmhub.io/integrations/valkey_client + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/valkey_client + description: Database charms from the Data Platform Team recommend using the generic + ``data_platform_libs.data_interfaces`` library to implement the interface. +- name: charmlibs.interfaces.istio_ingress_route + status: recommended + url: https://pypi.org/project/charmlibs-interfaces-istio-ingress-route + docs: https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio-ingress-route + src: https://github.com/canonical/charmlibs/tree/main/interfaces/istio-ingress-route + kind: PyPI + rel_name: istio-ingress-route + rel_url_charmhub: '' + rel_url_schema: '' + description: Advanced ingress routing through the istio-ingress-k8s charm with multi-port + listeners and gRPC support. +- name: charms.istio_ingress_k8s.istio_ingress_route + status: legacy + url: https://charmhub.io/istio-ingress-k8s/libraries/istio_ingress_route + docs: '' + src: https://github.com/canonical/istio-ingress-k8s-operator + kind: Charmhub + rel_name: istio-ingress-route + rel_url_charmhub: '' + rel_url_schema: '' + description: Deprecated in favour of ``charmlibs.interfaces.istio_ingress_route``. diff --git a/.scripts/ls.py b/.scripts/ls.py index 6a25192e9..a14d9d5cc 100755 --- a/.scripts/ls.py +++ b/.scripts/ls.py @@ -32,7 +32,6 @@ import argparse import contextlib -import csv import dataclasses import functools import io @@ -401,18 +400,17 @@ def _get_interface_version(path: pathlib.Path, root: pathlib.Path = _REPO_ROOT) @functools.cache def _lib_urls() -> dict[str, str]: result: dict[str, str] = {} - for p in 'general-libs.csv', 'interface-libs.csv': - with (_REPO_ROOT / '.docs' / 'reference' / p).open() as f: - for row in csv.DictReader(f): - # Library names should be unique, but we currently have an entry for - # charms.data_platform_libs.data_interfaces for each interface it supports - # This doesn't break our lookups though, since they all have the same metadata - # (aside from the interface column) - assert ( - row['name'] not in result - or row['name'] == 'charms.data_platform_libs.data_interfaces' - ) - result[row['name']] = row['url'] + data = yaml.safe_load((_REPO_ROOT / '.docs' / 'reference' / 'libs.yaml').read_text()) + for entry in (*data['general'], *data['interfaces']): + # Library names should be unique, but we currently have an entry for + # charms.data_platform_libs.data_interfaces for each interface it supports + # This doesn't break our lookups though, since they all have the same metadata + # (aside from the interface column) + assert ( + entry['name'] not in result + or entry['name'] == 'charms.data_platform_libs.data_interfaces' + ) + result[entry['name']] = entry['url'] return result From e288f54394a4e029ae0e64bc389ee2c6e54a54bd Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Tue, 2 Jun 2026 18:46:45 +1200 Subject: [PATCH 30/65] feat: support `str | os.PathLike` in `PathProtocol.glob` and `LocalPath.glob` (#505) Python 3.10/3.11/3.12 will hit the new code path; Py 3.13+ will pass through to the now-PathLike-accepting base, which is harmless. Existing `pathops` functional tests in `tests/functional/test_container_path.py::TestGlob` only ever pass `str` patterns. If you want functional/integration coverage of the `LocalPath` side too, I could add a parametrize over `[str, pathlib.PurePosixPath]` on the `test_ok`. I left it since the unit tests already cover the `LocalPath` shim and the protocol signature. The `pyright: ignore[reportIncompatibleMethodOverride]` is needed because pyright 1.1.408's stubs for `pathlib.Path.glob` declare `Generator[Path, None, None]` while `PathProtocol.glob` declares `Iterator[Self]`. Newer typeshed actually uses `Self`, so once the pinned `pyright==1.1.408` in `test-requirements.txt` is bumped the ignore may become unused. I couldn't remember whether we want the specific match in the ignore brackets, but I think we decided we did. The issue mentions `Generator[Self]` as the return; I kept the protocol return as `Iterator[Self]` (unchanged) and the `LocalPath` override as `Iterator[Self]`. In `pathops 1.2.1` both were relaxed to `Iterator` for the same `iterdir`/`glob` pair, so this matches that direction. Fixes #9 --- pathops/CHANGELOG.md | 4 +++ pathops/src/charmlibs/pathops/_local_path.py | 12 +++++++- pathops/src/charmlibs/pathops/_types.py | 7 ++--- pathops/src/charmlibs/pathops/_version.txt | 2 +- pathops/tests/unit/test_local_path.py | 31 +++++++++++++++++--- 5 files changed, 46 insertions(+), 10 deletions(-) diff --git a/pathops/CHANGELOG.md b/pathops/CHANGELOG.md index 1c3b6b3ee..5a11442e9 100644 --- a/pathops/CHANGELOG.md +++ b/pathops/CHANGELOG.md @@ -1,3 +1,7 @@ +# 1.3.0 - 2 June 2026 + +`PathProtocol.glob` and `LocalPath.glob` now accept a `str | os.PathLike[str]` pattern, matching `ContainerPath.glob` and `pathlib.Path.glob` on Python 3.13+. + # 1.2.1 - 6 February 2026 `PathProtocol` `iterdir` and `glob` now only promise to return an `Iterator` rather than a `Generator`. diff --git a/pathops/src/charmlibs/pathops/_local_path.py b/pathops/src/charmlibs/pathops/_local_path.py index 29e80634c..132506717 100644 --- a/pathops/src/charmlibs/pathops/_local_path.py +++ b/pathops/src/charmlibs/pathops/_local_path.py @@ -17,6 +17,7 @@ from __future__ import annotations import grp +import os import pathlib import pwd import shutil @@ -25,7 +26,9 @@ from . import _constants if typing.TYPE_CHECKING: - from typing_extensions import Buffer + from collections.abc import Iterator + + from typing_extensions import Buffer, Self class LocalPath(pathlib.PosixPath): @@ -161,6 +164,13 @@ def write_text( self.chmod(mode) return bytes_written + def glob( # pyright: ignore[reportIncompatibleMethodOverride] + self, pattern: str | os.PathLike[str] + ) -> Iterator[Self]: + # On Python 3.12 and earlier, pathlib.Path.glob only accepts a str pattern. + # ContainerPath.glob accepts str | os.PathLike[str], so we normalise here to match. + return super().glob(os.fspath(pattern)) + def mkdir( self, mode: int = _constants.DEFAULT_MKDIR_MODE, diff --git a/pathops/src/charmlibs/pathops/_types.py b/pathops/src/charmlibs/pathops/_types.py index a65c263dd..530eec798 100644 --- a/pathops/src/charmlibs/pathops/_types.py +++ b/pathops/src/charmlibs/pathops/_types.py @@ -259,9 +259,8 @@ def iterdir(self) -> Iterator[Self]: ... # NOTE: Not supported -- (Python 3.12) ``case_sensitive`` argument - # NOTE: Not supported -- (Python 3.13) ``pattern`` can be path-like # NOTE: Not supported -- (Python 3.13) ``recurse_symlinks`` - def glob(self, pattern: str) -> Iterator[Self]: + def glob(self, pattern: str | os.PathLike[str]) -> Iterator[Self]: r"""Iterate over this directory and yield all paths matching the provided pattern. For example, ``path.glob('*.txt')``, ``path.glob('*/foo.txt')``. @@ -271,8 +270,8 @@ def glob(self, pattern: str) -> Iterator[Self]: :meth:`ContainerPath.glob`. Args: - pattern: Must be relative, meaning it cannot begin with ``'/'``. - Matching is case-sensitive. + pattern: A :class:`str` or :class:`os.PathLike` object. The pattern must be relative, + meaning it cannot begin with ``'/'``. Matching is case-sensitive. Returns: A generator yielding objects of the same type as this path, corresponding to those diff --git a/pathops/src/charmlibs/pathops/_version.txt b/pathops/src/charmlibs/pathops/_version.txt index 6085e9465..f0bb29e76 100644 --- a/pathops/src/charmlibs/pathops/_version.txt +++ b/pathops/src/charmlibs/pathops/_version.txt @@ -1 +1 @@ -1.2.1 +1.3.0 diff --git a/pathops/tests/unit/test_local_path.py b/pathops/tests/unit/test_local_path.py index 300935973..a49477291 100644 --- a/pathops/tests/unit/test_local_path.py +++ b/pathops/tests/unit/test_local_path.py @@ -17,19 +17,16 @@ from __future__ import annotations import grp +import pathlib import pwd import re import shutil -import typing from dataclasses import dataclass import pytest from charmlibs.pathops import LocalPath -if typing.TYPE_CHECKING: - import pathlib - class MockChown: calls: list[tuple[pathlib.Path, str | int | None, str | int | None]] @@ -146,3 +143,29 @@ def test_write_text_newline_value_error(tmp_path: pathlib.Path): path.write_text('', newline='bad') with pytest.raises(ValueError): LocalPath(path).write_text('', newline='bad') + + +class TestGlobPattern: + @pytest.fixture + def populated_dir(self, tmp_path: pathlib.Path) -> pathlib.Path: + (tmp_path / 'a.txt').write_text('') + (tmp_path / 'b.txt').write_text('') + (tmp_path / 'c.md').write_text('') + return tmp_path + + def test_str_pattern(self, populated_dir: pathlib.Path): + result = sorted(p.name for p in LocalPath(populated_dir).glob('*.txt')) + assert result == ['a.txt', 'b.txt'] + + def test_pathlib_pattern(self, populated_dir: pathlib.Path): + pattern = pathlib.PurePosixPath('*.txt') + result = sorted(p.name for p in LocalPath(populated_dir).glob(pattern)) + assert result == ['a.txt', 'b.txt'] + + def test_custom_pathlike_pattern(self, populated_dir: pathlib.Path): + class _Pattern: + def __fspath__(self) -> str: + return '*.md' + + result = sorted(p.name for p in LocalPath(populated_dir).glob(_Pattern())) + assert result == ['c.md'] From 6385bf35eadb0ccf81d62666d9da85366dcb8dad Mon Sep 17 00:00:00 2001 From: James Garner Date: Tue, 2 Jun 2026 19:57:03 +1200 Subject: [PATCH 31/65] docs: populate lib fields for identity interfaces (#506) This PR adds `lib` entries to the interface metadata for some `identity` owned interfaces that were missing them. --- .../forward_auth/interface/v0/interface.yaml | 1 + .../hydra_endpoints/interface/v0/interface.yaml | 1 + interfaces/index.json | 16 ++++++++-------- .../kratos_info/interface/v0/interface.yaml | 1 + .../interface/v0/interface.yaml | 1 + 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/interfaces/forward_auth/interface/v0/interface.yaml b/interfaces/forward_auth/interface/v0/interface.yaml index f04745dbb..962f0a6d2 100644 --- a/interfaces/forward_auth/interface/v0/interface.yaml +++ b/interfaces/forward_auth/interface/v0/interface.yaml @@ -3,6 +3,7 @@ internal: true version: 0 status: draft +lib: charms.oathkeeper.forward_auth providers: - name: oathkeeper-operator diff --git a/interfaces/hydra_endpoints/interface/v0/interface.yaml b/interfaces/hydra_endpoints/interface/v0/interface.yaml index 7334b4f15..b0fdfd76a 100644 --- a/interfaces/hydra_endpoints/interface/v0/interface.yaml +++ b/interfaces/hydra_endpoints/interface/v0/interface.yaml @@ -3,6 +3,7 @@ internal: true version: 0 status: draft +lib: charms.hydra.hydra_endpoints providers: - name: hydra-operator diff --git a/interfaces/index.json b/interfaces/index.json index 9125f829d..43d0dc142 100644 --- a/interfaces/index.json +++ b/interfaces/index.json @@ -192,8 +192,8 @@ { "name": "forward_auth", "version": "0", - "lib": "", - "lib_url": "", + "lib": "charms.oathkeeper.forward_auth", + "lib_url": "https://charmhub.io/oathkeeper/libraries/forward_auth", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/forward_auth/", "summary": "", "description": "", @@ -232,8 +232,8 @@ { "name": "hydra_endpoints", "version": "0", - "lib": "", - "lib_url": "", + "lib": "charms.hydra.hydra_endpoints", + "lib_url": "https://charmhub.io/hydra/libraries/hydra_endpoints", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/hydra_endpoints/", "summary": "", "description": "", @@ -332,8 +332,8 @@ { "name": "kratos_info", "version": "0", - "lib": "", - "lib_url": "", + "lib": "charms.kratos.kratos_info", + "lib_url": "https://charmhub.io/kratos/libraries/kratos_info", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/kratos_info/", "summary": "", "description": "", @@ -372,8 +372,8 @@ { "name": "login_ui_endpoints", "version": "0", - "lib": "", - "lib_url": "", + "lib": "charms.identity_platform_login_ui_operator.login_ui_endpoints", + "lib_url": "https://charmhub.io/identity-platform-login-ui-operator/libraries/login_ui_endpoints", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/login_ui_endpoints/", "summary": "", "description": "", diff --git a/interfaces/kratos_info/interface/v0/interface.yaml b/interfaces/kratos_info/interface/v0/interface.yaml index ffd007d9b..709915906 100644 --- a/interfaces/kratos_info/interface/v0/interface.yaml +++ b/interfaces/kratos_info/interface/v0/interface.yaml @@ -3,6 +3,7 @@ internal: true version: 0 status: published +lib: charms.kratos.kratos_info providers: - name: kratos-operator diff --git a/interfaces/login_ui_endpoints/interface/v0/interface.yaml b/interfaces/login_ui_endpoints/interface/v0/interface.yaml index f71b60964..6639c96d9 100644 --- a/interfaces/login_ui_endpoints/interface/v0/interface.yaml +++ b/interfaces/login_ui_endpoints/interface/v0/interface.yaml @@ -3,6 +3,7 @@ internal: true version: 0 status: draft +lib: charms.identity_platform_login_ui_operator.login_ui_endpoints providers: - name: identity-platform-login-ui-operator From 36eec01b1ec086f4e03a17e66c2d9492e9fd5b11 Mon Sep 17 00:00:00 2001 From: James Garner Date: Wed, 3 Jun 2026 16:27:51 +1200 Subject: [PATCH 32/65] feat: library tags (#513) This PR adds tags to the metadata stored for each known charm library. The tags are exported in `interfaces/index.json` for consumption by Charmhub -- that's the key deliverable of this PR. I've also updated the library index tables to display these tags, as a key reason they were developed is to aid in library discoverability. The contribution guides have been updated to cover library metadata. The tags introduced in this PR are the result of a clustering study conducted by Design. The actual tagging of specific libraries should be considered provisional, and open to updating with low friction in future. Future plans: I think in future we may want all this library metadata to live in the library directory, with libs.yaml existing only as a fallback for libraries that don't live in this monorepo. Resolves #501 -- see the issue description for relevant links. ## Changes ### Data: tag definitions and library tagging `tags.yaml` defines two tag categories: **domain-tags** (`#data`, `#identity-auth`, `#ingress`, `#networking`, `#observability`, `#operations`, `#security`, `#substrate`, `#telco`) and **audience-tags** (`#internal`). Each tag has a description and criteria for when to apply it. Every entry in `libs.yaml` gains a `tags: []` field, populated with the applicable tags. ### Data: interface index `ls.py` gains a `tags` output field (read from `libs.yaml`), and the `interfaces-json` justfile recipe includes it. `interfaces/index.json` is regenerated with tags included. ### Docs: tags in the library tables Tags appear at the end of each library's description cell as clickable links (e.g. `#security`). Hovering shows the tag's description; clicking filters the table to that tag. A collapsible "Tags key" dropdown (below the existing status key) lists all tags present on the page with descriptions. Omitted if no libraries on the page have tags. ### Docs: contributing The tutorial and migration 'how to' have been updated to cover `libs.yaml` and `tags.yaml`. --- .docs/.sphinx/_static/project_specific.css | 45 +++ .docs/.sphinx/_static/tag_click_search.js | 19 ++ .docs/conf.py | 4 +- .docs/extensions/generate_tables.py | 151 +++++++-- .docs/extensions/test_generate_tables.py | 80 +++++ .docs/how-to/migrate.md | 11 + .docs/reference/general-libs.md | 15 + .docs/reference/interface-libs.md | 15 + .docs/reference/libs.yaml | 339 ++++++++++++++++++++- .docs/reference/tags.yaml | 88 ++++++ .docs/tutorial.md | 8 + .scripts/ls.py | 37 ++- interfaces/index.json | 166 ++++++++++ justfile | 1 + 14 files changed, 945 insertions(+), 34 deletions(-) create mode 100644 .docs/.sphinx/_static/tag_click_search.js create mode 100644 .docs/reference/tags.yaml diff --git a/.docs/.sphinx/_static/project_specific.css b/.docs/.sphinx/_static/project_specific.css index 9778532fe..fc80679b4 100644 --- a/.docs/.sphinx/_static/project_specific.css +++ b/.docs/.sphinx/_static/project_specific.css @@ -31,3 +31,48 @@ display: block; z-index: 9999; } + +/** + * Show tag-tooltip as floating text when hovering over tag-div. + */ +.tag-group { + display: inline-flex; + flex-wrap: wrap; + gap: 0.5em; +} +.tag-div { + position: relative; + display: inline-block; + cursor: pointer; +} +.tag-div:visited, +.tag-div:visited:hover { + color: var(--color-link); +} +.tag-tooltip { + display: none; + position: absolute; + top: 100%; + right: 0; + background-color: #EBEBEB; + color: #000000; + padding: 5px; + border-radius: 5px; + width: max-content; + max-width: 20em; +} +.tag-div:hover .tag-tooltip { + display: block; + z-index: 9999; +} + +/** + * Style interface names as subtle chips. + */ +td .chip { + background-color: #F2F1F0; + border: 1px solid #C6C1BB; + border-radius: 2px; + padding: 0.3em 0.6em; + white-space: nowrap; +} diff --git a/.docs/.sphinx/_static/tag_click_search.js b/.docs/.sphinx/_static/tag_click_search.js new file mode 100644 index 000000000..c18f9ae20 --- /dev/null +++ b/.docs/.sphinx/_static/tag_click_search.js @@ -0,0 +1,19 @@ +// Click a tag (e.g. #security) to search the DataTable for that tag. +$(document).ready(function () { + $(document).on("click", ".tag-div, .chip > a[href='#']", function (e) { + e.preventDefault(); + var tag = $(this).clone().children().remove().end().text().trim(); + var table = $(this).closest("table.dataTable"); + if (!table.length) { + // Clicked from outside the table (e.g. the dropdown key). + table = $("table.dataTable"); + } + if (table.length) { + var dt = table.DataTable(); + dt.search(tag).draw(); + // Also update the visible search input + var wrapper = $(table).closest(".dataTables_wrapper"); + wrapper.find("input[type='search']").val(tag); + } + }); +}); diff --git a/.docs/conf.py b/.docs/conf.py index a79f24689..250994cd2 100644 --- a/.docs/conf.py +++ b/.docs/conf.py @@ -170,7 +170,9 @@ ] # Adds custom JavaScript files, located under 'html_static_path' -# html_js_files = [] +html_js_files = [ + "tag_click_search.js", +] # Specifies a reST snippet to be prepended to each .rst file # This defines a :center: role that centers table cell content. diff --git a/.docs/extensions/generate_tables.py b/.docs/extensions/generate_tables.py index 3c10991ea..99876b6c9 100644 --- a/.docs/extensions/generate_tables.py +++ b/.docs/extensions/generate_tables.py @@ -60,6 +60,10 @@ def _generate(app: sphinx.application.Sphinx): 'machine': '🖥️', 'K8s': '☸️', } +_SUBSTRATE_TOOLTIPS = { + 'machine': 'Compatible with machine charms.', + 'K8s': 'Compatible with Kubernetes charms.', +} _STATUS_TOOLTIPS = { 'recommended': 'Recommended for use in new charms today!', 'dep': 'Dependency of other libs, unlikely to be required directly.', @@ -116,6 +120,21 @@ def _generate(app: sphinx.application.Sphinx): _KEY_DROPDOWN_HEADER = f""".. dropdown:: {_KEY_MSG} """ +_TAGS_KEY_TABLE_HEADER = """.. list-table:: + :widths: 1, 100 + :header-rows: 1 + + * - Tag + - Description +""" + + +class _TagInfo(typing.TypedDict): + description: str + criteria: str + + +_TagsYaml = dict[str, dict[str, _TagInfo]] class _LibEntry(typing.TypedDict, total=True): @@ -126,6 +145,7 @@ class _LibEntry(typing.TypedDict, total=True): src: str kind: str description: str + tags: list[str] class _InterfaceLibEntry(_LibEntry, total=True): @@ -156,11 +176,12 @@ def _generate_libs_tables(docs_dir: str | pathlib.Path) -> None: generated_dir = reference_dir / 'generated' generated_dir.mkdir(exist_ok=True) data: _LibsYaml = yaml.safe_load((reference_dir / 'libs.yaml').read_text()) + tag_descriptions = _load_tag_descriptions(reference_dir) interface_entries = data['interfaces'] general_entries = data['general'] _write_if_needed( path=(generated_dir / 'interface-libs-table.rst'), - content=_get_interface_libs_table(interface_entries), + content=_get_interface_libs_table(interface_entries, tag_descriptions), ) _write_if_needed( path=(generated_dir / 'interface-libs-status-key-table.rst'), @@ -168,12 +189,24 @@ def _generate_libs_tables(docs_dir: str | pathlib.Path) -> None: ) _write_if_needed( path=(generated_dir / 'general-libs-table.rst'), - content=_get_general_libs_table(general_entries), + content=_get_general_libs_table(general_entries, tag_descriptions), ) _write_if_needed( path=(generated_dir / 'general-libs-status-key-table.rst'), content=_get_status_key_table_dropdown(general_entries), ) + _write_if_needed( + path=(generated_dir / 'interface-libs-tags-key-table.rst'), + content=_get_tags_key_table_dropdown( + interface_entries, tag_descriptions, column='interface' + ), + ) + _write_if_needed( + path=(generated_dir / 'general-libs-tags-key-table.rst'), + content=_get_tags_key_table_dropdown( + general_entries, tag_descriptions, column='description' + ), + ) def _write_if_needed(path: pathlib.Path, content: str) -> None: @@ -187,30 +220,56 @@ def _write_if_needed(path: pathlib.Path, content: str) -> None: path.write_text(to_write) +def _load_tag_descriptions(reference_dir: pathlib.Path) -> dict[str, str]: + """Load tags.yaml and return a flat mapping of tag name to description.""" + tags_data: _TagsYaml = yaml.safe_load((reference_dir / 'tags.yaml').read_text()) + tag_descriptions: dict[str, str] = {} + for _category_key in ('domain-tags', 'audience-tags'): + for tag_name, tag_info in tags_data.get(_category_key, {}).items(): + tag_descriptions[tag_name] = tag_info['description'] + return tag_descriptions + + ########## # tables # ########## -def _get_interface_libs_table(entries: Iterable[_InterfaceLibEntry]) -> str: +def _get_interface_libs_table( + entries: Iterable[_InterfaceLibEntry], + tag_descriptions: dict[str, str], +) -> str: def key(row: tuple[str, ...]) -> tuple[str, ...]: status, _name, _kind, desc = row return status, desc rows = [ - (_status(entry), _name(entry), _kind(entry), _interface_description(entry)) + ( + _status(entry), + _name(entry), + _kind(entry), + _interface_description(entry, tag_descriptions), + ) for entry in entries if _is_listed(entry) ] return _INTERFACE_LIBS_TABLE_HEADER + _rst_rows(sorted(rows, key=key)) -def _get_general_libs_table(entries: Iterable[_GeneralLibEntry]) -> str: +def _get_general_libs_table( + entries: Iterable[_GeneralLibEntry], + tag_descriptions: dict[str, str], +) -> str: def key(row: _TableRow) -> tuple[str, ...]: return row.status, row.kind, row.name, row.description rows = [ - _TableRow(_status(entry), _name(entry), _kind(entry), _general_description(entry)) + _TableRow( + _status(entry), + _name(entry), + _kind(entry), + _general_description(entry, tag_descriptions), + ) for entry in entries if _is_listed(entry) ] @@ -229,6 +288,34 @@ def _get_status_key_table_dropdown(entries: Iterable[_LibEntry]) -> str: return _KEY_DROPDOWN_HEADER + _indent_lines(table, level=3) +def _get_tags_key_table_dropdown( + entries: Iterable[_LibEntry], + tag_descriptions: dict[str, str], + *, + column: str, +) -> str: + used_tags: set[str] = set() + for entry in entries: + if _is_listed(entry): + used_tags.update(entry['tags']) + if not used_tags: + return '' + rows = [ + ( + _rst_table_indent(_rst_raw_html(_html_tag_tooltip(f'#{tag}', None))), + tag_descriptions.get(tag, ''), + ) + for tag in sorted(used_tags) + ] + table = _TAGS_KEY_TABLE_HEADER + _rst_rows(rows) + return _tags_key_dropdown_header(column) + _indent_lines(table, level=3) + + +def _tags_key_dropdown_header(column: str) -> str: + msg = f'Tags are shown in the {column} column. See tooltips, or click here for a key.' + return f'.. dropdown:: {msg}\n\n' + + def _is_listed(entry: _LibEntry) -> bool: return entry['status'] != 'unlisted' @@ -263,19 +350,23 @@ def _kind(entry: _LibEntry) -> str: return _rst_table_indent('\n'.join(content)) -def _interface_description(entry: _InterfaceLibEntry) -> str: +def _interface_description( + entry: _InterfaceLibEntry, + tag_descriptions: dict[str, str], +) -> str: sortkeys = [ entry['rel_name'].ljust(64, 'z'), str(_STATUS_SORTKEYS[entry['status']]), entry['name'], str(_KIND_SORTKEYS[entry['kind']]), ] - html_lines = [_html_hidden_span(''.join(sortkeys))] + content = [_rst_raw_html(_html_hidden_span(''.join(sortkeys)))] if rel_links := _rel_links(entry): - html_lines.append(rel_links) - content = [_rst_raw_html('\n'.join(html_lines))] + content.append(_rst_raw_html(f'

{rel_links}

')) if desc := entry['description']: content.append(_rst_lines(desc)) + if tags := entry['tags']: + content.append(_tags_rst(tags, tag_descriptions)) return _rst_table_indent('\n'.join(content)) @@ -283,15 +374,18 @@ def _rel_links(entry: _InterfaceLibEntry) -> str: if not (name := entry['rel_name']): return '' if not (main_url := entry['rel_url_charmhub']): - return _html_no_spellcheck_span(name) + return f'{name}' main_link = _html_link(name, main_url) if not (schema_url := entry['rel_url_schema']): - return main_link + return f'{main_link}' schema_link = _html_link('schema', schema_url) - return f'{main_link} ({schema_link})' + return f'{main_link} ({schema_link})' -def _general_description(entry: _GeneralLibEntry) -> str: +def _general_description( + entry: _GeneralLibEntry, + tag_descriptions: dict[str, str], +) -> str: substrates = ('machine', 'K8s') sortkeys = [ *('0' if entry[s] else '1' for s in substrates), @@ -300,13 +394,28 @@ def _general_description(entry: _GeneralLibEntry) -> str: str(_KIND_SORTKEYS[entry['kind']]), ] content = [_rst_raw_html(_html_hidden_span(''.join(sortkeys)))] - if firstline := ' '.join(_EMOJIS.get(s, '') + s for s in substrates if entry[s]): - content.append(_rst_lines(firstline)) if desc := entry['description']: content.append(_rst_lines(desc)) + substrate_parts = [ + _html_tag_tooltip(f'{_EMOJIS.get(s, "")}{s}', _SUBSTRATE_TOOLTIPS.get(s)) + for s in substrates + if entry[s] + ] + tag_parts = [_html_tag_tooltip(f'#{t}', tag_descriptions.get(t)) for t in entry['tags']] + if substrate_parts or tag_parts: + joined = ' '.join(substrate_parts + tag_parts) + content.append(_rst_raw_html(f'{joined}')) return _rst_table_indent('\n'.join(content)) +def _tags_rst(tags: list[str], tag_descriptions: dict[str, str]) -> str: + """Return RST raw HTML for a tags line, or empty string if no tags.""" + assert tags + tag_htmls = [_html_tag_tooltip(f'#{t}', tag_descriptions.get(t)) for t in tags] + joined = ' '.join(tag_htmls) + return _rst_raw_html(f'{joined}') + + ####### # rst # ####### @@ -369,7 +478,11 @@ def _html_link(text: str, url: str) -> str: return f'{text}' -def _html_no_spellcheck_span(text: object) -> str: - e = ElementTree.Element('span', attrib={'class': 'no-spellcheck'}) - e.text = str(text) +def _html_tag_tooltip(tag_text: str, tooltip: str | None) -> str: + e = ElementTree.Element('a', attrib={'class': 'tag-div no-spellcheck', 'href': '#'}) + e.text = tag_text + if tooltip is not None: + child = ElementTree.Element('span', attrib={'class': 'tag-tooltip'}) + child.text = tooltip + e.append(child) return ElementTree.tostring(e, encoding='unicode') diff --git a/.docs/extensions/test_generate_tables.py b/.docs/extensions/test_generate_tables.py index 730cee056..acc9ae7eb 100644 --- a/.docs/extensions/test_generate_tables.py +++ b/.docs/extensions/test_generate_tables.py @@ -40,6 +40,65 @@ def test_status_key_table(): assert table is not None +def test_tags_key_table_with_tags(): + entries: list[generate_tables._LibEntry] = [ + { + 'name': 'lib1', + 'status': '', + 'url': '', + 'docs': '', + 'src': '', + 'kind': '', + 'description': '', + 'tags': ['security', 'data'], + }, + { + 'name': 'lib2', + 'status': '', + 'url': '', + 'docs': '', + 'src': '', + 'kind': '', + 'description': '', + 'tags': ['security'], + }, + ] + tag_descriptions = {'security': 'Security stuff', 'data': 'Data stuff'} + rst = generate_tables._get_tags_key_table_dropdown( + entries, tag_descriptions, column='description' + ) + assert rst # non-empty + dropdown_contents = '\n'.join(rst.split('\n')[1:]) + html_content = rst_to_html(dropdown_contents) + table = ElementTree.fromstring(html.unescape(html_content)).find('.//table') + assert table is not None + rows = table.findall('.//tr') + # header + 2 unique tags (data, security) + assert len(rows) == 3 + # Tags in the key should be clickable elements + tag_links = table.findall('.//a[@class="tag-div no-spellcheck"]') + assert len(tag_links) == 2 + tag_texts = sorted(link.text or '' for link in tag_links) + assert tag_texts == ['#data', '#security'] + + +def test_tags_key_table_no_tags(): + entries: list[generate_tables._LibEntry] = [ + { + 'name': 'lib1', + 'status': '', + 'url': '', + 'docs': '', + 'src': '', + 'kind': '', + 'description': '', + 'tags': [], + }, + ] + rst = generate_tables._get_tags_key_table_dropdown(entries, {}, column='description') + assert rst == '' + + @pytest.mark.parametrize('rows', ([('r1c1', 'r1c2'), ('r2c1', 'r2c2')],)) def test_rst_rows(rows: list[tuple[str, ...]]): rst = generate_tables._rst_rows(rows) @@ -80,3 +139,24 @@ def test_html_link(text: str, url: str): a = ElementTree.fromstring(html_content) assert a.attrib['href'] == url assert a.text == text + + +@pytest.mark.parametrize( + ('tag_text', 'tooltip'), + [('#security', 'TLS certificate management'), ('#data', None)], +) +def test_html_tag_tooltip(tag_text: str, tooltip: str | None): + html_content = generate_tables._html_tag_tooltip(tag_text, tooltip) + a = ElementTree.fromstring(html_content) + assert a.tag == 'a' + assert a.attrib['href'] == '#' + assert 'tag-div' in a.attrib['class'] + assert 'no-spellcheck' in a.attrib['class'] + assert a.text == tag_text + children = list(a) + if tooltip is not None: + assert len(children) == 1 + assert 'tag-tooltip' in children[0].attrib['class'] + assert children[0].text == tooltip + else: + assert len(children) == 0 diff --git a/.docs/how-to/migrate.md b/.docs/how-to/migrate.md index 3df154cdb..cdd2dfb43 100644 --- a/.docs/how-to/migrate.md +++ b/.docs/how-to/migrate.md @@ -279,6 +279,17 @@ docs └── tutorial.{md,rst} ``` +## Update the library metadata + +The {ref}`interface library listing ` and {ref}`general library listing ` are generated from `.docs/reference/libs.yaml`. + +There should already be an entry for the old Charmhub-hosted library. +If there isn't, add one with a `deprecated` status and a description noting it has been superseded by the new `charmlibs` package. + +Add a new entry for the `charmlibs` package with the new package name, source URL, status, description, and any relevant tags. +See `.docs/reference/tags.yaml` for the full list of available tags and their assignment criteria. +Don't invent new tags, only use tags defined in `tags.yaml`. + ## Deprecate the old library When migrating an existing Charmhub-hosted library, our recommendation is to do a bug-for-bug migration of the latest release. diff --git a/.docs/reference/general-libs.md b/.docs/reference/general-libs.md index 0c14f453a..d9d674ebb 100644 --- a/.docs/reference/general-libs.md +++ b/.docs/reference/general-libs.md @@ -1,3 +1,14 @@ +--- +hide-toc: true +--- + +```{raw} html + + +``` + (general-libs-listing)= # General libraries @@ -7,6 +18,10 @@ This page lists [non-interface libraries](#charm-libs-general). The search box s .. include:: generated/general-libs-status-key-table.rst ``` +```{eval-rst} +.. include:: generated/general-libs-tags-key-table.rst +``` + ```{eval-rst} .. include:: generated/general-libs-table.rst ``` diff --git a/.docs/reference/interface-libs.md b/.docs/reference/interface-libs.md index 99e1f1c7f..c972a6605 100644 --- a/.docs/reference/interface-libs.md +++ b/.docs/reference/interface-libs.md @@ -1,3 +1,14 @@ +--- +hide-toc: true +--- + +```{raw} html + + +``` + (interface-libs-listing)= # Interface libraries @@ -9,6 +20,10 @@ In future, this page will show which libraries are officially recommended based .. include:: generated/interface-libs-status-key-table.rst ``` +```{eval-rst} +.. include:: generated/interface-libs-tags-key-table.rst +``` + ```{eval-rst} .. include:: generated/interface-libs-table.rst ``` diff --git a/.docs/reference/libs.yaml b/.docs/reference/libs.yaml index 0ac36dd6c..a81496922 100644 --- a/.docs/reference/libs.yaml +++ b/.docs/reference/libs.yaml @@ -9,6 +9,8 @@ general: K8s: false description: Use ``apt`` to install and manage packages. Deprecated in favor of ``charmlibs.apt``. + tags: + - substrate - name: charms.mysql.architecture status: team url: https://charmhub.io/mysql/libraries/architecture @@ -18,6 +20,9 @@ general: machine: true K8s: true description: Shared code between the ``mysql`` and ``mysql-k8s`` charms. + tags: + - internal + - data - name: charms.mysql.async_replication status: team url: https://charmhub.io/mysql/libraries/async_replication @@ -27,6 +32,9 @@ general: machine: true K8s: true description: Shared code between the ``mysql`` and ``mysql-k8s`` charms. + tags: + - internal + - data - name: awsjuju status: unlisted url: https://pypi.org/project/awsjuju/ @@ -36,6 +44,7 @@ general: machine: false K8s: false description: Legacy library for managing AWS resources. + tags: [] - name: charms.mysql.backups status: team url: https://charmhub.io/mysql/libraries/backups @@ -45,6 +54,9 @@ general: machine: true K8s: true description: Shared code between the ``mysql`` and ``mysql-k8s`` charms. + tags: + - internal + - data - name: charms.harness_extensions.capture_events status: legacy url: https://charmhub.io/harness-extensions/libraries/capture_events @@ -55,6 +67,8 @@ general: K8s: true description: Helper for legacy harness tests. New charms should write state-transition tests with ``ops[testing]`` instead. + tags: + - operations - name: charms.observability_libs.cert_handler status: '' url: https://charmhub.io/observability-libs/libraries/cert_handler @@ -65,6 +79,8 @@ general: K8s: true description: Wraps the requirer side of the ``tls-certificates-interface`` charm's ``tls_certificates`` lib. + tags: + - security - name: charms.loki_k8s.charm_logging status: recommended url: https://charmhub.io/loki-k8s/libraries/charm_logging @@ -74,6 +90,8 @@ general: machine: true K8s: true description: Add charm code logging to logs sent via ``loki_push_api``. + tags: + - observability - name: charms.tempo_coordinator_k8s.charm_tracing status: legacy url: https://charmhub.io/tempo-coordinator-k8s/libraries/charm_tracing @@ -84,6 +102,8 @@ general: K8s: true description: Provided by the ``tempo-coordinator-k8s`` charm. Consider using ``ops[tracing]`` instead. + tags: + - observability - name: charms.tempo_k8s.charm_tracing status: legacy url: https://charmhub.io/tempo-k8s/libraries/charm_tracing @@ -94,6 +114,8 @@ general: K8s: true description: Deprecated in favor of the ``tempo-coordinator-k8s`` charm's libs. New charms should use ``ops[tracing]`` for tracing charm code instead. + tags: + - observability - name: charm-api status: experimental url: https://pypi.org/project/charm-api/ @@ -103,6 +125,8 @@ general: machine: true K8s: true description: Experimental API for writing charms. + tags: + - operations - name: charm-helpers status: legacy url: https://pypi.org/project/charmhelpers/ @@ -112,6 +136,8 @@ general: machine: true K8s: true description: Pre-Ops library used by reactive charms. + tags: + - operations - name: charm-json status: legacy url: https://pypi.org/project/charm-json/ @@ -122,6 +148,8 @@ general: K8s: true description: JSON typed relation data – unnecessary with Ops 2.23+ due to the addition of typed relation data. + tags: + - operations - name: charm-refresh status: recommended url: https://pypi.org/project/charm-refresh/ @@ -131,6 +159,8 @@ general: machine: true K8s: true description: In-place rolling refreshes of stateful charmed applications. + tags: + - operations - name: charm-refresh-build-version status: unlisted url: https://pypi.org/project/charm-refresh-build-version/ @@ -140,6 +170,7 @@ general: machine: true K8s: true description: Workaround for a Charmcraft issue. + tags: [] - name: charm-upgrade status: unlisted url: https://pypi.org/project/charm-upgrade/ @@ -149,6 +180,8 @@ general: machine: true K8s: true description: Alias for charm-refresh? (Repo URL redirects.) + tags: + - operations - name: charmed-kubeflow-chisme status: team url: https://pypi.org/project/charmed-kubeflow-chisme/ @@ -158,6 +191,8 @@ general: machine: true K8s: true description: Used internally by the Charmed Kubeflow team. + tags: + - internal - name: charmed-service-mesh-helpers status: team url: https://pypi.org/project/charmed-service-mesh-helpers/ @@ -167,6 +202,9 @@ general: machine: false K8s: true description: Used internally by the Service Mesh team. + tags: + - internal + - networking - name: charmlibs.apt status: recommended url: https://pypi.org/project/charmlibs-apt @@ -176,6 +214,8 @@ general: machine: true K8s: false description: Use ``apt`` to install and manage packages. + tags: + - substrate - name: charmlibs.passwd status: recommended url: https://pypi.org/project/charmlibs-passwd @@ -185,6 +225,8 @@ general: machine: true K8s: false description: Manage Linux users and groups. + tags: + - substrate - name: charmlibs.pathops status: recommended url: https://pypi.org/project/charmlibs-pathops @@ -194,6 +236,8 @@ general: machine: true K8s: true description: Substrate agnostic file operations. + tags: + - substrate - name: charmlibs.rollingops status: recommended url: https://pypi.org/project/charmlibs-rollingops @@ -204,6 +248,8 @@ general: K8s: true description: Coordinate rolling operations for charms, including across applications or clusters. + tags: + - operations - name: charmlibs.snap status: recommended url: https://pypi.org/project/charmlibs-snap @@ -213,6 +259,8 @@ general: machine: true K8s: false description: Use ``snapd`` to install and manage packages. + tags: + - substrate - name: charmlibs.sysctl status: recommended url: https://pypi.org/project/charmlibs-sysctl @@ -222,6 +270,8 @@ general: machine: true K8s: false description: Create and configure ``sysctl`` options. + tags: + - substrate - name: charmlibs.systemd status: recommended url: https://pypi.org/project/charmlibs-systemd @@ -231,6 +281,8 @@ general: machine: true K8s: false description: Use ``systemd`` to start, stop, and manage system services. + tags: + - substrate - name: charms.contextual_status status: experimental url: https://pypi.org/project/charms.contextual-status/ @@ -240,6 +292,8 @@ general: machine: true K8s: true description: Context manager based library for setting charm statuses. + tags: + - operations - name: charms.docker status: legacy url: https://pypi.org/project/charms.docker/ @@ -249,6 +303,8 @@ general: machine: false K8s: false description: Legacy library used by the ``docker-layer`` reactive charm. + tags: + - substrate - name: charms.proxylib status: '' url: https://github.com/canonical/charms.proxylib @@ -259,6 +315,8 @@ general: K8s: true description: A library that helps charms direct HTTP requests and subprocess calls through the model-configured proxy environment. + tags: + - networking - name: charms.reactive status: legacy url: https://pypi.org/project/charms.reactive/ @@ -268,6 +326,8 @@ general: machine: true K8s: true description: Legacy library used to implement reactive charms. + tags: + - operations - name: charms.reconciler status: experimental url: https://pypi.org/project/charms.reconciler/ @@ -277,6 +337,8 @@ general: machine: true K8s: true description: Handle all Juju events in an Ops-based charm with a single method. + tags: + - operations - name: charms.templating.jinja2 status: legacy url: https://pypi.org/project/charms.templating.jinja2/ @@ -286,6 +348,8 @@ general: machine: false K8s: false description: Legacy library for Jinja templating in reactive charms. + tags: + - operations - name: charms.zookeeper.client status: '' url: https://charmhub.io/zookeeper/libraries/client @@ -295,6 +359,8 @@ general: machine: true K8s: true description: Perform ``zookeeper`` operations. + tags: + - data - name: coordinated-workers status: team url: https://pypi.org/project/coordinated-workers/ @@ -305,6 +371,9 @@ general: K8s: true description: Abstractions for charms following the coordinator-worker pattern, used by Observability Team. + tags: + - internal + - observability - name: cosl status: team url: https://pypi.org/project/cosl/ @@ -315,6 +384,9 @@ general: K8s: true description: Used internally by the Observability Charm Engineering Team. A dependency (via ``PYDEPS``) of popular charm libs such as ``loki_push_api``. + tags: + - internal + - observability - name: charms.data_platform_libs.data_models status: legacy url: https://charmhub.io/data-platform-libs/libraries/data_models @@ -325,6 +397,8 @@ general: K8s: true description: '``pydantic``-based typed relation data – unnecessary with Ops 2.23+ due to the addition of typed relation data.' + tags: + - data - name: charms.data_platform_libs.data_secrets status: legacy url: https://charmhub.io/data-platform-libs/libraries/data_secrets @@ -335,6 +409,9 @@ general: K8s: true description: Secrets-related helpers for interfaces. Deprecated in favor of the ``data-platform-libs.data_inferfaces`` interface lib. + tags: + - data + - security - name: data-platform-helpers status: team url: https://pypi.org/project/data-platform-helpers/ @@ -344,6 +421,9 @@ general: machine: true K8s: true description: Used internally by the Data Charm Engineering team. + tags: + - internal + - data - name: charms.operator_libs_linux.dnf status: legacy url: https://charmhub.io/operator-libs-linux/libraries/dnf @@ -354,6 +434,8 @@ general: K8s: false description: Use ``dnf`` to install and manage packages – modern charms all run on Ubuntu, so this shouldn't be needed. + tags: + - substrate - name: charms.harness_extensions.evt_sequences status: legacy url: https://charmhub.io/harness-extensions/libraries/evt_sequences @@ -364,6 +446,8 @@ general: K8s: true description: Helper for legacy harness tests. New charms should write state-transition tests with ``ops[testing]`` instead. + tags: + - operations - name: charms.operator_libs_linux.grub status: legacy url: https://charmhub.io/operator-libs-linux/libraries/grub @@ -375,6 +459,8 @@ general: description: Use GRUB to manage kernel configuration. This library has fallen out of use and is no longer actively maintained. Contact Charm Tech if you have a need for this functionality in your charm. + tags: + - substrate - name: charms.opensearch.helper_cos status: team url: https://charmhub.io/opensearch/libraries/helper_cos @@ -384,6 +470,9 @@ general: machine: false K8s: false description: Relies on ``data_platform_helpers``. + tags: + - internal + - observability - name: hpc-libs status: team url: https://github.com/charmed-hpc/hpc-libs @@ -393,6 +482,8 @@ general: machine: true K8s: true description: Used internally by HPC charms. + tags: + - internal - name: charms.operator_libs_linux.juju_systemd_notices status: legacy url: https://charmhub.io/operator-libs-linux/libraries/juju_systemd_notices @@ -404,6 +495,8 @@ general: description: Use ``systemd`` to observe and emit notices when services change state. This library has fallen out of use and is no longer actively maintained. Contact Charm Tech if you have a need for this functionality in your charm. + tags: + - substrate - name: charms.observability_libs.juju_topology status: legacy url: https://charmhub.io/observability-libs/libraries/juju_topology @@ -413,6 +506,8 @@ general: machine: true K8s: true description: Deprecated in favor of ``cosl.juju_topology.JujuTopology``. + tags: + - observability - name: jujubigdata status: legacy url: https://pypi.org/project/jujubigdata/ @@ -422,6 +517,8 @@ general: machine: false K8s: false description: Legacy library for developing Big Data charms. + tags: + - data - name: jujuresources status: legacy url: https://pypi.org/project/jujuresources/ @@ -432,6 +529,8 @@ general: K8s: true description: Legacy library for loading binary resources. New charms should use ``ops.Model.resources``. + tags: + - operations - name: charms.observability_libs.kubernetes_compute_resources_patch status: '' url: https://charmhub.io/observability-libs/libraries/kubernetes_compute_resources_patch @@ -441,6 +540,8 @@ general: machine: false K8s: true description: Patch Kubernetes compute resource limits. + tags: + - substrate - name: charms.observability_libs.kubernetes_service_patch status: legacy url: https://charmhub.io/observability-libs/libraries/kubernetes_service_patch @@ -450,6 +551,8 @@ general: machine: false K8s: true description: Deprecated in favor of ``ops.Unit.set_ports``. + tags: + - substrate - name: charms.observability_libs.metrics_endpoint_discovery status: '' url: https://charmhub.io/observability-libs/libraries/metrics_endpoint_discovery @@ -460,6 +563,8 @@ general: K8s: true description: Discover metric endpoints exposed by applications deployed to a K8s cluster. + tags: + - observability - name: mongo-charms-single-kernel status: team url: https://pypi.org/project/mongo-charms-single-kernel/ @@ -469,6 +574,9 @@ general: machine: true K8s: true description: Used internally by the Data Charm Engineering team. + tags: + - internal + - data - name: charms.kubernetes_charm_libraries.multus status: '' url: https://charmhub.io/kubernetes-charm-libraries/libraries/multus @@ -478,6 +586,8 @@ general: machine: false K8s: true description: Use the Multus Kubernetes Container Network Interface. + tags: + - networking - name: charms.mysql.mysql status: team url: https://charmhub.io/mysql/libraries/mysql @@ -487,6 +597,9 @@ general: machine: true K8s: true description: Shared code between the ``mysql`` and ``mysql-k8s`` charms. + tags: + - internal + - data - name: oci-image status: legacy url: https://pypi.org/project/oci-image/ @@ -497,6 +610,8 @@ general: K8s: true description: Work with OCI images in podspec charms. New Kubernetes charms should use the sidecar pattern. Repo archived in March 2024. + tags: + - substrate - name: ops_reactive_interface status: legacy url: https://pypi.org/project/ops-reactive-interface/ @@ -507,6 +622,8 @@ general: K8s: true description: Helper for interface library developers, to allow an Ops-based interface library to interact with legacy, reactive charms. + tags: + - operations - name: ops.manifest status: '' url: https://pypi.org/project/ops.manifest/ @@ -516,6 +633,8 @@ general: machine: false K8s: true description: Work with Kubernetes manifests. + tags: + - substrate - name: charms.operator_libs_linux.passwd status: legacy url: https://charmhub.io/operator-libs-linux/libraries/passwd @@ -525,6 +644,8 @@ general: machine: true K8s: false description: Manage Linux users and groups. Deprecated in favor of ``charmlibs.passwd``. + tags: + - substrate - name: charms.pgbouncer_k8s.pgb status: team url: https://charmhub.io/pgbouncer-k8s/libraries/pgb @@ -534,6 +655,9 @@ general: machine: true K8s: true description: Shared code between ``pgbouncer`` and ``pgbouncer-k8s`` charms. + tags: + - internal + - data - name: charms.postgresql_k8s.postgresql status: team url: https://charmhub.io/postgresql-k8s/libraries/postgresql @@ -543,6 +667,9 @@ general: machine: true K8s: true description: Shared code between the ``postgresql`` and ``postgresql-k8s`` charms. + tags: + - internal + - data - name: charms.rolling_ops.rollingops status: legacy url: https://charmhub.io/rolling-ops/libraries/rollingops @@ -553,6 +680,8 @@ general: K8s: true description: Legacy library for performing "rolling" operations across units, for example rolling restarts. New charms should use ``charmlibs.rollingops``. + tags: + - operations - name: charms.mysql.s3_helpers status: team url: https://charmhub.io/mysql/libraries/s3_helpers @@ -562,6 +691,9 @@ general: machine: true K8s: true description: Shared code between the ``mysql`` and ``mysql-k8s`` charms. + tags: + - internal + - data - name: sborl status: legacy url: https://pypi.org/project/sborl/ @@ -572,6 +704,8 @@ general: K8s: true description: Legacy library for implementing interface libraries. New interface libraries can use the typed relation data feature available in Ops 2.23+. + tags: + - operations - name: serialized-data-interface status: legacy url: https://pypi.org/project/serialized-data-interface/ @@ -581,6 +715,8 @@ general: machine: true K8s: true description: Relation data validation – use the features available in Ops instead. + tags: + - operations - name: charms.operator_libs_linux.snap status: legacy url: https://charmhub.io/operator-libs-linux/libraries/snap @@ -591,6 +727,8 @@ general: K8s: false description: Use ``snapd`` to install and manage packages. Deprecated in favor of ``charmlibs.snap``. + tags: + - substrate - name: charms.operator_libs_linux.sysctl status: legacy url: https://charmhub.io/operator-libs-linux/libraries/sysctl @@ -600,6 +738,8 @@ general: machine: true K8s: false description: Create and configure ``sysctl`` options. Deprecated in favor of ``charmlibs.sysctl``. + tags: + - substrate - name: charms.operator_libs_linux.systemd status: legacy url: https://charmhub.io/operator-libs-linux/libraries/systemd @@ -610,6 +750,8 @@ general: K8s: false description: Use ``systemd`` to start, stop, and manage system services. Deprecated in favor of ``charmlibs.systemd``. + tags: + - substrate - name: charms.mysql.tls status: team url: https://charmhub.io/mysql/libraries/tls @@ -619,6 +761,9 @@ general: machine: true K8s: true description: Shared code between the ``mysql`` and ``mysql-k8s`` charms. + tags: + - internal + - security - name: charms.data_platform_libs.upgrade status: legacy url: https://charmhub.io/data-platform-libs/libraries/upgrade @@ -628,6 +773,8 @@ general: machine: true K8s: true description: Manage in-place upgrades. Deprecated in favor of ``charm-refresh``. + tags: + - operations interfaces: - name: charms.opencti.opencti_connector status: '' @@ -639,6 +786,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/opencti_connector rel_url_schema: https://documentation.ubuntu.com/charmlibs/reference/interfaces/opencti_connector/v0/ description: '' + tags: + - security - name: charms.filesystem_client.filesystem_info status: '' url: https://charmhub.io/filesystem-client/libraries/filesystem_info @@ -649,6 +798,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/filesystem_info rel_url_schema: '' description: '' + tags: + - data - name: charms.storage_libs.nfs_interfaces status: legacy url: https://charmhub.io/storage-libs/libraries/nfs_interfaces @@ -659,6 +810,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/nfs_share rel_url_schema: '' description: This interface is deprecated in favour of ``filesystem_info``. + tags: + - data - name: charms.alertmanager_k8s.alertmanager_dispatch status: '' url: https://charmhub.io/alertmanager-k8s/libraries/alertmanager_dispatch @@ -669,6 +822,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/alertmanager_dispatch rel_url_schema: '' description: '' + tags: + - observability - name: charms.alertmanager_k8s.alertmanager_remote_configuration status: '' url: https://charmhub.io/alertmanager-k8s/libraries/alertmanager_remote_configuration @@ -679,6 +834,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/alertmanager_remote_configuration rel_url_schema: '' description: '' + tags: + - observability - name: charms.oathkeeper.auth_proxy status: '' url: https://charmhub.io/oathkeeper/libraries/auth_proxy @@ -689,6 +846,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/auth_proxy rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/auth_proxy/v0 description: '' + tags: + - identity-auth - name: charms.oauth2_proxy_k8s.auth_proxy status: '' url: https://charmhub.io/oauth2-proxy-k8s/libraries/auth_proxy @@ -699,6 +858,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/auth_proxy rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/auth_proxy/v0 description: '' + tags: + - identity-auth - name: charms.blackbox_exporter_k8s.blackbox_probes status: '' url: https://charmhub.io/blackbox-exporter-k8s/libraries/blackbox_probes @@ -710,6 +871,8 @@ interfaces: rel_url_schema: '' description: (The Charmhub interfaces page doesn't load, this link is to a charm that uses the interface.) + tags: + - observability - name: charms.catalogue_k8s.catalogue status: '' url: https://charmhub.io/catalogue-k8s/libraries/catalogue @@ -720,6 +883,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/catalogue rel_url_schema: '' description: Transfer catalogue data for display by the provider. + tags: + - observability - name: charms.certificate_transfer_interface.certificate_transfer status: '' url: https://charmhub.io/certificate-transfer-interface/libraries/certificate_transfer @@ -730,6 +895,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/certificate_transfer rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/certificate_transfer/v1 description: Transfer certificates between charms. + tags: + - security - name: charms.grafana_cloud_integrator.cloud_config_provider status: '' url: https://charmhub.io/grafana-cloud-integrator/libraries/cloud_config_provider @@ -740,6 +907,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/grafana_cloud_config rel_url_schema: '' description: '' + tags: + - observability - name: charms.grafana_cloud_integrator.cloud_config_requirer status: '' url: https://charmhub.io/grafana-cloud-integrator/libraries/cloud_config_requirer @@ -750,6 +919,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/grafana_cloud_config rel_url_schema: '' description: '' + tags: + - observability - name: charms.cloudflare_configurator.cloudflared_route status: '' url: https://charmhub.io/cloudflare-configurator/libraries/cloudflared_route @@ -760,6 +931,9 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/cloudflared_route rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/cloudflared_route/v0 description: '' + tags: + - ingress + - networking - name: charms.grafana_agent.cos_agent status: '' url: https://charmhub.io/grafana-agent/libraries/cos_agent @@ -771,6 +945,8 @@ interfaces: rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/cos_agent/v0 description: Machine charm specific interface for exchanging observability related data. + tags: + - observability - name: charms.data_platform_libs.data_interfaces status: '' url: https://charmhub.io/data-platform-libs/libraries/data_interfaces @@ -782,6 +958,9 @@ interfaces: rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/etcd_client description: Database charms from the Data Platform Team recommend using the generic ``data_platform_libs.data_interfaces`` library to implement the interface. + tags: &id001 + - data + - identity-auth - name: charms.data_platform_libs.data_interfaces status: '' url: https://charmhub.io/data-platform-libs/libraries/data_interfaces @@ -793,6 +972,7 @@ interfaces: rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/mysql_client description: Database charms from the Data Platform Team recommend using the generic ``data_platform_libs.data_interfaces`` library to implement the interface. + tags: *id001 - name: charms.data_platform_libs.data_interfaces status: '' url: https://charmhub.io/data-platform-libs/libraries/data_interfaces @@ -804,6 +984,7 @@ interfaces: rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/mongodb_client description: Database charms from the Data Platform Team recommend using the generic ``data_platform_libs.data_interfaces`` library to implement the interface. + tags: *id001 - name: charms.data_platform_libs.data_interfaces status: '' url: https://charmhub.io/data-platform-libs/libraries/data_interfaces @@ -815,6 +996,7 @@ interfaces: rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/opensearch_client description: Database charms from the Data Platform Team recommend using the generic ``data_platform_libs.data_interfaces`` library to implement the interface. + tags: *id001 - name: charms.data_platform_libs.data_interfaces status: '' url: https://charmhub.io/data-platform-libs/libraries/data_interfaces @@ -826,6 +1008,7 @@ interfaces: rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/postgresql_client description: Database charms from the Data Platform Team recommend using the generic ``data_platform_libs.data_interfaces`` library to implement the interface. + tags: *id001 - name: charms.data_platform_libs.database_provides status: legacy url: https://charmhub.io/data-platform-libs/libraries/database_provides @@ -836,6 +1019,8 @@ interfaces: rel_url_charmhub: '' rel_url_schema: '' description: Deprecated in favour of ``data_platform_helpers.data_interfaces``. + tags: + - data - name: charms.data_platform_libs.database_requires status: legacy url: https://charmhub.io/data-platform-libs/libraries/database_requires @@ -846,6 +1031,8 @@ interfaces: rel_url_charmhub: '' rel_url_schema: '' description: Deprecated in favour of ``data_platform_helpers.data_interfaces``. + tags: + - data - name: charms.dex_auth.dex_oidc_config status: '' url: https://charmhub.io/dex-auth/libraries/dex_oidc_config @@ -856,6 +1043,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/dex-oidc-config rel_url_schema: '' description: '' + tags: + - identity-auth - name: charms.sdcore_nms_k8s.fiveg_core_gnb status: '' url: https://charmhub.io/sdcore-nms-k8s/libraries/fiveg_core_gnb @@ -866,6 +1055,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/fiveg_core_gnb rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/fiveg_core_gnb/v0 description: '' + tags: + - telco - name: charms.sdcore_amf_k8s.fiveg_n2 status: '' url: https://charmhub.io/sdcore-amf-k8s/libraries/fiveg_n2 @@ -876,6 +1067,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/fiveg_n2 rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/fiveg_n2/v0 description: '' + tags: + - telco - name: charms.sdcore_upf_k8s.fiveg_n4 status: '' url: https://charmhub.io/sdcore-upf-k8s/libraries/fiveg_n4 @@ -886,6 +1079,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/fiveg_n4 rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/fiveg_n4/v0 description: '' + tags: + - telco - name: charms.sdcore_nrf_k8s.fiveg_nrf status: '' url: https://charmhub.io/sdcore-nrf-k8s/libraries/fiveg_nrf @@ -897,6 +1092,8 @@ interfaces: rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/fiveg_nrf/v0 description: Transfer Network Repository Function information from one charm to another. + tags: + - telco - name: charms.oathkeeper.forward_auth status: '' url: https://charmhub.io/oathkeeper/libraries/forward_auth @@ -907,6 +1104,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/forward_auth rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/forward_auth/v0 description: '' + tags: + - identity-auth - name: charms.oauth2_proxy_k8s.forward_auth status: '' url: https://charmhub.io/oauth2-proxy-k8s/libraries/forward_auth @@ -917,6 +1116,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/forward_auth rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/forward_auth/v0 description: '' + tags: + - identity-auth - name: charms.glauth_utils.glauth_auxiliary status: '' url: https://charmhub.io/glauth-utils/libraries/glauth_auxiliary @@ -927,6 +1128,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/glauth_auxiliary rel_url_schema: '' description: '' + tags: + - identity-auth - name: charms.grafana_k8s.grafana_auth status: '' url: https://charmhub.io/grafana-k8s/libraries/grafana_auth @@ -937,6 +1140,9 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/grafana_auth rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/grafana_auth/v0 description: '' + tags: + - identity-auth + - observability - name: charms.grafana_k8s.grafana_dashboard status: '' url: https://charmhub.io/grafana-k8s/libraries/grafana_dashboard @@ -947,6 +1153,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/grafana_dashboard rel_url_schema: '' description: Manage dashboards for Grafana. + tags: + - observability - name: charms.grafana_k8s.grafana_metadata status: '' url: https://charmhub.io/grafana-k8s/libraries/grafana_metadata @@ -957,6 +1165,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/grafana_metadata rel_url_schema: '' description: '' + tags: + - observability - name: charms.grafana_k8s.grafana_source status: '' url: https://charmhub.io/grafana-k8s/libraries/grafana_source @@ -967,6 +1177,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/grafana-source rel_url_schema: '' description: Provide a data source for Grafana dashboards. + tags: + - observability - name: charms.hydra.hydra_endpoints status: '' url: https://charmhub.io/hydra/libraries/hydra_endpoints @@ -977,6 +1189,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/hydra_endpoints rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/hydra_endpoints/v0 description: '' + tags: + - identity-auth - name: charms.traefik_k8s.ingress status: '' url: https://charmhub.io/traefik-k8s/libraries/ingress @@ -987,6 +1201,9 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/ingress rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/ingress/v2 description: Manage per-application ingress and and load-balancing. + tags: + - ingress + - networking - name: charms.nginx_ingress_integrator.ingress status: '' url: https://charmhub.io/nginx-ingress-integrator/libraries/ingress @@ -997,6 +1214,9 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/ingress rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/ingress/v2 description: Manage ingress. + tags: + - ingress + - networking - name: charms.traefik_k8s.ingress_per_unit status: '' url: https://charmhub.io/traefik-k8s/libraries/ingress_per_unit @@ -1007,6 +1227,9 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/ingress_per_unit rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/ingress_per_unit/v0 description: '' + tags: + - ingress + - networking - name: charms.istio_pilot.istio_gateway_info status: '' url: https://charmhub.io/istio-pilot/libraries/istio_gateway_info @@ -1017,6 +1240,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/istio-gateway-info rel_url_schema: '' description: '' + tags: + - networking - name: charms.mlops_libs.k8s_service_info status: '' url: https://charmhub.io/mlops-libs/libraries/k8s_service_info @@ -1027,6 +1252,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/k8s-service rel_url_schema: '' description: '' + tags: + - networking - name: charms.karma_k8s.karma_dashboard status: '' url: https://charmhub.io/karma-k8s/libraries/karma_dashboard @@ -1037,6 +1264,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/karma_dashboard rel_url_schema: '' description: '' + tags: + - observability - name: charms.kratos_external_idp_integrator.kratos_external_provider status: '' url: https://charmhub.io/kratos-external-idp-integrator/libraries/kratos_external_provider @@ -1047,6 +1276,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/external_provider rel_url_schema: '' description: '' + tags: + - identity-auth - name: charms.kratos.kratos_info status: '' url: https://charmhub.io/kratos/libraries/kratos_info @@ -1057,6 +1288,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/kratos_info rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/kratos_info/v0 description: '' + tags: + - identity-auth - name: charms.kratos.kratos_registration_webhook status: unlisted url: https://charmhub.io/kratos/libraries/kratos_registration_webhook @@ -1067,6 +1300,8 @@ interfaces: rel_url_charmhub: '' rel_url_schema: '' description: '' + tags: + - identity-auth - name: charms.kubeflow_dashboard.kubeflow_dashboard_links status: '' url: https://charmhub.io/kubeflow-dashboard/libraries/kubeflow_dashboard_links @@ -1077,6 +1312,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/kubeflow_dashboard_links rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/kubeflow_dashboard_links/v0 description: '' + tags: + - operations - name: charms.resource_dispatcher.kubernetes_manifests status: '' url: https://charmhub.io/resource-dispatcher/libraries/kubernetes_manifests @@ -1087,6 +1324,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/kubernetes_manifest rel_url_schema: '' description: '' + tags: + - operations - name: charms.glauth_k8s.ldap status: '' url: https://charmhub.io/glauth-k8s/libraries/ldap @@ -1097,6 +1336,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/ldap rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/ldap/v0 description: '' + tags: + - identity-auth - name: charms.lego_base_k8s.lego_client status: legacy url: https://charmhub.io/lego-base-k8s/libraries/lego_client @@ -1107,6 +1348,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/tls_certificates rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/tls_certificates/v1 description: Legacy charm library used to implement the provider side of this interface. + tags: + - security - name: loadbalancer_interface status: '' url: https://pypi.org/project/loadbalancer-interface/ @@ -1117,6 +1360,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/loadbalancer rel_url_schema: '' description: '' + tags: + - networking - name: charms.identity_platform_login_ui_operator.login_ui_endpoints status: '' url: https://charmhub.io/identity-platform-login-ui-operator/libraries/login_ui_endpoints @@ -1127,6 +1372,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/login_ui_endpoints rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/login_ui_endpoints/v0 description: '' + tags: + - identity-auth - name: charms.loki_k8s.loki_push_api status: '' url: https://charmhub.io/loki-k8s/libraries/loki_push_api @@ -1137,6 +1384,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/loki_push_api rel_url_schema: '' description: Manage logs for Loki. + tags: + - observability - name: charms.synapse.matrix_auth status: '' url: https://charmhub.io/synapse/libraries/matrix_auth @@ -1148,6 +1397,8 @@ interfaces: rel_url_schema: '' description: (The Charmhub interfaces page doesn't load, this link is to a charm that uses the interface.) + tags: + - identity-auth - name: charms.nginx_ingress_integrator.nginx_route status: '' url: https://charmhub.io/nginx-ingress-integrator/libraries/nginx_route @@ -1158,6 +1409,9 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/nginx_route rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/nginx_route/v0 description: Manage external HTTP access to Kubernetes workloads using Nginx. + tags: + - ingress + - networking - name: charms.oathkeeper.oathkeeper_info status: '' url: https://charmhub.io/oathkeeper/libraries/oathkeeper_info @@ -1168,6 +1422,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/oathkeeper_info rel_url_schema: '' description: '' + tags: + - identity-auth - name: charms.hydra.oauth status: '' url: https://charmhub.io/hydra/libraries/oauth @@ -1178,6 +1434,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/oauth rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/oauth/v0 description: '' + tags: + - identity-auth - name: charms.s3proxy_k8s.object_storage status: '' url: https://charmhub.io/s3proxy-k8s/libraries/object_storage @@ -1188,6 +1446,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/s3 rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/s3/v0 description: '' + tags: + - data - name: charms.openfga_k8s.openfga status: '' url: https://charmhub.io/openfga-k8s/libraries/openfga @@ -1198,6 +1458,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/openfga rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/openfga/v1 description: '' + tags: + - identity-auth - name: ops-lib-mysql status: legacy url: https://pypi.org/project/ops-lib-mysql/ @@ -1209,6 +1471,8 @@ interfaces: rel_url_schema: '' description: Modern charms should use the ``mysql_client`` interface, using the ``data_interfaces`` lib from ``data-platform-helpers``. + tags: + - data - name: ops-lib-pgsql status: legacy url: https://pypi.org/project/ops-lib-pgsql/ @@ -1218,8 +1482,10 @@ interfaces: rel_name: pgsql rel_url_charmhub: '' rel_url_schema: '' - description: Modern charms should use the ``postgresql_client`` interface, using the - ``data_interfaces`` lib from ``data-platform-helpers``. + description: Modern charms should use the ``postgresql_client`` interface, using + the ``data_interfaces`` lib from ``data-platform-helpers``. + tags: + - data - name: ops.interface_kube_control status: '' url: https://pypi.org/project/ops.interface-kube-control/ @@ -1230,6 +1496,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/kube-control rel_url_schema: '' description: '' + tags: + - operations - name: charms.parca.parca_scrape status: legacy url: https://charmhub.io/parca/libraries/parca_scrape @@ -1241,6 +1509,8 @@ interfaces: rel_url_schema: '' description: Deprecated in favour of the ``parca-k8s`` charm's ``parca_scrape`` lib. + tags: + - observability - name: charms.parca_k8s.parca_scrape status: '' url: https://charmhub.io/parca-k8s/libraries/parca_scrape @@ -1251,6 +1521,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/parca_scrape rel_url_schema: '' description: '' + tags: + - observability - name: charms.mimir_coordinator_k8s.prometheus_api status: '' url: https://charmhub.io/mimir-coordinator-k8s/libraries/prometheus_api @@ -1261,6 +1533,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/prometheus_api rel_url_schema: '' description: '' + tags: + - observability - name: charms.prometheus_k8s.prometheus_remote_write status: '' url: https://charmhub.io/prometheus-k8s/libraries/prometheus_remote_write @@ -1271,6 +1545,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/prometheus_remote_write rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/prometheus_remote_write/v0 description: Transfer Prometheus metrics data between charms. + tags: + - observability - name: charms.prometheus_k8s.prometheus_scrape status: '' url: https://charmhub.io/prometheus-k8s/libraries/prometheus_scrape @@ -1281,6 +1557,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/prometheus_scrape rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/prometheus_scrape/v0 description: Manage metrics for Prometheus. + tags: + - observability - name: charms.prometheus_pushgateway_k8s.pushgateway status: '' url: https://charmhub.io/prometheus-pushgateway-k8s/libraries/pushgateway @@ -1292,6 +1570,8 @@ interfaces: rel_url_schema: '' description: (The Charmhub interfaces page doesn't load, this link is to a charm that uses the interface.) + tags: + - observability - name: charms.redis_k8s.redis status: '' url: https://charmhub.io/redis-k8s/libraries/redis @@ -1302,6 +1582,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/redis rel_url_schema: '' description: '' + tags: + - data - name: charms.data_platform_libs.s3 status: '' url: https://charmhub.io/data-platform-libs/libraries/s3 @@ -1312,6 +1594,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/s3 rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/s3/v0 description: Manage s3 credentials and metadata. + tags: + - data - name: charms.saml_integrator.saml status: '' url: https://charmhub.io/saml-integrator/libraries/saml @@ -1322,6 +1606,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/saml rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/saml/v0 description: '' + tags: + - identity-auth - name: charms.sdcore_nms_k8s.sdcore_config status: '' url: https://charmhub.io/sdcore-nms-k8s/libraries/sdcore_config @@ -1332,6 +1618,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/sdcore_config rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/sdcore_config/v0 description: Provide or require the web UI gRPC address for SD-Core configuration. + tags: + - telco - name: charms.sdcore_webui_k8s.sdcore_management status: '' url: https://charmhub.io/sdcore-webui-k8s/libraries/sdcore_management @@ -1342,6 +1630,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/sdcore_management rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/sdcore_management/v0 description: '' + tags: + - telco - name: charmlibs.interfaces.service_mesh status: recommended url: https://pypi.org/project/charmlibs-interfaces-service-mesh @@ -1352,6 +1642,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/service_mesh rel_url_schema: '' description: Enroll charms onto a service mesh and provision network policies. + tags: + - networking - name: charms.istio_beacon_k8s.service_mesh status: legacy url: https://charmhub.io/istio-beacon-k8s/libraries/service_mesh @@ -1362,6 +1654,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/service_mesh rel_url_schema: '' description: Deprecated in favour of ``charmlibs.interfaces.service_mesh``. + tags: + - networking - name: charms.smtp_integrator.smtp status: '' url: https://charmhub.io/smtp-integrator/libraries/smtp @@ -1372,6 +1666,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/smtp rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/smtp/v0 description: '' + tags: + - operations - name: charms.tempo_coordinator_k8s.tempo_api status: '' url: https://charmhub.io/tempo-coordinator-k8s/libraries/tempo_api @@ -1382,6 +1678,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/tempo_api rel_url_schema: '' description: '' + tags: + - observability - name: charmlibs.interfaces.tls_certificates status: recommended url: https://pypi.org/project/charmlibs-interfaces-tls-certificates @@ -1392,6 +1690,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/tls_certificates rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/tls_certificates/v1 description: Manage TLS certificates. + tags: + - security - name: charms.tls_certificates_interface.tls_certificates status: legacy url: https://charmhub.io/tls-certificates-interface/libraries/tls_certificates @@ -1402,6 +1702,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/tls_certificates rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/tls_certificates/v1 description: Manage TLS certificates. Deprecated in favor of ``charmlibs.interfaces.tls_certificates``. + tags: + - security - name: charms.tempo_k8s.tracing status: legacy url: https://charmhub.io/tempo-k8s/libraries/tracing @@ -1412,6 +1714,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/tracing rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/tracing/v2 description: Deprecated in favour of ``tempo_coordinator_k8s .tracing``. + tags: + - observability - name: charms.tempo_coordinator_k8s.tracing status: '' url: https://charmhub.io/tempo-coordinator-k8s/libraries/tracing @@ -1422,6 +1726,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/tracing rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/tracing/v2 description: Provide and consume tracing data. + tags: + - observability - name: charms.traefik_route_k8s.traefik_route status: legacy url: https://charmhub.io/traefik-route-k8s/libraries/traefik_route @@ -1433,6 +1739,9 @@ interfaces: rel_url_schema: '' description: Deprecated in favour of the ``traefik-k8s`` charm's ``traefik_route`` lib. + tags: + - ingress + - networking - name: charms.traefik_k8s.traefik_route status: '' url: https://charmhub.io/traefik-k8s/libraries/traefik_route @@ -1443,6 +1752,9 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/traefik_route rel_url_schema: '' description: '' + tags: + - ingress + - networking - name: charms.vault_k8s.vault_kv status: '' url: https://charmhub.io/vault-k8s/libraries/vault_kv @@ -1453,6 +1765,8 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/vault_kv rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/vault_kv/v0 description: '' + tags: + - security - name: litmus-libs status: '' url: https://pypi.org/project/litmus-libs/ @@ -1463,6 +1777,9 @@ interfaces: rel_url_charmhub: https://charmhub.io/integrations/litmus_auth rel_url_schema: https://documentation.ubuntu.com/charmlibs/reference/interfaces/litmus_auth/v0/ description: '' + tags: + - internal + - operations - name: charmlibs.interfaces.istio_request_auth status: recommended url: https://pypi.org/project/charmlibs-interfaces-istio-request-auth @@ -1473,6 +1790,9 @@ interfaces: rel_url_charmhub: '' rel_url_schema: '' description: Configure Istio RequestAuthentication resources via relation data. + tags: + - identity-auth + - networking - name: charmed_service_mesh_helpers.interfaces.request_auth status: legacy url: https://pypi.org/project/charmed-service-mesh-helpers/ @@ -1483,6 +1803,9 @@ interfaces: rel_url_charmhub: '' rel_url_schema: '' description: Deprecated in favour of charmlibs.interfaces.istio_request_auth. + tags: + - identity-auth + - networking - name: charmlibs.interfaces.gateway_metadata status: recommended url: https://pypi.org/project/charmlibs-interfaces-gateway-metadata @@ -1493,6 +1816,8 @@ interfaces: rel_url_charmhub: '' rel_url_schema: '' description: Share Kubernetes Gateway API workload metadata between charms. + tags: + - networking - name: charmed_service_mesh_helpers.interfaces.gateway_metadata status: legacy url: https://pypi.org/project/charmed-service-mesh-helpers/ @@ -1503,6 +1828,8 @@ interfaces: rel_url_charmhub: '' rel_url_schema: '' description: Deprecated in favour of ``charmlibs.interfaces.gateway_metadata``. + tags: + - networking - name: dpcharmlibs.interfaces status: '' url: https://pypi.org/project/dpcharmlibs-interfaces @@ -1514,6 +1841,8 @@ interfaces: rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/valkey_client description: Database charms from the Data Platform Team recommend using the generic ``data_platform_libs.data_interfaces`` library to implement the interface. + tags: + - data - name: charmlibs.interfaces.istio_ingress_route status: recommended url: https://pypi.org/project/charmlibs-interfaces-istio-ingress-route @@ -1525,6 +1854,9 @@ interfaces: rel_url_schema: '' description: Advanced ingress routing through the istio-ingress-k8s charm with multi-port listeners and gRPC support. + tags: + - ingress + - networking - name: charms.istio_ingress_k8s.istio_ingress_route status: legacy url: https://charmhub.io/istio-ingress-k8s/libraries/istio_ingress_route @@ -1535,3 +1867,6 @@ interfaces: rel_url_charmhub: '' rel_url_schema: '' description: Deprecated in favour of ``charmlibs.interfaces.istio_ingress_route``. + tags: + - ingress + - networking diff --git a/.docs/reference/tags.yaml b/.docs/reference/tags.yaml new file mode 100644 index 000000000..a2f38635f --- /dev/null +++ b/.docs/reference/tags.yaml @@ -0,0 +1,88 @@ +domain-tags: + substrate: + description: >- + Charm-to-machine interaction: package management, system configuration, + file operations, container resource management. + criteria: >- + The library wraps or abstracts an operating system or substrate-level + operation. It does not implement a charm-to-charm relation interface. + + observability: + description: >- + Telemetry and monitoring: metrics, logging, tracing, profiling, + dashboarding, alerting, and integration with the Canonical Observability + Stack. + criteria: >- + The library implements an observability-related relation interface, + provides helpers for emitting or collecting telemetry, or integrates with + COS components. + + identity-auth: + description: >- + Authentication, authorisation, and identity management: OAuth, OIDC, + SAML, LDAP, forward auth, fine-grained authorisation. + criteria: >- + The library implements an identity or auth-related relation interface, or + provides helpers for authentication and authorisation flows. + + ingress: + description: >- + External access and routing: ingress configuration, load balancing, + reverse proxy integration. + criteria: >- + The library implements an ingress-related relation interface or provides + helpers for configuring external access to charmed applications. + + networking: + description: >- + External connectivity, DNS, routing, and access management: ingress, + load balancing, reverse proxy integration, DNS record management, IP + routing, and service exposure. + criteria: >- + The library implements a networking-related relation interface or provides + helpers for external connectivity or routing. Superset of ingress: all + ingress-tagged libraries also carry this tag. + + data: + description: >- + Database and storage integration: relational databases, NoSQL, object + storage, message queues, key-value stores. + criteria: >- + The library implements a data or storage-related relation interface or + provides helpers for interacting with data platforms. + + security: + description: >- + TLS certificate management, certificate transfer, secret storage, and + security-related integrations. + criteria: >- + The library implements a TLS/certificate-related relation interface, + manages secrets, or provides security-related functionality. + + operations: + description: >- + Charm lifecycle, coordination, and general-purpose utilities not specific + to any domain: rolling operations, leader coordination, status management. + criteria: >- + The library provides broadly applicable charm operational functionality + and does not belong exclusively to any of the above domain tags. + + telco: + description: >- + Telecommunications and 5G network function integration: interfaces for + 5G core network functions (AMF, UPF, NRF, etc.) and SD-Core management. + criteria: >- + The library implements a relation interface for a telecommunications + network function or provides helpers for integrating with SD-Core or 5G + network components. + +audience-tags: + internal: + description: >- + Scoped to a charm team. Shares implementation logic within a set of + related charms maintained together. Not intended for use by developers + outside the publishing team. + criteria: >- + The library is not designed as a stable public API for external consumers. + It typically carries the name of its publishing charm family (e.g. + mysql.*, postgresql_k8s.*). diff --git a/.docs/tutorial.md b/.docs/tutorial.md index 5759b0db3..88f53f0f3 100644 --- a/.docs/tutorial.md +++ b/.docs/tutorial.md @@ -355,6 +355,14 @@ If you're following along with your own library, then see the next section for h Otherwise, if you're using the `uptime` example, open a PR against the `main` branch of your fork -- just make sure you enable the workflows first! You'll be prompted to do this if you visit `https://github.com//charmlibs/actions`. +## Add library metadata + +The {ref}`interface library listing ` and {ref}`general library listing ` are generated from `.docs/reference/libs.yaml`. +Add an entry for your library so it appears in the listing. +The entry includes your library's name, status, URLs, description, and tags. +See `.docs/reference/tags.yaml` for the full list of available tags and their assignment criteria. +Don't invent new tags, only use tags defined in `tags.yaml`. + ## Next steps You can stop here if you're using the `uptime` example. diff --git a/.scripts/ls.py b/.scripts/ls.py index a14d9d5cc..b7efc228f 100755 --- a/.scripts/ls.py +++ b/.scripts/ls.py @@ -66,8 +66,9 @@ class Info: description: str = '' status: str = '' schema_path: str = '' + tags: list[str] = dataclasses.field(default_factory=list[str]) - def to_dict(self, *fields: str) -> dict[str, str]: + def to_dict(self, *fields: str) -> dict[str, object]: """Return dictionary containing only specified fields.""" return {field: getattr(self, field) for field in fields} @@ -105,7 +106,7 @@ def _main() -> None: if args.output: result = sorted( (info.to_dict(*args.output) for info in infos), - key=lambda di: tuple(di.items()), + key=lambda di: json.dumps(di, default=str), ) else: result = sorted(getattr(info, args.output_only) for info in infos) @@ -190,6 +191,8 @@ def _ls( info.status = _get_status(category, root, path) if 'schema_path' in output: info.schema_path = _get_schema_path_str(category, root, path) + if 'tags' in output: + info.tags = _lib_tags().get(_get_lib_name(category, root, path), []) infos.append(info) return infos @@ -398,10 +401,21 @@ def _get_interface_version(path: pathlib.Path, root: pathlib.Path = _REPO_ROOT) @functools.cache +def _pyproject_toml(package: pathlib.Path, root: pathlib.Path = _REPO_ROOT): + with (root / package / 'pyproject.toml').open('rb') as f: + return tomllib.load(f) + + +@functools.cache +def _interface_yaml(path: pathlib.Path, root: pathlib.Path = _REPO_ROOT): + version = _get_interface_version(path, root=root) + with (root / path / 'interface' / f'v{version}' / 'interface.yaml').open() as f: + return yaml.safe_load(f) + + def _lib_urls() -> dict[str, str]: result: dict[str, str] = {} - data = yaml.safe_load((_REPO_ROOT / '.docs' / 'reference' / 'libs.yaml').read_text()) - for entry in (*data['general'], *data['interfaces']): + for entry in (*_libs_yaml()['general'], *_libs_yaml()['interfaces']): # Library names should be unique, but we currently have an entry for # charms.data_platform_libs.data_interfaces for each interface it supports # This doesn't break our lookups though, since they all have the same metadata @@ -414,17 +428,16 @@ def _lib_urls() -> dict[str, str]: return result -@functools.cache -def _pyproject_toml(package: pathlib.Path, root: pathlib.Path = _REPO_ROOT): - with (root / package / 'pyproject.toml').open('rb') as f: - return tomllib.load(f) +def _lib_tags() -> dict[str, list[str]]: + return { + entry['name']: sorted(entry.get('tags') or []) + for entry in (*_libs_yaml()['general'], *_libs_yaml()['interfaces']) + } @functools.cache -def _interface_yaml(path: pathlib.Path, root: pathlib.Path = _REPO_ROOT): - version = _get_interface_version(path, root=root) - with (root / path / 'interface' / f'v{version}' / 'interface.yaml').open() as f: - return yaml.safe_load(f) +def _libs_yaml(): + return yaml.safe_load((_REPO_ROOT / '.docs' / 'reference' / 'libs.yaml').read_text()) def _normalize(name: str) -> str: diff --git a/interfaces/index.json b/interfaces/index.json index 43d0dc142..3212139ca 100644 --- a/interfaces/index.json +++ b/interfaces/index.json @@ -7,6 +7,9 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/auth_proxy/", "summary": "Configure an Identity and Access Proxy.", "description": "The `auth_proxy` interface allows charms to configure an Identity and Access Proxy.\nThe requirer supplies the configuration, such as protected URLs, allowed endpoints, and headers.\nThe provider is responsible for transforming the configuration into access rules and forwarding relevant configuration to the proxy.", + "tags": [ + "identity-auth" + ], "status": "draft" }, { @@ -17,6 +20,10 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/azure_service_principal/", "summary": "Interact with Microsoft Entra ID using service principal credentials.", "description": "The `azure_service_principal` interface allows charms to receive the service principal credential needed to access Entra ID objects such as Azure storage.\nRequirer charms can specify which fields should be transferred as Juju secrets, and the provider charm will deliver the credentials as requested.", + "tags": [ + "data", + "identity-auth" + ], "status": "draft" }, { @@ -27,6 +34,7 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/azure_storage/", "summary": "Access Azure Blob Storage and Data Lake Storage (Gen 2).", "description": "The `azure_storage` interface allows charms to access Azure Blob Storage and Data Lake Storage (Gen 2) with appropriate credentials and connection details.\nThe provider charm supplies storage account credentials and configuration, while the requirer charm specifies the container to access.", + "tags": [], "status": "draft" }, { @@ -37,6 +45,9 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/certificate_transfer/", "summary": "Receive public or CA certificates.", "description": "The `certificate_transfer` interface allows charms to receive public and/or CA certificates.\nThe provider charm shares its certificates with the requirer.", + "tags": [ + "security" + ], "status": "draft" }, { @@ -47,6 +58,10 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/cloudflared_route/", "summary": "Configure a cloudflared tunnel.", "description": "The `cloudflared_route` interface allows information to be exchanged between the cloudflared tunnel configurator charm and the cloudflared tunnel charm.", + "tags": [ + "ingress", + "networking" + ], "status": "draft" }, { @@ -57,6 +72,10 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/connect_client/", "summary": "Interact with Kafka Connect clusters.", "description": "The `connect_client` interface allows charms to interact with Kafka Connect clusters for connector plugin and task management.\nThe provider charm manages the Kafka Connect cluster and deploys connector plugins from URLs supplied by the requirer, while the requirer charm manages its connectors and tasks using the provider's REST endpoints.", + "tags": [ + "data", + "identity-auth" + ], "status": "draft" }, { @@ -67,6 +86,9 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/cos_agent/", "summary": "Machine-charm-specific interface for managing multiple observability-related data streams.", "description": "The `cos_agent` interface allows machine charms to send telemetry \u2014 such as metrics, logs, dashboards, and alert rules \u2014 to Opentelemetry Colector or Grafana Agent charms.\n\nThis interface is designed specifically for machine charms, where the requirer is typically the [grafana-agent](https://charmhub.io/grafana-agent) and the [opentelemetry-collector](https://github.com/canonical/opentelemetry-collector-operator/) subordinate charms.", + "tags": [ + "observability" + ], "status": "published" }, { @@ -77,6 +99,7 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/database_backup/", "summary": "Manage database backup and restore operations.", "description": "The `database_backup` interface allows a database backup manager charm to coordinate backup and restore operations with database charms.\nThe provider charm (database) executes backup and restore jobs based on requirer requests and reports their status, while the requirer charm (backup manager) requests jobs, manages storage configuration, and stores backup artifacts.", + "tags": [], "status": "draft" }, { @@ -87,6 +110,7 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/dns_record/", "summary": "Create and manage DNS records.", "description": "", + "tags": [], "status": "draft" }, { @@ -97,6 +121,10 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/etcd_client/", "summary": "Access etcd clusters with credentials and TLS configuration.", "description": "The `etcd_client` interface allows charms to access etcd clusters with user-specific credentials and TLS configuration.\nThe provider charm creates etcd users and roles based on requirer requests, while the requirer charm shares certificate information and key access prefixes.", + "tags": [ + "data", + "identity-auth" + ], "status": "draft" }, { @@ -107,6 +135,9 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/filesystem_info/", "summary": "Export mount information about shared filesystems.", "description": "The `filesystem_info` interface allows charms that export shared filesystems to expose the required mount information.\nThe provider charm exports mount information for the requirer charm.", + "tags": [ + "data" + ], "status": "draft" }, { @@ -117,6 +148,9 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/fiveg_core_gnb/", "summary": "", "description": "", + "tags": [ + "telco" + ], "status": "draft" }, { @@ -127,6 +161,7 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/fiveg_f1/", "summary": "", "description": "", + "tags": [], "status": "draft" }, { @@ -137,6 +172,7 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/fiveg_gnb_identity/", "summary": "", "description": "", + "tags": [], "status": "draft" }, { @@ -147,6 +183,9 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/fiveg_n2/", "summary": "", "description": "", + "tags": [ + "telco" + ], "status": "draft" }, { @@ -157,6 +196,7 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/fiveg_n3/", "summary": "", "description": "", + "tags": [], "status": "draft" }, { @@ -167,6 +207,9 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/fiveg_n4/", "summary": "", "description": "", + "tags": [ + "telco" + ], "status": "draft" }, { @@ -177,6 +220,9 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/fiveg_nrf/", "summary": "", "description": "", + "tags": [ + "telco" + ], "status": "draft" }, { @@ -187,6 +233,7 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/fiveg_rfsim/", "summary": "", "description": "", + "tags": [], "status": "draft" }, { @@ -197,6 +244,9 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/forward_auth/", "summary": "", "description": "", + "tags": [ + "identity-auth" + ], "status": "draft" }, { @@ -207,6 +257,10 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/grafana_auth/", "summary": "Provide authentication configuration for Grafana.", "description": "The `grafana_auth` interface supports configuring Grafana authentication.\nThe provider sends the authentication mode and its configuration.\nThe requirer configures authentication with Grafana and replies with its publicly reachable URL.", + "tags": [ + "identity-auth", + "observability" + ], "status": "draft" }, { @@ -217,6 +271,9 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/grafana_datasource/", "summary": "Provide Grafana data source servers.", "description": "The `grafana_source` interface supports exposing servers implementing the Grafana source HTTP API.\nThe provider publishes the endpoint URL for the servers, one per unit.\nThe requirer replies with unique IDs for the Grafana application and a mapping of unique IDs for the units.", + "tags": [ + "observability" + ], "status": "published" }, { @@ -227,6 +284,10 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/grafana_datasource_exchange/", "summary": "Share UIDs for Grafana datasources between charms.", "description": "The `grafana_datasource_exchange` interface allows charms that generate telemetry and have a reference to the datasources where said telemetry is queriable, to share those references to other charms for correlation and cross-referencing purposes.\n\nThis is a symmetrical relation, where implementers broadcast the same information regardless of whether they are the provider or requirer.\nTo avoid constraints on integration topology, charms should implement both a requirer and provider endpoint for this interface.", + "tags": [ + "internal", + "observability" + ], "status": "draft" }, { @@ -237,6 +298,9 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/hydra_endpoints/", "summary": "", "description": "", + "tags": [ + "identity-auth" + ], "status": "draft" }, { @@ -247,6 +311,10 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/ingress/", "summary": "", "description": "", + "tags": [ + "ingress", + "networking" + ], "status": "published" }, { @@ -257,6 +325,10 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/ingress_per_unit/", "summary": "", "description": "", + "tags": [ + "ingress", + "networking" + ], "status": "published" }, { @@ -267,6 +339,7 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/ip_router/", "summary": "", "description": "", + "tags": [], "status": "draft" }, { @@ -277,6 +350,7 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/jwt/", "summary": "Provide and consume JSON Web Token configuration.", "description": "The `jwt` interface allows charms to provide or require JSON Web Token configuration for secure token validation.\nThe provider charm supplies signing keys and optional validation parameters, while the requirer charm applies these settings to validate incoming JWTs in its workload.", + "tags": [], "status": "draft" }, { @@ -287,6 +361,7 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/k8s-service/", "summary": "", "description": "", + "tags": [], "status": "published" }, { @@ -297,6 +372,7 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/k8s_backup_target/", "summary": "Configure Kubernetes backup target specifications.", "description": "The `k8s_backup_target` interface allows client charms to specify Kubernetes backup requirements to a backup integrator charm.\nThe provider charm (client) provides backup specifications including namespaces, resource types, and retention policies, while the requirer charm (backup integrator) receives these specifications and forwards them to a backup operator.", + "tags": [], "status": "draft" }, { @@ -307,6 +383,10 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/kafka_client/", "summary": "Interact with Kafka clusters as a client.", "description": "The `kafka_client` interface allows charms to interact with Kafka clusters as producers, consumers, or admin clients.\nThe provider charm creates Kafka users with appropriate ACLs and permissions based on requested roles, while the requirer charm specifies its desired topic and role.", + "tags": [ + "data", + "identity-auth" + ], "status": "draft" }, { @@ -317,6 +397,7 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/karapace_client/", "summary": "Access Karapace schema registry services.", "description": "The `karapace_client` interface allows charms to access Karapace schema registry services with user-specific credentials.\nThe provider charm manages Karapace users and subject access controls, while the requirer charm requests access to specific subjects with desired role permissions.", + "tags": [], "status": "draft" }, { @@ -327,6 +408,7 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/kratos_external_idp/", "summary": "", "description": "", + "tags": [], "status": "draft" }, { @@ -337,6 +419,9 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/kratos_info/", "summary": "", "description": "", + "tags": [ + "identity-auth" + ], "status": "published" }, { @@ -347,6 +432,7 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/kubeflow_dashboard_links/", "summary": "", "description": "", + "tags": [], "status": "draft" }, { @@ -357,6 +443,9 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/ldap/", "summary": "Configure and connect to a Lightweight Directory Access Protocol (LDAP) server.", "description": "The `ldap` interface supports sharing configuration and connection information for a Lightweight Directory Access Protocol (LDAP) server.\nThe provider is responsible for sharing the LDAP URL and other information needed for the requirer to connect and perform LDAP operations.", + "tags": [ + "identity-auth" + ], "status": "draft" }, { @@ -367,6 +456,10 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/litmus_auth/", "summary": "Exchange gRPC server endpoints for communication between Litmus auth and backend charms.", "description": "The `litmus_auth` interface enables communication between Litmus auth and Litmus backend charms.\nBoth the provider and requirer publish their respective gPRC server's endpoint.", + "tags": [ + "internal", + "operations" + ], "status": "draft" }, { @@ -377,6 +470,9 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/login_ui_endpoints/", "summary": "", "description": "", + "tags": [ + "identity-auth" + ], "status": "draft" }, { @@ -387,6 +483,7 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/milter/", "summary": "Provide or consume a mail filter.", "description": "The `milter` interface allows charms to connect to a charm serving as a mail filter (milter).\nThe provider charm shares the information needed to connect.", + "tags": [], "status": "published" }, { @@ -397,6 +494,7 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/mimir_cluster/", "summary": "Mimir coordinator and worker charm communication.", "description": "The `mimir_cluster` interface supports communication between the [mimir-coordinator-k8s](https://charmhub.io/mimir-coordinator-k8s) and [mimir-worker-k8s](https://charmhub.io/mimir-worker-k8s) charms.", + "tags": [], "status": "draft" }, { @@ -407,6 +505,10 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/mongodb_client/", "summary": "Access MongoDB databases with unique credentials.", "description": "The `mongodb_client` interface allows charms to access MongoDB databases with unique, relation-specific credentials transmitted via Juju Secrets.\nThe provider charm creates database users and provides connection details, while the requirer charm requests a database name and optional custom entities with specific role permissions.", + "tags": [ + "data", + "identity-auth" + ], "status": "draft" }, { @@ -417,6 +519,10 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/mysql_client/", "summary": "Access MySQL databases with unique credentials.", "description": "The `mysql_client` interface allows charms to access MySQL databases with unique, relation-specific credentials transmitted via Juju Secrets.\nThe provider charm creates database users and provides connection details, while the requirer charm requests a database name and optional custom entities with specific role permissions.", + "tags": [ + "data", + "identity-auth" + ], "status": "published" }, { @@ -427,6 +533,10 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/nginx-route/", "summary": "Create an Nginx ingress resource in a Kubernetes cluster.", "description": "The `nginx_route` interface allows charms to request the creation of an Nginx ingress resource in the Kubernetes cluster.\nThe requirer charm supplies the configuration for the ingress resources.\nThe provider charm creates the resource as requested.", + "tags": [ + "ingress", + "networking" + ], "status": "draft" }, { @@ -437,6 +547,9 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/oauth/", "summary": "Connect to an OAuth2/OIDC Provider.", "description": "The `oauth` interface allows charms to interface with an OAuth2/OIDC Provider.\nThe requirer acts as an OAuth2 client.\nThe provider exposes an OAuth2/OIDC Provider.", + "tags": [ + "identity-auth" + ], "status": "draft" }, { @@ -447,6 +560,9 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/opencti_connector/", "summary": "Connect a single OpenCTI provider to one or more OpenCTI connectors.", "description": "The `opencti_connector` interface supports connecting an OpenCTI provider charm to one or more OpenCTI connectors.\nThe provider charm specifies the expected connector type for each connector relation.\nEach requirer charm shares its OpenCTI URL (and token) for the provider to connect to.", + "tags": [ + "security" + ], "status": "published" }, { @@ -457,6 +573,9 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/openfga/", "summary": "Set up and connect to an OpenFGA store.", "description": "The `openfga` interface supports setting up an OpenFGA store and connecting to it.\nThe requirer specifies the store name, while the provider creates the store and shares the data needed to connect.", + "tags": [ + "identity-auth" + ], "status": "published" }, { @@ -467,6 +586,10 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/opensearch_client/", "summary": "Access OpenSearch and ElasticSearch clusters.", "description": "The `opensearch_client` interface allows charms to access OpenSearch and ElasticSearch clusters with user-specific credentials and index permissions transmitted via Juju Secrets.\nThe provider charm creates users and manages index access controls, while the requirer charm requests access to specific indices with optional role permissions.", + "tags": [ + "data", + "identity-auth" + ], "status": "draft" }, { @@ -477,6 +600,10 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/postgresql_client/", "summary": "Access PostgreSQL databases with unique credentials.", "description": "The `postgresql_client` interface allows charms to access PostgreSQL databases with unique, relation-specific credentials.\nThe provider charm creates database users and provides connection details, while the requirer charm requests a database name and optional custom entities with specific role permissions. Supports subordinate charms that provide external connectivity.", + "tags": [ + "data", + "identity-auth" + ], "status": "draft" }, { @@ -487,6 +614,7 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/profiling/", "summary": "", "description": "", + "tags": [], "status": "draft" }, { @@ -497,6 +625,9 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/prometheus_remote_write/", "summary": "Write Prometheus metrics to remote endpoint.", "description": "The `prometheus_remote_write` endpoint supports writing Prometheus metrics to a remote endpoint.\nThe provider publishes one or more endpoints, and emits alerts per rule files.\nThe requirer pushes metrics to the provided endpoints, and sends rule files to the provider.", + "tags": [ + "observability" + ], "status": "published" }, { @@ -507,6 +638,9 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/prometheus_scrape/", "summary": "Configure Prometheus scraping jobs.", "description": "The `prometheus_scrape` interface supports configuring Prometheus scrape jobs and recording and alerting rules.\nThe provider specifies one or more scrape compatible metrics endpoints, and rule files.\nThe requirer then scrapes the provider's metrics and emits alerts per rule files.", + "tags": [ + "observability" + ], "status": "published" }, { @@ -517,6 +651,7 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/pyroscope_cluster/", "summary": "Pyroscope coordinator and worker charm communication.", "description": "The `pyroscope_cluster` interface supports communication between the [pyroscope-coordinator-k8s](https://charmhub.io/pyroscope-coordinator-k8s) and [pyroscope-worker-k8s](https://charmhub.io/pyroscope-worker-k8s) charms.", + "tags": [], "status": "draft" }, { @@ -527,6 +662,7 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/s3/", "summary": "", "description": "", + "tags": [], "status": "draft" }, { @@ -537,6 +673,9 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/saml/", "summary": "Authenticate users with SAML.", "description": "The `saml` interface allows charms to connect to a SAML Identity Provider for authenticating users.\nThe provider charm is the SAML Identity Provider, and shares the necessary data with requirer charms (SAML Service Providers).", + "tags": [ + "identity-auth" + ], "status": "published" }, { @@ -547,6 +686,9 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/sdcore_config/", "summary": "", "description": "", + "tags": [ + "telco" + ], "status": "draft" }, { @@ -557,6 +699,9 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/sdcore_management/", "summary": "", "description": "", + "tags": [ + "telco" + ], "status": "draft" }, { @@ -567,6 +712,9 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/smtp/", "summary": "Connect to a SMTP server.", "description": "The `smtp` interface allows charms to connect to a SMTP server.\nThe provider charm provides the SMTP details so that the requirer can connect.", + "tags": [ + "operations" + ], "status": "published" }, { @@ -577,6 +725,7 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/spark_service_account/", "summary": "", "description": "", + "tags": [], "status": "draft" }, { @@ -587,6 +736,7 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/tempo_cluster/", "summary": "Tempo coordinator and worker charm communication.", "description": "The `tempo_cluster` interface supports communication between the [tempo-coordinator-k8s](https://charmhub.io/tempo-coordinator-k8s) and [tempo-worker-k8s](https://charmhub.io/tempo-worker-k8s) charms.", + "tags": [], "status": "published" }, { @@ -597,6 +747,9 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/tls-certificates/", "summary": "Securely request TLS certificates.", "description": "The `tls-certificates` interface allows charms to securely request and receive TLS certificates. The requirer charm is responsible for its private key and defining its certificate signing requests (CSRs). The provider charm delivers the certificate for each request, including the CA chain and CA certificate.", + "tags": [ + "security" + ], "status": "published" }, { @@ -607,6 +760,9 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/tracing/", "summary": "Request endpoints for pushing trace data.", "description": "The `tracing` interface supports push-based tracing.\nThe requirer publishes the list of protocols it wants to use to send traces.\nThe provider replies with an API URL and an endpoint for each protocol, omitting those it does not support.", + "tags": [ + "observability" + ], "status": "published" }, { @@ -617,6 +773,9 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/valkey_client/", "summary": "Access Valkey databases with credentials and TLS configuration.", "description": "The `valkey_client` interface allows charms to access Valkey databases with user-specific credentials and TLS configuration.\nThe provider charm creates Valkey users and permissions based on requirer requests, while the requirer charm shares the desired key access prefix.", + "tags": [ + "data" + ], "status": "published" }, { @@ -627,6 +786,7 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/vault-autounseal/", "summary": "", "description": "", + "tags": [], "status": "draft" }, { @@ -637,6 +797,9 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/vault-kv/", "summary": "Securely share Vault credentials to interact with the Vault Key-Value (KV) backend.", "description": "The `vault-kv` interface allows the charms to securely request and receive the credentials needed to interact with the Key-Value (KV) backend of Vault.\nThe requirer charm requests credentials, and the provider charm supplies them.", + "tags": [ + "security" + ], "status": "draft" }, { @@ -647,6 +810,7 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/velero_backup_config/", "summary": "Configure Kubernetes backup requirements for Velero.", "description": "The `velero_backup_config` interface allows client charms to specify Kubernetes backup requirements to a Velero Operator charm.\nThe requirer charm (client) provides backup specifications including namespaces, resource types, and retention policies, while the provider charm (Velero Operator) receives these specifications and uses them to execute backup operations.", + "tags": [], "status": "draft" }, { @@ -657,6 +821,7 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/wazuh_api_client/", "summary": "The `wazuh_api_client` interface allows charms to connect to a Wazuh server.\nThe provider charm manages access to the server, securely providing credentials and endpoints to the requirer charms.", "description": "Connect to a Wazuh server.", + "tags": [], "status": "draft" }, { @@ -667,6 +832,7 @@ "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/zookeeper/", "summary": "", "description": "", + "tags": [], "status": "draft" } ] diff --git a/justfile b/justfile index 241242ea4..23f5f54e9 100644 --- a/justfile +++ b/justfile @@ -184,6 +184,7 @@ interfaces-json: --output docs_url \ --output summary \ --output description \ + --output tags \ --output status \ --indent-json \ > interfaces/index.json From 37fb8ad230ed14c1f026351b84a3c70017bfe5e1 Mon Sep 17 00:00:00 2001 From: James Garner Date: Wed, 3 Jun 2026 16:44:01 +1200 Subject: [PATCH 33/65] docs: fix typo in readme (#514) This PR fixes a typo in the repository readme. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e9d1762b1..9391d0d26 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Charms are Python programs that use the [Ops](https://documentation.ubuntu.com/ops/) framework to manage workloads on Kubernetes or machine clouds. Charm libraries package up common functionality so that teams don't have to reinvent the wheel. > [!IMPORTANT] -> Each library in this monorepo is distributed as a separate Python package on PyPI, so you charms only include what they actually need. +> Each library in this monorepo is distributed as a separate Python package on PyPI, so charms only include what they actually need. There are two kinds of charm libraries: From 52a55e72a738528fa69dc422be079134359aa14b Mon Sep 17 00:00:00 2001 From: swetha1654 Date: Wed, 3 Jun 2026 16:58:23 +0530 Subject: [PATCH 34/65] feat: Cache private key in TLS library (#511) Addresses: #512 --- interfaces/tls-certificates/CHANGELOG.md | 4 ++++ .../interfaces/tls_certificates/_tls_certificates.py | 11 +++++++++-- .../charmlibs/interfaces/tls_certificates/_version.py | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/interfaces/tls-certificates/CHANGELOG.md b/interfaces/tls-certificates/CHANGELOG.md index a68155144..fa25d54ae 100644 --- a/interfaces/tls-certificates/CHANGELOG.md +++ b/interfaces/tls-certificates/CHANGELOG.md @@ -1,3 +1,7 @@ +# 1.8.2 - 3 June 2026 + +Update the library to cache the `private_key`. + # 1.8.1 - 27 February 2025 Cleanup secrets when relation is removed. diff --git a/interfaces/tls-certificates/src/charmlibs/interfaces/tls_certificates/_tls_certificates.py b/interfaces/tls-certificates/src/charmlibs/interfaces/tls_certificates/_tls_certificates.py index dee1d049c..90d199694 100644 --- a/interfaces/tls-certificates/src/charmlibs/interfaces/tls_certificates/_tls_certificates.py +++ b/interfaces/tls-certificates/src/charmlibs/interfaces/tls_certificates/_tls_certificates.py @@ -1837,6 +1837,7 @@ def __init__( "Invalid renewal relative time. Must be between 0.5 and 1.0" ) self._private_key = private_key + self._private_key_cache: dict[Mode, PrivateKey | None] = {} self.renewal_relative_time = renewal_relative_time self.framework.observe(charm.on[relationship_name].relation_created, self._configure) self.framework.observe(charm.on[relationship_name].relation_changed, self._configure) @@ -2010,13 +2011,17 @@ def _get_mode_and_private_key( def _get_private_key_for_mode(self, mode: Literal[Mode.APP, Mode.UNIT]) -> PrivateKey | None: if self._private_key: return self._private_key + if mode in self._private_key_cache: + return self._private_key_cache[mode] if mode == Mode.APP and not self.model.unit.is_leader(): logger.warning("Only the leader can access the private key in APP mode") return None try: secret = self.charm.model.get_secret(label=self._get_private_key_secret_label(mode)) - private_key = secret.get_content(refresh=True)["private-key"] - return PrivateKey.from_string(private_key) + private_key_str = secret.get_content(refresh=True)["private-key"] + private_key = PrivateKey.from_string(private_key_str) + self._private_key_cache[mode] = private_key + return private_key except (SecretNotFoundError, KeyError): return None @@ -2398,6 +2403,7 @@ def _generate_private_key(self, mode: Literal[Mode.UNIT, Mode.APP]) -> None: def _store_private_key_in_secret( self, private_key: PrivateKey, mode: Literal[Mode.UNIT, Mode.APP] ) -> None: + self._private_key_cache.pop(mode, None) app_or_unit = self._get_app_or_unit_for_mode(mode) try: secret = self.charm.model.get_secret(label=self._get_private_key_secret_label(mode)) @@ -2411,6 +2417,7 @@ def _store_private_key_in_secret( def _remove_private_key_secret(self, mode: Literal[Mode.UNIT, Mode.APP]) -> None: """Remove the private key secret.""" + self._private_key_cache.pop(mode, None) if mode == Mode.APP and not self.model.unit.is_leader(): logger.debug("Not leader, cannot remove app owned private key secret") return diff --git a/interfaces/tls-certificates/src/charmlibs/interfaces/tls_certificates/_version.py b/interfaces/tls-certificates/src/charmlibs/interfaces/tls_certificates/_version.py index b6c57fa17..ae8fece9b 100644 --- a/interfaces/tls-certificates/src/charmlibs/interfaces/tls_certificates/_version.py +++ b/interfaces/tls-certificates/src/charmlibs/interfaces/tls_certificates/_version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.8.1" +__version__ = "1.8.2" From c8fad75c49f9c9fd056125a82197cef07d10d08a Mon Sep 17 00:00:00 2001 From: James Garner Date: Fri, 5 Jun 2026 17:55:15 +1200 Subject: [PATCH 35/65] docs: drop block quotes for links (#493) This PR updates the docs to avoid using block quotes for links, and updates our `AGENTS.md`'s documentation section accordingly. In future we should look into how best to ensure that guidance for agents in this repository follows our team style guide. Resolves #494 --- .docs/explanation/charm-libs.md | 4 +--- .docs/explanation/charmlibs-tests.md | 4 ++-- .docs/how-to/customize-functional-tests.md | 2 +- .docs/how-to/customize-integration-tests.md | 2 +- .docs/how-to/manage-libraries.md | 4 +--- .docs/how-to/migrate.md | 4 ++-- .docs/how-to/python-package.md | 2 +- .docs/tutorial.md | 17 +++++++++-------- .package/README.md | 2 +- AGENTS.md | 8 ++++++++ interfaces/.package/README.md | 2 +- 11 files changed, 28 insertions(+), 23 deletions(-) diff --git a/.docs/explanation/charm-libs.md b/.docs/explanation/charm-libs.md index 90136e19c..31924ec01 100644 --- a/.docs/explanation/charm-libs.md +++ b/.docs/explanation/charm-libs.md @@ -39,9 +39,7 @@ The `lib/` directory is added to the [PYTHONPATH](https://docs.python.org/3/usin Charmhub-hosted libraries are typically committed to your charm's version control. -> Read more: -> - {ref}`manage-charmhub-libraries` -> - {ref}`Charmcraft | Manage libraries ` +Read more: {ref}`manage-charmhub-libraries`, {ref}`Charmcraft | Manage libraries ` (charm-libs-purpose)= ## Library purpose diff --git a/.docs/explanation/charmlibs-tests.md b/.docs/explanation/charmlibs-tests.md index 4a4d74960..2f4b0bfc4 100644 --- a/.docs/explanation/charmlibs-tests.md +++ b/.docs/explanation/charmlibs-tests.md @@ -49,7 +49,7 @@ Before running `pytest`, `just functional ` will first source `/tests/functional/teardown.sh` is sourced. Finally, the recipe exits with the return code of the `pytest` run. -> Read more: {ref}`how-to-customize-functional-tests` +Read more: {ref}`how-to-customize-functional-tests` (charmlibs-integration-tests)= ## Integration tests @@ -68,4 +68,4 @@ These will run the tests under `/tests/integration`, selecting either a You'll need to have a Juju controller set up locally to run your integration tests. In CI, Juju is set up for you by [concierge](https://github.com/canonical/concierge). -> Read more: {ref}`how-to-customize-integration-tests` +Read more: {ref}`how-to-customize-integration-tests` diff --git a/.docs/how-to/customize-functional-tests.md b/.docs/how-to/customize-functional-tests.md index b1e6ac696..c924e8fc0 100644 --- a/.docs/how-to/customize-functional-tests.md +++ b/.docs/how-to/customize-functional-tests.md @@ -3,7 +3,7 @@ Functional tests are executed much like your unit tests, but they provide more entry points for customisation to accommodate the need to interact with external processes. -> Read more: {ref}`charmlibs-tests` +Read more: {ref}`charmlibs-tests` Functional tests are only executed if the `/tests/functional` directory exists. If you don't need functional tests, remove the directory. diff --git a/.docs/how-to/customize-integration-tests.md b/.docs/how-to/customize-integration-tests.md index 6cc338b2a..f482a2340 100644 --- a/.docs/how-to/customize-integration-tests.md +++ b/.docs/how-to/customize-integration-tests.md @@ -5,7 +5,7 @@ In its integration tests, your library will be packed into one or more charms, t You can customise your integration tests in several ways, according to the needs of your library. You'll determine whether it makes sense to test on both K8s and machine clouds, with one or more charms, and whether to rerun the integration tests with other permutations. -> Read more: {ref}`charmlibs-tests` +Read more: {ref}`charmlibs-tests` Integration tests are only executed if the `/tests/integration` directory exists. If you don't need integration tests, remove the directory. diff --git a/.docs/how-to/manage-libraries.md b/.docs/how-to/manage-libraries.md index de3c1b2ff..307954119 100644 --- a/.docs/how-to/manage-libraries.md +++ b/.docs/how-to/manage-libraries.md @@ -125,6 +125,4 @@ charm-libs: version: "0" ``` -> Read more: -> - {ref}`charm-libs-charmhub-hosted` -> - {ref}`Charmcraft | Manage libraries ` +Read more: {ref}`charm-libs-charmhub-hosted`, {ref}`Charmcraft | Manage libraries ` diff --git a/.docs/how-to/migrate.md b/.docs/how-to/migrate.md index cdd2dfb43..b0f37863f 100644 --- a/.docs/how-to/migrate.md +++ b/.docs/how-to/migrate.md @@ -234,13 +234,13 @@ They're intended for tests that interact with the real world, but don't require The process for migrating them is exactly the same as for unit tests. -> Read more: {ref}`tutorial-add-functional-tests`, {ref}`charmlibs-functional-tests` +Read more: {ref}`tutorial-add-functional-tests`, {ref}`charmlibs-functional-tests` ### Integration tests Integration tests involve packing your library into a charm and deploying it on a real Juju model. -> Read more: {ref}`tutorial-add-integration-tests`, {ref}`charmlibs-integration-tests` +Read more: {ref}`tutorial-add-integration-tests`, {ref}`charmlibs-integration-tests` If you take a look at your `/tests/integration` directory, you'll see a `pack.sh` script. Currently it packs a simple `k8s` or `machine` charm, depending on the `CHARMLIBS_SUBSTRATE` variable that is set in CI. diff --git a/.docs/how-to/python-package.md b/.docs/how-to/python-package.md index 09ffbc43e..cc362bd19 100644 --- a/.docs/how-to/python-package.md +++ b/.docs/how-to/python-package.md @@ -58,7 +58,7 @@ It's also very useful when developing a new library or porting a Charmhub-hosted You don't need to do anything special to make your Python package installable with `git` -- just commit it and push to your repository as usual. -> Read more: {ref}`manage-git-dependencies` +Read more: {ref}`manage-git-dependencies` (python-package-distribution-local)= ### Local files diff --git a/.docs/tutorial.md b/.docs/tutorial.md index 88f53f0f3..e36d187b4 100644 --- a/.docs/tutorial.md +++ b/.docs/tutorial.md @@ -40,10 +40,11 @@ If you want to run the Juju integration tests locally, you'll also need `charmcr In CI, these are installed and set up for you using [concierge](https://github.com/canonical/concierge?tab=readme-ov-file#presets), with the `microk8s` and `machine` presets. The `dev` preset is suitable for local development and testing of both K8s and machine charms, but you may find it easier to run the Juju integration tests in CI when following this tutorial. -> See more: -> - {ref}`Charmcraft | Install charmcraft ` -> - {ref}`Juju | Set up your Juju deployment ` -> - {ref}`Juju | Set up an isolated environment ` +Read more: + +- {ref}`Charmcraft | Install charmcraft ` +- {ref}`Juju | Set up your Juju deployment ` +- {ref}`Juju | Set up an isolated environment ` If you're not already using some alternative authentication for `git push`, you'll also want to [make sure you've set up your Github account with your SSH key](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account). @@ -176,7 +177,7 @@ The `charmlibs` monorepo supports three distinct types of tests: unit, functiona The template starts you off with a simple passing test for each. We'll add a test of each kind for our `uptime` library in the following sections. -> Read more: {ref}`charmlibs-tests` +Read more: {ref}`charmlibs-tests` ### Add unit tests @@ -251,7 +252,7 @@ In this repository, functional tests are essentially integration or end-to-end t In contrast to unit tests, which typically mock out external concerns, functional tests interact with real systems, external processes, and networks. However, they do not interact with a real Juju environment, which is reserved for tests under the `integration/` directory. -> Read more: {ref}`how-to-customize-functional-tests` +Read more: {ref}`how-to-customize-functional-tests` ````{tip} If you're working on an interface library, functional tests probably aren't a good fit, since the main thing the library interacts with is Juju. @@ -288,7 +289,7 @@ Integration tests are the most complicated and most heavyweight part of the libr They involve packing a real charm that includes your library, and deploying it on a real Juju model. They can be customized in several ways depending on your library's needs. -> Read more: {ref}`how-to-customize-integration-tests` +Read more: {ref}`how-to-customize-integration-tests` For now, we'll just add an integration test for our `uptime` function. @@ -384,4 +385,4 @@ Their review will cover whether the name and purpose of the library is appropria This type of review will be repeated for major version bumps of your library. All other releases will only require `CODEOWNERS` approval. -> Read more: {ref}`charmlibs-publishing` +Read more: {ref}`charmlibs-publishing` diff --git a/.package/README.md b/.package/README.md index 629ebd043..0ae25af06 100644 --- a/.package/README.md +++ b/.package/README.md @@ -4,7 +4,7 @@ This package should not be installed - it exists solely to reserve the `charmlib This namespace is for packages designed to be used by [Juju charms](https://canonical.com/juju/charms-architecture). Visit the [charmlibs documentation site](https://canonical-charmlibs.readthedocs-hosted.com/) for more information about charm libraries. -See more: +Read more: - Get started with charm development, using [Charmcraft tutorials](https://documentation.ubuntu.com/charmcraft/stable/tutorial/) and [Ops tutorials](https://documentation.ubuntu.com/ops/latest/tutorial/) - Learn how to [use and write charm libraries](https://documentation.ubuntu.com/ops/latest/howto/manage-libraries/) diff --git a/AGENTS.md b/AGENTS.md index af197f8b7..684cc9926 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -222,6 +222,14 @@ This is also run as part of `just check`. The full docs site (all packages) is b When writing or editing docstrings in `__init__.py` or other public modules, remember they appear verbatim in the published reference at [documentation.ubuntu.com/charmlibs](https://documentation.ubuntu.com/charmlibs). Keep them informative for library users, not implementation notes. +Don't use block quotes (`>`) for "Read more", "See also", or similar cross-reference sections. Instead, use bare text like `Read more: {ref}\`some-page\`` or, for two links, a comma-separated list or, for three or more links a bulleted list. For example: + +``` +Read more: {ref}`how-to-customize-integration-tests` + +Read more: {ref}`charm-libs-charmhub-hosted`, {ref}`Charmcraft | Manage libraries ` +``` + ## Common pitfalls - **Don't call `uv add` directly** — use `just add ` to respect repo-level constraints. diff --git a/interfaces/.package/README.md b/interfaces/.package/README.md index 926155756..a47bd79ee 100644 --- a/interfaces/.package/README.md +++ b/interfaces/.package/README.md @@ -4,7 +4,7 @@ This package should not be installed - it exists solely to reserve the `charmlib This namespace is for packages designed to be used by [Juju charms](https://canonical.com/juju/charms-architecture) to implement [relations](https://documentation.ubuntu.com/ops/latest/howto/manage-relations/), known as charm interface libraries. Read the [charmlibs documentation](https://documentation.ubuntu.com/charmlibs) for more information about charm libraries. -See more: +Read more: - Get started with charm development, using [Charmcraft tutorials](https://documentation.ubuntu.com/charmcraft/stable/tutorial/) and [Ops tutorials](https://documentation.ubuntu.com/ops/latest/tutorial/) - Learn how to [use and write charm libraries](https://documentation.ubuntu.com/ops/latest/howto/manage-libraries/) From 0c3bef14a38d2482012817d5820fa0cf3bf470ac Mon Sep 17 00:00:00 2001 From: James Garner Date: Fri, 5 Jun 2026 18:42:45 +1200 Subject: [PATCH 36/65] docs: diataxis package docs (#504) This PR adds docs infra to build diataxis docs for each package (tutorial, how-to, and explanation -- reference docs are already built). It also adds a docs page explaining how library authors can do this. To exercise this machinery, and because it would be nice to improve the `pathops` documentation, this PR also includes a tutorial and explanation doc for `pathops`. --------- Co-authored-by: Dave Wilding --- .docs/.gitignore | 3 + .docs/conf.py | 3 +- .docs/explanation/index.md | 3 + .docs/extensions/README | 7 +- .docs/extensions/diataxis_docs_fallback.py | 46 ++++ .docs/how-to/add-library-docs.md | 115 ++++++++ .docs/how-to/index.md | 4 + .docs/how-to/migrate.md | 56 +++- .docs/index.md | 8 +- .docs/scripts/diataxis_preprocessor.py | 209 +++++++++++++++ .docs/tests/conftest.py | 26 ++ .docs/tests/pyproject.toml | 5 + .docs/tests/test_diataxis_docs_fallback.py | 62 +++++ .docs/tests/test_diataxis_preprocessor.py | 251 ++++++++++++++++++ .../test_generate_tables.py | 0 .docs/tutorial.md | 4 +- .docs/tutorials/index.md | 10 + .../a-charmlibs-example-explanation.md | 17 ++ .../docs/how-to/a-charmlibs-example-how-to.md | 17 ++ .example/docs/tutorial.md | 15 ++ .github/workflows/ci.yaml | 10 + .scripts/import_discourse_docs.py | 99 +++++++ .scripts/ls.py | 32 ++- .scripts/tests/conftest.py | 25 ++ .scripts/tests/test_import_discourse_docs.py | 93 +++++++ ... cookiecutter.__dist_pkg }}-explanation.md | 17 ++ .../a-{{ cookiecutter.__dist_pkg }}-how-to.md | 17 ++ .../docs/tutorial.md | 15 ++ AGENTS.md | 9 + docs.just | 17 +- ...nterfaces-example-interface-explanation.md | 17 ++ ...ibs-interfaces-example-interface-how-to.md | 17 ++ interfaces/.example/docs/tutorial.md | 15 ++ justfile | 5 + pathops/docs/explanation/design.rst | 41 +++ pathops/docs/tutorial.md | 102 +++++++ 36 files changed, 1364 insertions(+), 28 deletions(-) create mode 100644 .docs/extensions/diataxis_docs_fallback.py create mode 100644 .docs/how-to/add-library-docs.md create mode 100755 .docs/scripts/diataxis_preprocessor.py create mode 100644 .docs/tests/conftest.py create mode 100644 .docs/tests/pyproject.toml create mode 100644 .docs/tests/test_diataxis_docs_fallback.py create mode 100644 .docs/tests/test_diataxis_preprocessor.py rename .docs/{extensions => tests}/test_generate_tables.py (100%) create mode 100644 .docs/tutorials/index.md create mode 100644 .example/docs/explanation/a-charmlibs-example-explanation.md create mode 100644 .example/docs/how-to/a-charmlibs-example-how-to.md create mode 100644 .example/docs/tutorial.md create mode 100755 .scripts/import_discourse_docs.py create mode 100644 .scripts/tests/conftest.py create mode 100644 .scripts/tests/test_import_discourse_docs.py create mode 100644 .template/{{ cookiecutter.project_slug }}/docs/explanation/a-{{ cookiecutter.__dist_pkg }}-explanation.md create mode 100644 .template/{{ cookiecutter.project_slug }}/docs/how-to/a-{{ cookiecutter.__dist_pkg }}-how-to.md create mode 100644 .template/{{ cookiecutter.project_slug }}/docs/tutorial.md create mode 100644 interfaces/.example/docs/explanation/a-charmlibs-interfaces-example-interface-explanation.md create mode 100644 interfaces/.example/docs/how-to/a-charmlibs-interfaces-example-interface-how-to.md create mode 100644 interfaces/.example/docs/tutorial.md create mode 100644 pathops/docs/explanation/design.rst create mode 100644 pathops/docs/tutorial.md diff --git a/.docs/.gitignore b/.docs/.gitignore index 302698aaf..6d8dd1dcb 100644 --- a/.docs/.gitignore +++ b/.docs/.gitignore @@ -28,5 +28,8 @@ __pycache__ charmlibs/ interfaces/ generated/ + +# generated per-library diataxis include files +_lib-*.md .~lock.* .save/ diff --git a/.docs/conf.py b/.docs/conf.py index 250994cd2..e5b1058b7 100644 --- a/.docs/conf.py +++ b/.docs/conf.py @@ -4,7 +4,7 @@ # local extensions sys.path.insert(0, str(pathlib.Path(__file__).parent / 'extensions')) -local_extensions = ['generate_tables', 'interface_docs', 'package_docs'] +local_extensions = ['generate_tables', 'interface_docs', 'package_docs', 'diataxis_docs_fallback'] # So that sphinx.ext.autodoc can find charmlibs code root = pathlib.Path(__file__).parent.parent @@ -162,6 +162,7 @@ # Excludes files or directories from processing exclude_patterns = [ "doc-cheat-sheet*", + "**/_lib-*.md", ] # Adds custom CSS files, located under 'html_static_path' diff --git a/.docs/explanation/index.md b/.docs/explanation/index.md index 3a96f9533..3e9be3d6b 100644 --- a/.docs/explanation/index.md +++ b/.docs/explanation/index.md @@ -8,3 +8,6 @@ charmhub-libraries-deprecation charmlibs: Types of tests charmlibs: Publishing packages ``` + +```{include} _lib-explanation.md +``` diff --git a/.docs/extensions/README b/.docs/extensions/README index d70c9208c..aad1e1e34 100644 --- a/.docs/extensions/README +++ b/.docs/extensions/README @@ -1,5 +1,6 @@ -Directory for local sphinx extensions. +Directory for local Sphinx extensions. -This directory is referenced in the docs `conf.py`, and in the `docs.just` file (for running unit tests). +This directory is referenced in the docs `conf.py`. Modules must be manually +added to `local_extensions` in `conf.py` for Sphinx to detect them. -Modules must be manually added to `local_extensions` in `conf.py` for Sphinx to detect them. +Tests live in `../tests/`. Run them with `just docs ext-unit`. diff --git a/.docs/extensions/diataxis_docs_fallback.py b/.docs/extensions/diataxis_docs_fallback.py new file mode 100644 index 000000000..3665c380e --- /dev/null +++ b/.docs/extensions/diataxis_docs_fallback.py @@ -0,0 +1,46 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Sphinx fallback extension for per-library diataxis docs. + +The ``diataxis_preprocessor.py`` script (run by ``just docs``) copies library +docs into the Sphinx source tree and generates ``_lib-*.md`` include files. + +This extension provides a fallback: if any of those include files don't exist +— either because the preprocessor wasn't run, or because no libraries have docs +for a given category — it writes empty versions so that the ``{include}`` +directives in the category index pages resolve without errors. +""" + +from __future__ import annotations + +import pathlib +import typing + +if typing.TYPE_CHECKING: + import sphinx.application + + +def setup(app: sphinx.application.Sphinx) -> dict[str, str | bool]: + """Sphinx extension entrypoint — registers the fallback hook.""" + app.connect('builder-inited', _fallback) + return {'version': '2.0.0', 'parallel_read_safe': False, 'parallel_write_safe': False} + + +def _fallback(app: sphinx.application.Sphinx) -> None: + docs_dir = pathlib.Path(app.confdir) + for category in 'tutorials', 'how-to', 'explanation': + path = docs_dir / category / f'_lib-{category}.md' + if not path.exists(): + path.write_text('') diff --git a/.docs/how-to/add-library-docs.md b/.docs/how-to/add-library-docs.md new file mode 100644 index 000000000..099270268 --- /dev/null +++ b/.docs/how-to/add-library-docs.md @@ -0,0 +1,115 @@ +--- +myst: + html_meta: + description: Add tutorials, how-to guides, and explanations to your charmlibs library. +--- + +(how-to-add-library-docs)= +# How to add docs to a library + +Each library in the charmlibs monorepo can include its own documentation — tutorials, how-to guides, and explanations — which are automatically included in this very documentation site. + +This guide shows you how to add docs to your library package. + +## Create the docs directory + +Create a `docs/` directory in your library's root: + +```text +mylib/ +├── docs/ +│ ├── tutorial.md +│ ├── how-to/ +│ │ ├── deploy.md +│ │ └── configure.md +│ └── explanation/ +│ └── architecture.md +├── src/ +├── tests/ +└── pyproject.toml +``` + +The three supported categories are: + +| Category | Location | Notes | +|---|---|---| +| Tutorial | `docs/tutorial.md` or `docs/tutorial.rst` | One file only | +| How-to guides | `docs/how-to/*.md` or `docs/how-to/*.rst` | Multiple files | +| Explanations | `docs/explanation/*.md` or `docs/explanation/*.rst` | Multiple files | + +Only these categories are discovered. Other directories under `docs/` are ignored. Within `how-to/` and `explanation/`, only files directly in the directory are picked up — nested subdirectories are not discovered. + +```{note} +There is no `reference` category. Reference documentation is generated automatically from your library's docstrings, so a `docs/reference/` directory is ignored. Put hand-written conceptual material in an explanation instead. +``` + +Each doc can be written in Markdown (`.md`) or reStructuredText (`.rst`). See the [MyST syntax guide](https://documentation.ubuntu.com/sphinx-stack/latest/reference/myst-syntax/) and the [reStructuredText syntax guide](https://documentation.ubuntu.com/sphinx-stack/latest/reference/rst-syntax/) for the available markup. + +## Give each doc a title + +Each doc file must start with a top-level heading: + +````markdown +# Getting Started + +In this tutorial, you'll learn how to ... +```` + +The heading text is used as the page title in the docs site, prefixed with your library name. For example, a tutorial in `pathops/docs/tutorial.md` with heading `# Getting Started` will appear as **pathops: Getting Started** in the site navigation. + +## Add a meta description + +Each doc should have a short meta description for search engines and link previews. In a Markdown file, add a `description` to the front matter, before the top-level heading: + +````markdown +--- +myst: + html_meta: + description: Manage files in a charm workload with pathops. +--- + +# Getting Started +```` + +In a reStructuredText file, use the `meta` directive instead: + +```rst +.. meta:: + :description: Manage files in a charm workload with pathops. + +Getting Started +=============== +``` + +## Link to other docs + +Use relative links to reference other files in your library or in the repo. The build system rewrites these links automatically: + +- Links to other docs files (tutorials, how-to guides, explanations, or `.docs/` pages) become internal Sphinx links. +- Links to any other repo files (source code, READMEs, etc.) become GitHub links pointing to the `main` branch. + +For example, from `docs/how-to/deploy.md`: + +```markdown +See the [architecture overview](../explanation/architecture.md) for background. + +Check the [source code](../../src/charmlibs/mylib/_impl.py) for details. +``` + +## Build and preview + +To build the docs and verify your changes: + +```bash +just docs html mylib +``` + +This runs the preprocessor (which copies your docs into the Sphinx source tree) and builds the site. Your docs will appear under the corresponding category pages. + +Listing a package only limits which packages get their reference docs generated. The preprocessor always copies the Diátaxis docs for *every* package, so tutorials, how-to guides, and explanations from other libraries will still appear in the build. + +To build docs for all packages: + +```bash +just docs +``` diff --git a/.docs/how-to/index.md b/.docs/how-to/index.md index 51cee4698..4960b4762 100644 --- a/.docs/how-to/index.md +++ b/.docs/how-to/index.md @@ -8,6 +8,10 @@ Manage charm libraries Distribute charm libraries Migrate to the charmlibs monorepo Provide relation data for charm tests +charmlibs: Add docs to a library charmlibs: Customize functional tests charmlibs: Customize integration tests ``` + +```{include} _lib-how-to.md +``` diff --git a/.docs/how-to/migrate.md b/.docs/how-to/migrate.md index b0f37863f..3912c84fb 100644 --- a/.docs/how-to/migrate.md +++ b/.docs/how-to/migrate.md @@ -258,27 +258,57 @@ The `charmlibs` CI is aware of two special `pytest` marks: `k8s_only` and `machi If there are no tests compatible with a substrate, then it's skipped completely. By default each test is treated as compatible with both substrates. +(how-to-migrate-docs)= ## Migrate your library's docs -Your library's reference documentation is automatically built from its docstrings and source code. +Your library's reference documentation is automatically built from its docstrings and source code. On top of that, you can add tutorials, how-to guides, and explanations under `/docs`, and they'll be included in this documentation site under the respective categories. -```{warning} -🚧 Actually including the docs described below in this documentation site is coming soon™ 🚧 +Read more: {ref}`how-to-add-library-docs` + +```{note} +There is no `reference` docs category -- reference documentation is generated from your library's docstrings. If your existing docs include hand-written reference material, fold the relevant content into your docstrings, or rework it into an explanation or how-to guide. ``` -Additional documentation may be placed under `/docs`, and will be included in this documentation site under the respective categories. -The following files are consumed. +If your library already has documentation hosted on Charmhub, here's how to bring it across. + +### Find the source topics + +Charmhub renders a charm's documentation from a set of topics on [Discourse](https://discourse.charmhub.io). To find them: + +1. Start from the charm's Charmhub page -- `https://charmhub.io/`. +2. Follow the **Help improve this document in the forum** link at the bottom of the page. This link points to the Discourse *root topic* for the charm's docs. + +The root topic contains a table of contents linking each child topic. This usually already encodes a Diátaxis-like layout (tutorial / how-to / explanation / reference) that you can carry over. + +```{tip} +Some charms host their docs on a Read the Docs / Sphinx site instead of Discourse. That content isn't available through the Discourse API, but it's straightforward to migrate by hand: copy the doc site's source files into your library's `docs/` directory and adapt them. +``` + +### Download the topics + +Download each Discourse topic into your library's `docs/` directory: + +```bash +.scripts/import_discourse_docs.py /docs//.md ``` -docs -├── explanation -│   └── *.{md,rst} -├── how-to -│   └── *.{md,rst} -├── reference -│   └── *.{md,rst} -└── tutorial.{md,rst} + +For example: + +```bash +.scripts/import_discourse_docs.py \ + https://discourse.charmhub.io/t/tls-certificates-interface/15539 \ + interfaces/tls-certificates/docs/explanation/tls-certificates-interface.md ``` + +The script downloads the topic, extracts its Markdown source, and resolves any embedded images. + +Repeat for each topic, choosing a Diátaxis category for each. + +### Finish up + +Edit the imported pages to follow the conventions described in {ref}`how-to-add-library-docs`: give each page a title and meta description, fix up cross-references, and confirm the categorisation. + ## Update the library metadata The {ref}`interface library listing ` and {ref}`general library listing ` are generated from `.docs/reference/libs.yaml`. diff --git a/.docs/index.md b/.docs/index.md index 03061e7a5..31663a2db 100644 --- a/.docs/index.md +++ b/.docs/index.md @@ -8,7 +8,7 @@ relatedlinks: "[Charmcraft](https://documentation.ubuntu.com/charmcraft/stable/) :maxdepth: 3 :hidden: false -tutorial +tutorials/index how-to/index reference/index explanation/index @@ -33,8 +33,8 @@ If you're new to charms, see {ref}`Juju | Charm `. ````{grid} 1 1 2 2 -```{grid-item-card} [Tutorial](tutorial) -**Start here:** Write your first charm library and contribute it to the monorepo. +```{grid-item-card} [Tutorials](tutorials/index) +**Start here:** [Write your first charm library](tutorial) and contribute it to the monorepo. ``` ```{grid-item-card} [How-to guides](how-to/index) @@ -53,7 +53,7 @@ If you're new to charms, see {ref}`Juju | Charm `. :reverse: ```{grid-item-card} [Reference](reference/index) -**Technical information** +**Technical information** - {ref}`General libraries ` and {ref}`interface libraries ` - {ref}`charmlibs package docs ` - {ref}`charmlibs.interfaces package docs ` diff --git a/.docs/scripts/diataxis_preprocessor.py b/.docs/scripts/diataxis_preprocessor.py new file mode 100755 index 000000000..f9a959433 --- /dev/null +++ b/.docs/scripts/diataxis_preprocessor.py @@ -0,0 +1,209 @@ +#!/usr/bin/env -S uv run --script --no-project + +# /// script +# requires-python = ">=3.12" +# /// + +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Copy per-library diataxis docs into the Sphinx source tree. + +Walks every package returned by ``.scripts/ls.py``, looks for a ``docs/`` +directory, and copies tutorial, how-to, and explanation pages into the +corresponding Sphinx source directories. Generates ``_lib-*.md`` include +files containing toctree entries that the index pages pull in via +``{include}``. + +Run from ``just docs``; see ``docs.just`` for the invocation. + +This is a standalone preprocessor script rather than a Sphinx extension. A +preprocessor step is simpler and more maintainable than an extension in +general, and it's a particularly good fit here: we want this work to run only +once (an extension would need extra machinery to enable/disable itself per +build, like the package reference docs do), and we need it to run very early +— before Sphinx has started building a docs tree of any kind. Copying the +source files and rewriting links is a one-time preparation of the source tree +that a final, straightforward Sphinx build then consumes. The companion +``diataxis_docs_fallback`` extension only writes empty ``_lib-*.md`` +placeholders when this script hasn't written anything, so the ``{include}`` +directives still resolve. +""" + +from __future__ import annotations + +import json +import pathlib +import re +import subprocess +from typing import Any + +_DOCS_DIR = pathlib.Path(__file__).parent.parent.resolve() +_REPO_ROOT = _DOCS_DIR.parent +_REPO_MAIN_URL = 'https://github.com/canonical/charmlibs/blob/main' +_TOCTREE_HEADER = """\ +```{toctree} +:maxdepth: 1 + +""" +_TOCTREE_FOOTER = '```\n' + + +def _main() -> None: + """Discover packages and copy their docs into the Sphinx source tree.""" + cmd = [ + _REPO_ROOT / '.scripts' / 'ls.py', + 'packages', + '--exclude-examples', + '--exclude-placeholders', + '--exclude-testing', + *('--output', 'path'), + *('--output', 'docs'), + ] + packages: list[dict[str, Any]] = json.loads(subprocess.check_output(cmd, text=True)) + sphinx_map = _build_sphinx_map(packages) + # Copy each package's docs and collect toctree entries for each category. + all_entries: dict[str, list[str]] = {} + for pkg in packages: + lib_path = pathlib.PurePath(pkg['path']) + docs: dict[str, list[str]] = pkg.get('docs', {}) + for category, doc_files in docs.items(): + sources = [_REPO_ROOT / lib_path / f for f in doc_files] + entries = _copy_category(sources, lib_path, category, sphinx_map) + all_entries.setdefault(category, []).extend(entries) + # Write include files with toctree entries for each category. + for category, entries in all_entries.items(): + if not entries: + continue + path = _DOCS_DIR / category / f'_lib-{category}.md' + content = _TOCTREE_HEADER + '\n'.join(sorted(entries)) + '\n' + _TOCTREE_FOOTER + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content) + + +def _copy_category( + sources: list[pathlib.Path], + lib_path: pathlib.PurePath, + category: str, + sphinx_map: dict[pathlib.PurePath, str], +) -> list[str]: + """Copy a library's docs for one category and return toctree entries.""" + if not sources: + return [] + out_dir = _DOCS_DIR / category / 'charmlibs' / lib_path + out_dir.mkdir(parents=True, exist_ok=True) + entries: list[str] = [] + for source in sources: + content = source.read_text() + title = _extract_h1(content, source) + content = _prefix_h1(content, lib_path.name, source.suffix) + content = _rewrite_links(content, source, sphinx_map) + (out_dir / source.name).write_text(content) + entries.append(f'{lib_path.name}: {title} ') + return entries + + +def _extract_h1(content: str, source: pathlib.Path) -> str: + """Extract the first H1 heading text from content.""" + if source.suffix == '.md': + match = re.search(r'^# (.+)$', content, re.MULTILINE) + else: + match = re.search(r'^(.+)\n(?:=+|-+)$', content, re.MULTILINE) + if not match: + raise ValueError(f'no title found in {source}') + return match.group(1).strip() + + +def _prefix_h1(content: str, lib_name: str, ext: str) -> str: + """Prepend the library name to the first H1 heading.""" + if ext == '.md': + return re.sub(r'^(# )', rf'\1{lib_name}: ', content, count=1, flags=re.MULTILINE) + + def _replace(m: re.Match[str]) -> str: + title = f'{lib_name}: {m.group(1)}' + return f'{title}\n{m.group(2)[0] * len(title)}' + + return re.sub(r'^(.+)\n(=+|-+)$', _replace, content, count=1, flags=re.MULTILINE) + + +def _build_sphinx_map(packages: list[dict[str, Any]]) -> dict[pathlib.PurePath, str]: + """Build a mapping from repo-relative file paths to Sphinx doc paths. + + Covers: + - ``.docs/`` pages (how-to, explanation, tutorials, reference, etc.) + - Per-library diataxis docs (tutorial, how-to/*, explanation/*) + - Interface version READMEs (``interfaces/{name}/interface/v{N}/README.md``) + """ + docs_rel = _DOCS_DIR.relative_to(_REPO_ROOT) + m: dict[pathlib.PurePath, str] = {} + + # Static .docs/ pages — walk all .md/.rst files (excluding build artifacts). + skip_dirs = {'_build', '.sphinx', '.save', 'scripts', 'tests', 'extensions'} + for path in _DOCS_DIR.rglob('*'): + if path.suffix not in ('.md', '.rst'): + continue + rel = path.relative_to(_DOCS_DIR) + if rel.parts[0] in skip_dirs or rel.name.startswith('_'): + continue + m[docs_rel / rel] = f'/{rel.with_suffix("")}' + + # Per-library diataxis docs + interface version READMEs. + for pkg in packages: + lib_path = pathlib.PurePath(pkg['path']) + docs: dict[str, list[str]] = pkg.get('docs', {}) + for category, doc_files in docs.items(): + for doc_rel in doc_files: + stem = pathlib.PurePath(doc_rel).stem + m[lib_path / doc_rel] = f'/{category}/charmlibs/{lib_path}/{stem}' + # Interface version READMEs (interfaces/{name}/interface/v{N}/README.md). + if lib_path.parent.name == 'interfaces': + interface_dir = _REPO_ROOT / lib_path / 'interface' + for readme in interface_dir.glob('v[0-9]*/README.md'): + m[readme.relative_to(_REPO_ROOT)] = ( + f'/reference/interfaces/{lib_path.name}/{readme.parent.name}' + ) + + return m + + +def _rewrite_links( + content: str, source_file: pathlib.Path, sphinx_map: dict[pathlib.PurePath, str] +) -> str: + """Rewrite markdown links: Sphinx paths for known docs, GitHub URLs otherwise.""" + + def _replace(m: re.Match[str]) -> str: + text = m.group(1) + raw_target = m.group(2) + # Split off any anchor fragment. + path_part, sep, raw_fragment = raw_target.partition('#') + fragment = sep + raw_fragment # either '' or '#something' + # Resolve relative to the source file's directory. + resolved = (source_file.parent / path_part).resolve() + repo_rel = resolved.relative_to(_REPO_ROOT) # ValueError if link goes above repo root + # Look up in the Sphinx map, falling back to GitHub URL. + url = sphinx_map.get(repo_rel, f'{_REPO_MAIN_URL}/{repo_rel}') + return f'[{text}]({url}{fragment})' + + relative_link = ( + r'\[(.+?)\]' # [link text] -- capture group 1 + r'\(' # ( + r'(?!https?://)' # not an absolute URL + r'([^)]+)' # relative target -- capture group 2 + r'\)' # ) + ) + return re.sub(relative_link, _replace, content) + + +if __name__ == '__main__': + _main() diff --git a/.docs/tests/conftest.py b/.docs/tests/conftest.py new file mode 100644 index 000000000..9d2291a10 --- /dev/null +++ b/.docs/tests/conftest.py @@ -0,0 +1,26 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Pytest configuration for docs tests. + +Adds ``extensions/`` and ``scripts/`` to sys.path so that Sphinx +extensions and the diataxis preprocessor can be imported from test files. +""" + +import pathlib +import sys + +_DOCS_DIR = pathlib.Path(__file__).resolve().parent.parent +sys.path.insert(0, str(_DOCS_DIR / 'extensions')) +sys.path.insert(0, str(_DOCS_DIR / 'scripts')) diff --git a/.docs/tests/pyproject.toml b/.docs/tests/pyproject.toml new file mode 100644 index 000000000..d870893a3 --- /dev/null +++ b/.docs/tests/pyproject.toml @@ -0,0 +1,5 @@ +[tool.pyright] +typeCheckingMode = "strict" +reportPrivateUsage = false +reportUnnecessaryTypeIgnoreComment = "error" +extraPaths = ["../extensions", "../scripts"] diff --git a/.docs/tests/test_diataxis_docs_fallback.py b/.docs/tests/test_diataxis_docs_fallback.py new file mode 100644 index 000000000..a01e6ab58 --- /dev/null +++ b/.docs/tests/test_diataxis_docs_fallback.py @@ -0,0 +1,62 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ruff: noqa: D103 (function docstrings) + +"""Unit tests for the diataxis_docs_fallback Sphinx extension.""" + +from __future__ import annotations + +import types +import typing + +import diataxis_docs_fallback as fallback + +if typing.TYPE_CHECKING: + import pathlib + + import sphinx.application + +CATEGORIES = ('tutorials', 'how-to', 'explanation') + + +def _app(confdir: pathlib.Path) -> sphinx.application.Sphinx: + """A minimal stand-in for the Sphinx app: ``_fallback`` only reads ``confdir``.""" + return typing.cast('sphinx.application.Sphinx', types.SimpleNamespace(confdir=str(confdir))) + + +def test_fallback_writes_empty_include_when_missing(tmp_path: pathlib.Path): + """Fallback writes empty include files when preprocessor hasn't run.""" + for category in CATEGORIES: + (tmp_path / category).mkdir() + + fallback._fallback(_app(tmp_path)) + + for category in CATEGORIES: + path = tmp_path / category / f'_lib-{category}.md' + assert path.exists() + assert path.read_text() == '' + + +def test_fallback_skips_when_include_exists(tmp_path: pathlib.Path): + """Fallback does nothing when preprocessor has already run.""" + for category in CATEGORIES: + (tmp_path / category).mkdir() + (tmp_path / category / f'_lib-{category}.md').write_text('existing') + + fallback._fallback(_app(tmp_path)) + + for category in CATEGORIES: + path = tmp_path / category / f'_lib-{category}.md' + assert path.read_text() == 'existing' diff --git a/.docs/tests/test_diataxis_preprocessor.py b/.docs/tests/test_diataxis_preprocessor.py new file mode 100644 index 000000000..453029107 --- /dev/null +++ b/.docs/tests/test_diataxis_preprocessor.py @@ -0,0 +1,251 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ruff: noqa: D103 (function docstrings) + +"""Unit tests for the diataxis preprocessor script.""" + +from __future__ import annotations + +import pathlib + +import diataxis_preprocessor as pp +import pytest + +# --- _build_sphinx_map --- + + +def test_build_sphinx_map_tutorial(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch): + """Tutorial files appear in the sphinx map.""" + monkeypatch.setattr(pp, '_REPO_ROOT', tmp_path) + monkeypatch.setattr(pp, '_DOCS_DIR', tmp_path / '.docs') + (tmp_path / '.docs').mkdir() + packages: list[dict[str, object]] = [ + {'path': 'mylib', 'docs': {'tutorials': ['docs/tutorial.md']}}, + ] + m = pp._build_sphinx_map(packages) + assert m[pathlib.PurePath('mylib/docs/tutorial.md')] == '/tutorials/charmlibs/mylib/tutorial' + + +def test_build_sphinx_map_interface_version_readme( + tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch +): + """Interface version READMEs map to /reference/interfaces/{name}/v{N}.""" + monkeypatch.setattr(pp, '_REPO_ROOT', tmp_path) + monkeypatch.setattr(pp, '_DOCS_DIR', tmp_path / '.docs') + (tmp_path / '.docs').mkdir() + v1_dir = tmp_path / 'interfaces' / 'tls-certificates' / 'interface' / 'v1' + v1_dir.mkdir(parents=True) + (v1_dir / 'README.md').touch() + packages: list[dict[str, object]] = [ + {'path': 'interfaces/tls-certificates', 'docs': {}}, + ] + m = pp._build_sphinx_map(packages) + assert ( + m[pathlib.PurePath('interfaces/tls-certificates/interface/v1/README.md')] + == '/reference/interfaces/tls-certificates/v1' + ) + + +# --- _extract_h1 --- + + +def test_extract_h1_md(): + assert pp._extract_h1('# Getting Started\n\nText.\n', pathlib.Path('tutorial.md')) == ( + 'Getting Started' + ) + + +def test_extract_h1_md_no_heading(): + with pytest.raises(ValueError, match=r'tutorial\.md'): + pp._extract_h1('No heading here.\n', pathlib.Path('tutorial.md')) + + +def test_extract_h1_rst(): + assert pp._extract_h1('My Title\n========\n\nText.\n', pathlib.Path('tutorial.rst')) == ( + 'My Title' + ) + + +def test_extract_h1_rst_no_heading(): + with pytest.raises(ValueError, match=r'tutorial\.rst'): + pp._extract_h1('No heading here.\n', pathlib.Path('tutorial.rst')) + + +# --- _prefix_h1 --- + + +def test_prefix_h1_md(): + content = '# Getting Started\n\nSome text.\n' + result = pp._prefix_h1(content, 'tls-certificates', '.md') + assert result.startswith('# tls-certificates: Getting Started\n') + + +def test_prefix_h1_md_preserves_rest(): + content = '# Title\n\n## Subtitle\n\nBody.\n' + result = pp._prefix_h1(content, 'mylib', '.md') + assert result == '# mylib: Title\n\n## Subtitle\n\nBody.\n' + + +def test_prefix_h1_md_only_first(): + content = '# First\n\n# Second\n' + result = pp._prefix_h1(content, 'lib', '.md') + assert result == '# lib: First\n\n# Second\n' + + +def test_prefix_h1_rst(): + content = 'Getting Started\n================\n\nSome text.\n' + result = pp._prefix_h1(content, 'tls-certificates', '.rst') + expected_title = 'tls-certificates: Getting Started' + lines = result.split('\n') + assert lines[0] == expected_title + assert lines[1] == '=' * len(expected_title) + + +# --- _rewrite_links --- + + +def test_rewrite_links_known_doc(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch): + """Relative link to a file in the sphinx map becomes a Sphinx path.""" + monkeypatch.setattr(pp, '_REPO_ROOT', tmp_path) + source = tmp_path / 'pkg' / 'docs' / 'tutorial.md' + source.parent.mkdir(parents=True) + # Target file must exist for resolve() to produce a consistent path. + target = tmp_path / 'pkg' / 'docs' / 'how-to' / 'deploy.md' + target.parent.mkdir(parents=True) + target.touch() + sphinx_map = {pathlib.PurePath('pkg/docs/how-to/deploy.md'): '/how-to/charmlibs/pkg/deploy'} + content = 'See [guide](how-to/deploy.md) for details.' + result = pp._rewrite_links(content, source, sphinx_map) + assert result == 'See [guide](/how-to/charmlibs/pkg/deploy) for details.' + + +def test_rewrite_links_with_anchor(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch): + """Anchors are preserved after rewriting.""" + monkeypatch.setattr(pp, '_REPO_ROOT', tmp_path) + source = tmp_path / 'pkg' / 'docs' / 'tutorial.md' + source.parent.mkdir(parents=True) + target = tmp_path / 'pkg' / 'docs' / 'how-to' / 'deploy.md' + target.parent.mkdir(parents=True) + target.touch() + sphinx_map = {pathlib.PurePath('pkg/docs/how-to/deploy.md'): '/how-to/charmlibs/pkg/deploy'} + content = 'See [step 2](how-to/deploy.md#step-2).' + result = pp._rewrite_links(content, source, sphinx_map) + assert result == 'See [step 2](/how-to/charmlibs/pkg/deploy#step-2).' + + +def test_rewrite_links_unknown_file_github( + tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch +): + """Relative link to a non-doc file falls back to GitHub URL.""" + monkeypatch.setattr(pp, '_REPO_ROOT', tmp_path) + monkeypatch.setattr(pp, '_REPO_MAIN_URL', 'https://github.com/canonical/charmlibs/blob/main') + source = tmp_path / 'pkg' / 'docs' / 'tutorial.md' + source.parent.mkdir(parents=True) + target = tmp_path / 'pkg' / 'src' / 'mod.py' + target.parent.mkdir(parents=True) + target.touch() + content = 'See [source](../src/mod.py).' + result = pp._rewrite_links(content, source, {}) + assert 'https://github.com/canonical/charmlibs/blob/main/pkg/src/mod.py' in result + + +def test_rewrite_links_preserves_http(): + """Absolute HTTP(S) links are left unchanged.""" + content = 'See [docs](https://example.com/page) for details.' + result = pp._rewrite_links(content, pathlib.Path('/fake/source.md'), {}) + assert result == content + + +def test_rewrite_links_image_github(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch): + """Image links to non-doc files fall back to GitHub URL.""" + monkeypatch.setattr(pp, '_REPO_ROOT', tmp_path) + monkeypatch.setattr(pp, '_REPO_MAIN_URL', 'https://github.com/canonical/charmlibs/blob/main') + source = tmp_path / 'pkg' / 'docs' / 'tutorial.md' + source.parent.mkdir(parents=True) + target = tmp_path / 'pkg' / 'docs' / 'images' / 'arch.png' + target.parent.mkdir(parents=True) + target.touch() + content = '![diagram](images/arch.png)' + result = pp._rewrite_links(content, source, {}) + assert 'https://github.com/canonical/charmlibs/blob/main/pkg/docs/images/arch.png' in result + + +def test_rewrite_links_outside_repo_raises( + tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch +): + """Link resolving outside the repo root raises ValueError.""" + monkeypatch.setattr(pp, '_REPO_ROOT', tmp_path / 'repo') + source = tmp_path / 'repo' / 'pkg' / 'docs' / 'tutorial.md' + source.parent.mkdir(parents=True) + content = 'See [bad](../../../../etc/passwd).' + with pytest.raises(ValueError): + pp._rewrite_links(content, source, {}) + + +# --- _copy_category --- + + +def test_copy_category_tutorial(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch): + docs_dir = tmp_path / 'docs_site' + source = tmp_path / 'lib' / 'docs' / 'tutorial.md' + source.parent.mkdir(parents=True) + source.write_text('# My Tutorial\n\nContent.\n') + monkeypatch.setattr(pp, '_DOCS_DIR', docs_dir) + monkeypatch.setattr(pp, '_REPO_ROOT', tmp_path) + entries = pp._copy_category([source], pathlib.PurePath('mylib'), 'tutorials', {}) + out = docs_dir / 'tutorials' / 'charmlibs' / 'mylib' / 'tutorial.md' + assert out.exists() + assert out.read_text().startswith('# mylib: My Tutorial\n') + assert entries == ['mylib: My Tutorial '] + + +def test_copy_category(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch): + docs_dir = tmp_path / 'docs_site' + howto_dir = tmp_path / 'lib' / 'docs' / 'how-to' + howto_dir.mkdir(parents=True) + (howto_dir / 'deploy.md').write_text('# Deploy\n\nSteps.\n') + (howto_dir / 'upgrade.md').write_text('# Upgrade\n\nSteps.\n') + monkeypatch.setattr(pp, '_DOCS_DIR', docs_dir) + monkeypatch.setattr(pp, '_REPO_ROOT', tmp_path) + sources = sorted(howto_dir.glob('*.md')) + entries = pp._copy_category(sources, pathlib.PurePath('mylib'), 'how-to', {}) + out_dir = docs_dir / 'how-to' / 'charmlibs' / 'mylib' + assert (out_dir / 'deploy.md').exists() + assert (out_dir / 'upgrade.md').exists() + assert (out_dir / 'deploy.md').read_text().startswith('# mylib: Deploy\n') + assert len(entries) == 2 + assert 'mylib: Deploy ' in entries + + +def test_copy_category_interface(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch): + docs_dir = tmp_path / 'docs_site' + expl_dir = tmp_path / 'lib' / 'docs' / 'explanation' + expl_dir.mkdir(parents=True) + (expl_dir / 'security.md').write_text('# Security\n') + monkeypatch.setattr(pp, '_DOCS_DIR', docs_dir) + monkeypatch.setattr(pp, '_REPO_ROOT', tmp_path) + entries = pp._copy_category( + [expl_dir / 'security.md'], pathlib.PurePath('interfaces/tls-certs'), 'explanation', {} + ) + out = docs_dir / 'explanation' / 'charmlibs' / 'interfaces' / 'tls-certs' / 'security.md' + assert out.exists() + assert entries == ['tls-certs: Security '] + + +def test_copy_category_empty_sources(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch): + docs_dir = tmp_path / 'docs_site' + monkeypatch.setattr(pp, '_DOCS_DIR', docs_dir) + entries = pp._copy_category([], pathlib.PurePath('mylib'), 'how-to', {}) + assert entries == [] diff --git a/.docs/extensions/test_generate_tables.py b/.docs/tests/test_generate_tables.py similarity index 100% rename from .docs/extensions/test_generate_tables.py rename to .docs/tests/test_generate_tables.py diff --git a/.docs/tutorial.md b/.docs/tutorial.md index e36d187b4..1fd8ba5a5 100644 --- a/.docs/tutorial.md +++ b/.docs/tutorial.md @@ -1,4 +1,4 @@ -# Tutorial +# charmlibs: Write your first charm library In this tutorial you'll add a new library to the `charmlibs` monorepo. @@ -354,7 +354,7 @@ Note that this will require `charmcraft` installed locally for packing, and a Ju However, you may find it easier to run the integration tests in CI instead, which is most easily done by opening a pull request. If you're following along with your own library, then see the next section for how to do this for real. Otherwise, if you're using the `uptime` example, open a PR against the `main` branch of your fork -- just make sure you enable the workflows first! -You'll be prompted to do this if you visit `https://github.com//charmlibs/actions`. +You'll be prompted to do this if you visit `https://github.com//charmlibs/actions`. ## Add library metadata diff --git a/.docs/tutorials/index.md b/.docs/tutorials/index.md new file mode 100644 index 000000000..80614e1c6 --- /dev/null +++ b/.docs/tutorials/index.md @@ -0,0 +1,10 @@ +# Tutorials + +```{toctree} +:maxdepth: 1 + +charmlibs: Write your first charm library +``` + +```{include} _lib-tutorials.md +``` diff --git a/.example/docs/explanation/a-charmlibs-example-explanation.md b/.example/docs/explanation/a-charmlibs-example-explanation.md new file mode 100644 index 000000000..2ec23ee0a --- /dev/null +++ b/.example/docs/explanation/a-charmlibs-example-explanation.md @@ -0,0 +1,17 @@ +--- +myst: + html_meta: + description: ... +--- + +# charmlibs.example explanation + +Explanations provide background, context, details, and design rationale. They help users understand why and how the library works the way it does, rather than how to use it. + +Good explanation topics include: design philosophy, how the library relates to the broader Juju ecosystem, and complicated implementation details that are important for advanced users. + +If the design is straightforward enough to cover in the package docstring, prefer that over a separate explanation page. + +Delete this file (and the `explanation/` directory) if no explanations are needed. + +See also: {ref}`how-to-add-library-docs` diff --git a/.example/docs/how-to/a-charmlibs-example-how-to.md b/.example/docs/how-to/a-charmlibs-example-how-to.md new file mode 100644 index 000000000..5a53dca3d --- /dev/null +++ b/.example/docs/how-to/a-charmlibs-example-how-to.md @@ -0,0 +1,17 @@ +--- +myst: + html_meta: + description: ... +--- + +# How to charmlibs.example + +How-to guides are goal-oriented: each one answers "how do I do X?" for a user who already understands the basics. They are not tutorials (no narrative walkthrough) and not explanations (no background theory). + +Common how-to topics include: configuring advanced options, migrating from a previous version, integrating with another library, or handling a specific edge case. + +If the procedure is short enough to fit in a docstring, prefer that over a separate how-to page. + +Delete this file (and the `how-to/` directory) if no how-to guides are needed. + +See also: {ref}`how-to-add-library-docs` diff --git a/.example/docs/tutorial.md b/.example/docs/tutorial.md new file mode 100644 index 000000000..2d38d97e6 --- /dev/null +++ b/.example/docs/tutorial.md @@ -0,0 +1,15 @@ +--- +myst: + html_meta: + description: ... +--- + +# charmlibs.example + +Quick-start information (installation, basic usage, a short example) belongs in the package docstring in `__init__.py` — that's where most users look first, and it appears in the generated API reference. + +Only add a tutorial if the library needs a longer, step-by-step walkthrough that would be too large for the docstring. For example, if your library requires deployment of multiple charms in a complicated configuration. + +Delete this file if a tutorial is not needed for your library. + +See also: {ref}`how-to-add-library-docs` diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2214c133f..807046f3c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -125,6 +125,16 @@ jobs: uvx --from rust-just just interfaces-json git diff --exit-code + meta: # tests for the repository's own CI tooling and helper scripts + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@v7 + - name: Run unit tests for the repository tooling scripts + run: uvx --from rust-just just scripts-unit + template-and-example-in-sync: runs-on: ubuntu-latest steps: diff --git a/.scripts/import_discourse_docs.py b/.scripts/import_discourse_docs.py new file mode 100755 index 000000000..c638deaa5 --- /dev/null +++ b/.scripts/import_discourse_docs.py @@ -0,0 +1,99 @@ +#!/usr/bin/env -S uv run --script --no-project + +# /// script +# requires-python = ">=3.12" +# dependencies = [ +# ] +# /// + +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +r"""Download a charm's Discourse documentation topic as a Markdown file. + +Charmhub renders charm docs from topics on https://discourse.charmhub.io. This +script downloads a single topic and writes its Markdown source to a file, +resolving any embedded images along the way. + +It's a helper for migrating a library's docs into the charmlibs monorepo -- +see the "Migrate your library's docs" section of the migration how-to guide: +https://documentation.ubuntu.com/charmlibs/how-to/migrate/ + +Usage: + .scripts/import_discourse_docs.py + +For example: + .scripts/import_discourse_docs.py \ + https://discourse.charmhub.io/t/tls-certificates-interface/15539 \ + interfaces/tls-certificates/docs/explanation/tls-certificates-interface.md +""" + +from __future__ import annotations + +import argparse +import json +import pathlib +import re +import urllib.request + +# Matches the numeric topic ID in a Discourse URL, e.g. +# https://discourse.charmhub.io/t//(/)? +_TOPIC_ID_RE = re.compile(r'/t/(?:[^/]+/)?(\d+)') + + +def main() -> None: + """Download the topic named on the command line and write it to a file.""" + parser = argparse.ArgumentParser( + description="Download a charm's Discourse documentation topic as a Markdown file." + ) + parser.add_argument( + 'url', help='Discourse topic URL, e.g. https://discourse.charmhub.io/t//' + ) + parser.add_argument('output', type=pathlib.Path, help='Markdown file to write') + args = parser.parse_args() + + post = _fetch(_topic_id(args.url))['post_stream']['posts'][0] + markdown = _resolve_images(post['raw'], post['cooked']) + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(markdown) + print(f'Wrote {args.output}') + + +def _topic_id(url: str) -> int: + """Extract the numeric topic ID from a Discourse topic URL.""" + match = _TOPIC_ID_RE.search(url) + if match is None: + raise ValueError(f'Not a Discourse topic URL: {url!r}') + return int(match.group(1)) + + +def _fetch(topic_id: int): + """Download a Discourse topic as JSON, including the raw Markdown source.""" + url = f'https://discourse.charmhub.io/t/{topic_id}.json?include_raw=true' + with urllib.request.urlopen(url, timeout=30) as response: # noqa: S310 # trusted host + return json.loads(response.read()) + + +def _resolve_images(raw: str, cooked: str) -> str: + """Rewrite ``upload://`` image shortcodes to real URLs using the cooked HTML.""" + uploads = re.findall(r'upload://[^\s)]+', raw) + img_urls = re.findall(r']+src=["\']([^"\'\s]+)', cooked) + for upload, image_url in zip(uploads, img_urls, strict=False): # matched positionally + raw = raw.replace(upload, image_url) + # Strip Discourse's |WIDTHxHEIGHT suffix from image alt text. + return re.sub(r'!\[([^\]]*?)\|\d+x\d+\]', r'![\1]', raw) + + +if __name__ == '__main__': + main() diff --git a/.scripts/ls.py b/.scripts/ls.py index b7efc228f..d7ccd547a 100755 --- a/.scripts/ls.py +++ b/.scripts/ls.py @@ -66,6 +66,7 @@ class Info: description: str = '' status: str = '' schema_path: str = '' + docs: dict[str, list[str]] = dataclasses.field(default_factory=dict) tags: list[str] = dataclasses.field(default_factory=list[str]) def to_dict(self, *fields: str) -> dict[str, object]: @@ -191,6 +192,8 @@ def _ls( info.status = _get_status(category, root, path) if 'schema_path' in output: info.schema_path = _get_schema_path_str(category, root, path) + if 'docs' in output: + info.docs = _get_docs(root, path) if 'tags' in output: info.tags = _lib_tags().get(_get_lib_name(category, root, path), []) infos.append(info) @@ -347,8 +350,8 @@ def _get_lib_name(category: str, root: pathlib.Path, path: pathlib.Path) -> str: if category == 'packages': if path.name == '.package': # .package -> () -> 'charmlibs' - # interfaces/.package -> ('interfaces') -> 'charmlibs.interface' - parts, _ = path.parts + # interfaces/.package -> ('interfaces',) -> 'charmlibs.interface' + parts = path.parts[:-1] else: # For special cases like '.tutorial' -> ('tutorial') -> 'charmlibs.tutorial' parts = tuple(p.removeprefix('.') for p in path.parts) @@ -372,6 +375,31 @@ def _get_status(category: str, root: pathlib.Path, path: pathlib.Path) -> str: return _interface_yaml(path, root=root).get('status', '').strip() +def _get_docs(root: pathlib.Path, path: pathlib.Path) -> dict[str, list[str]]: + """Return diataxis doc files for a package, relative to the package directory.""" + docs_dir = root / path / 'docs' + result: dict[str, list[str]] = {} + if not docs_dir.is_dir(): + return result + for ext in ('.md', '.rst'): + tutorial = docs_dir / f'tutorial{ext}' + if tutorial.exists(): + result['tutorials'] = [str(tutorial.relative_to(root / path))] + break + for cat in ('how-to', 'explanation'): + cat_dir = docs_dir / cat + if not cat_dir.is_dir(): + continue + files = sorted( + str(f.relative_to(root / path)) + for f in cat_dir.iterdir() + if f.suffix in ('.md', '.rst') + ) + if files: + result[cat] = files + return result + + def _get_schema_path_str(category: str, root: pathlib.Path, path: pathlib.Path) -> str: if category == 'packages': return '' diff --git a/.scripts/tests/conftest.py b/.scripts/tests/conftest.py new file mode 100644 index 000000000..bf7191688 --- /dev/null +++ b/.scripts/tests/conftest.py @@ -0,0 +1,25 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Pytest configuration for the .scripts tests. + +Add the .scripts directory to sys.path so the tooling scripts can be imported +from test files. +""" + +import pathlib +import sys + +_SCRIPTS_DIR = pathlib.Path(__file__).resolve().parent.parent +sys.path.insert(0, str(_SCRIPTS_DIR)) diff --git a/.scripts/tests/test_import_discourse_docs.py b/.scripts/tests/test_import_discourse_docs.py new file mode 100644 index 000000000..90ce8b94c --- /dev/null +++ b/.scripts/tests/test_import_discourse_docs.py @@ -0,0 +1,93 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ruff: noqa: D103 (function docstrings) + +"""Unit tests for the import_discourse_docs script.""" + +import pathlib + +import import_discourse_docs as ids +import pytest + +# --- _topic_id --- + + +@pytest.mark.parametrize( + ('url', 'expected'), + [ + ('https://discourse.charmhub.io/t/tls-certificates-interface/15539', 15539), + ('https://discourse.charmhub.io/t/some-slug/123/4', 123), # trailing post number + ('https://discourse.charmhub.io/t/15539', 15539), # slug omitted + ], +) +def test_topic_id(url: str, expected: int): + assert ids._topic_id(url) == expected + + +def test_topic_id_invalid(): + with pytest.raises(ValueError, match='Not a Discourse topic URL'): + ids._topic_id('https://charmhub.io/some-charm') + + +# --- _resolve_images --- + + +def test_resolve_images_replaces_upload_shortcodes(): + raw = 'before ![image](upload://abc123.png) after' + cooked = '

before image after

' + result = ids._resolve_images(raw, cooked) + assert result == 'before ![image](https://cdn.example/abc123.png) after' + + +def test_resolve_images_strips_dimension_suffix(): + raw = '![diagram|690x420](upload://abc.png)' + cooked = '' + result = ids._resolve_images(raw, cooked) + assert result == '![diagram](https://cdn.example/abc.png)' + + +def test_resolve_images_no_images_is_noop(): + raw = 'plain text with no images' + assert ids._resolve_images(raw, '') == raw + + +# --- main --- + + +def test_main_writes_resolved_markdown(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch): + out = tmp_path / 'docs' / 'explanation' / 'page.md' + + def fake_fetch(topic_id: int): + assert topic_id == 15539 + return { + 'post_stream': { + 'posts': [ + { + 'raw': '# Title\n\n![i](upload://x.png)', + 'cooked': '', + }, + ], + }, + } + + monkeypatch.setattr(ids, '_fetch', fake_fetch) + monkeypatch.setattr( + 'sys.argv', + ['import_discourse_docs.py', 'https://discourse.charmhub.io/t/slug/15539', str(out)], + ) + + ids.main() + + assert out.read_text() == '# Title\n\n![i](https://cdn.example/x.png)' diff --git a/.template/{{ cookiecutter.project_slug }}/docs/explanation/a-{{ cookiecutter.__dist_pkg }}-explanation.md b/.template/{{ cookiecutter.project_slug }}/docs/explanation/a-{{ cookiecutter.__dist_pkg }}-explanation.md new file mode 100644 index 000000000..f67af7a2d --- /dev/null +++ b/.template/{{ cookiecutter.project_slug }}/docs/explanation/a-{{ cookiecutter.__dist_pkg }}-explanation.md @@ -0,0 +1,17 @@ +--- +myst: + html_meta: + description: ... +--- + +# {{ cookiecutter.__import_pkg }} explanation + +Explanations provide background, context, details, and design rationale. They help users understand why and how the library works the way it does, rather than how to use it. + +Good explanation topics include: design philosophy, how the library relates to the broader Juju ecosystem, and complicated implementation details that are important for advanced users. + +If the design is straightforward enough to cover in the package docstring, prefer that over a separate explanation page. + +Delete this file (and the `explanation/` directory) if no explanations are needed. + +See also: {ref}`how-to-add-library-docs` diff --git a/.template/{{ cookiecutter.project_slug }}/docs/how-to/a-{{ cookiecutter.__dist_pkg }}-how-to.md b/.template/{{ cookiecutter.project_slug }}/docs/how-to/a-{{ cookiecutter.__dist_pkg }}-how-to.md new file mode 100644 index 000000000..90d4f4b09 --- /dev/null +++ b/.template/{{ cookiecutter.project_slug }}/docs/how-to/a-{{ cookiecutter.__dist_pkg }}-how-to.md @@ -0,0 +1,17 @@ +--- +myst: + html_meta: + description: ... +--- + +# How to {{ cookiecutter.__import_pkg }} + +How-to guides are goal-oriented: each one answers "how do I do X?" for a user who already understands the basics. They are not tutorials (no narrative walkthrough) and not explanations (no background theory). + +Common how-to topics include: configuring advanced options, migrating from a previous version, integrating with another library, or handling a specific edge case. + +If the procedure is short enough to fit in a docstring, prefer that over a separate how-to page. + +Delete this file (and the `how-to/` directory) if no how-to guides are needed. + +See also: {ref}`how-to-add-library-docs` diff --git a/.template/{{ cookiecutter.project_slug }}/docs/tutorial.md b/.template/{{ cookiecutter.project_slug }}/docs/tutorial.md new file mode 100644 index 000000000..0b6bfb52b --- /dev/null +++ b/.template/{{ cookiecutter.project_slug }}/docs/tutorial.md @@ -0,0 +1,15 @@ +--- +myst: + html_meta: + description: ... +--- + +# {{ cookiecutter.__import_pkg }} + +Quick-start information (installation, basic usage, a short example) belongs in the package docstring in `__init__.py` — that's where most users look first, and it appears in the generated API reference. + +Only add a tutorial if the library needs a longer, step-by-step walkthrough that would be too large for the docstring. For example, if your library requires deployment of multiple charms in a complicated configuration. + +Delete this file if a tutorial is not needed for your library. + +See also: {ref}`how-to-add-library-docs` diff --git a/AGENTS.md b/AGENTS.md index 684cc9926..6654b5080 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -230,6 +230,15 @@ Read more: {ref}`how-to-customize-integration-tests` Read more: {ref}`charm-libs-charmhub-hosted`, {ref}`Charmcraft | Manage libraries ` ``` +### Migrating a library's docs + +To bring an existing library's tutorials, how-to guides, and explanations into the monorepo, follow the **Migrate your library's docs** section of [How to migrate to the charmlibs monorepo](https://documentation.ubuntu.com/charmlibs/how-to/migrate/). In short: + +- For Charmhub-hosted docs, download each Discourse topic with `uv run .scripts/import-discourse-docs.py `, choosing a diataxis category (and so an output path) for each. +- Then edit the imported pages to fit charmlibs conventions, following [How to add docs to a library](https://documentation.ubuntu.com/charmlibs/how-to/add-library-docs/). + +There is no `reference` docs category — reference docs are generated from docstrings. + ## Common pitfalls - **Don't call `uv add` directly** — use `just add ` to respect repo-level constraints. diff --git a/docs.just b/docs.just index d3c8215db..9875cead2 100644 --- a/docs.just +++ b/docs.just @@ -9,10 +9,14 @@ Build the docs, (re)generating reference docs for all packages, or just those sp `just docs` is an alias for `just docs html`. To specify packages, you must use the full recipe, e.g. `just docs html interfaces/foo`. """)] -html *packages: (_packages packages) +html *packages: _diataxis_docs (_packages packages) uvx --with-requirements=.sphinx/requirements.txt --from=sphinx \ sphinx-build -b dirhtml -T -W --keep-going -d .sphinx/.doctrees -D language=en . '{{build_dir}}/html' +[doc('Copy per-library diataxis docs into the Sphinx source tree and generate toctree include files.')] +_diataxis_docs: + ./scripts/diataxis_preprocessor.py + [doc(""" Run html builder once per package, with its deps installed, setting the package argument and suppressing reference warnings. @@ -78,14 +82,21 @@ clean: rm -rf reference/charmlibs rm -rf reference/interfaces rm -rf .save + # per-library diataxis docs + rm -rf tutorials/charmlibs + rm -rf how-to/charmlibs + rm -rf explanation/charmlibs + rm -f tutorials/_lib-tutorials.md + rm -f how-to/_lib-how-to.md + rm -f explanation/_lib-explanation.md [doc('Run `pyright` for local sphinx extensions.')] ext-static *pyrightargs: - cd extensions && uvx --with-requirements=../.sphinx/requirements.txt --with=pytest \ + cd tests && uvx --with-requirements=../.sphinx/requirements.txt --with=pytest \ pyright {{pyrightargs}} [doc('Run unit tests with `coverage` for local sphinx extensions.')] ext-unit +flags='-rA': uvx --with-requirements=.sphinx/requirements.txt --with=pytest \ - coverage run --omit='test_*.py' -m pytest --tb=native -vv {{flags}} extensions + coverage run --omit='test_*.py' --source=extensions,scripts -m pytest --tb=native -vv {{flags}} tests uvx coverage report diff --git a/interfaces/.example/docs/explanation/a-charmlibs-interfaces-example-interface-explanation.md b/interfaces/.example/docs/explanation/a-charmlibs-interfaces-example-interface-explanation.md new file mode 100644 index 000000000..f1dbb4d26 --- /dev/null +++ b/interfaces/.example/docs/explanation/a-charmlibs-interfaces-example-interface-explanation.md @@ -0,0 +1,17 @@ +--- +myst: + html_meta: + description: ... +--- + +# charmlibs.interfaces.example_interface explanation + +Explanations provide background, context, details, and design rationale. They help users understand why and how the library works the way it does, rather than how to use it. + +Good explanation topics include: design philosophy, how the library relates to the broader Juju ecosystem, and complicated implementation details that are important for advanced users. + +If the design is straightforward enough to cover in the package docstring, prefer that over a separate explanation page. + +Delete this file (and the `explanation/` directory) if no explanations are needed. + +See also: {ref}`how-to-add-library-docs` diff --git a/interfaces/.example/docs/how-to/a-charmlibs-interfaces-example-interface-how-to.md b/interfaces/.example/docs/how-to/a-charmlibs-interfaces-example-interface-how-to.md new file mode 100644 index 000000000..460fa6856 --- /dev/null +++ b/interfaces/.example/docs/how-to/a-charmlibs-interfaces-example-interface-how-to.md @@ -0,0 +1,17 @@ +--- +myst: + html_meta: + description: ... +--- + +# How to charmlibs.interfaces.example_interface + +How-to guides are goal-oriented: each one answers "how do I do X?" for a user who already understands the basics. They are not tutorials (no narrative walkthrough) and not explanations (no background theory). + +Common how-to topics include: configuring advanced options, migrating from a previous version, integrating with another library, or handling a specific edge case. + +If the procedure is short enough to fit in a docstring, prefer that over a separate how-to page. + +Delete this file (and the `how-to/` directory) if no how-to guides are needed. + +See also: {ref}`how-to-add-library-docs` diff --git a/interfaces/.example/docs/tutorial.md b/interfaces/.example/docs/tutorial.md new file mode 100644 index 000000000..6be1cf696 --- /dev/null +++ b/interfaces/.example/docs/tutorial.md @@ -0,0 +1,15 @@ +--- +myst: + html_meta: + description: ... +--- + +# charmlibs.interfaces.example_interface + +Quick-start information (installation, basic usage, a short example) belongs in the package docstring in `__init__.py` — that's where most users look first, and it appears in the generated API reference. + +Only add a tutorial if the library needs a longer, step-by-step walkthrough that would be too large for the docstring. For example, if your library requires deployment of multiple charms in a complicated configuration. + +Delete this file if a tutorial is not needed for your library. + +See also: {ref}`how-to-add-library-docs` diff --git a/justfile b/justfile index 23f5f54e9..f8f6f4a5b 100644 --- a/justfile +++ b/justfile @@ -188,3 +188,8 @@ interfaces-json: --output status \ --indent-json \ > interfaces/index.json + +[doc('Run unit tests for the repository tooling scripts in `.scripts/`.')] +scripts-unit +flags='-rA': + {{_uv_run_with_test_requirements}} \ + pytest --tb=native -vv {{flags}} .scripts/tests diff --git a/pathops/docs/explanation/design.rst b/pathops/docs/explanation/design.rst new file mode 100644 index 000000000..0b3d5c158 --- /dev/null +++ b/pathops/docs/explanation/design.rst @@ -0,0 +1,41 @@ +.. meta:: + :description: Why pathops models charm filesystem operations on pathlib. + +Design philosophy +================= + +:mod:`pathops` provides a :mod:`pathlib`-like interface for working with files in both local and remote (Pebble-based) filesystems. This document explains the design choices behind the library. + +A pathlib-like interface +------------------------ + +The core goal of ``pathops`` is to let charm authors write filesystem code once and have it work on both Kubernetes and machine charms. To achieve this, :class:`~pathops.ContainerPath` mirrors the :class:`pathlib.PurePosixPath` and :class:`pathlib.Path` APIs as closely as possible. + +``pathlib`` has a familiar interface — the ``/`` operator for joining, :meth:`~pathlib.Path.read_text` and :meth:`~pathlib.Path.write_text` for file I/O, :meth:`~pathlib.Path.mkdir` for directory creation, :meth:`~pathlib.Path.glob` for matching. By matching ``pathlib``'s interface, ``pathops`` avoids introducing new concepts. Code that works with ``pathlib.Path`` should look almost identical when ported to ``ContainerPath``. + +Where ``pathops`` diverges from ``pathlib``, it's because the underlying Pebble API imposes constraints: + +- **No** ``open()`` **method.** Pebble's push/pull model doesn't map cleanly to Python file handles. Use :meth:`~pathlib.Path.read_text`, :meth:`~pathlib.Path.read_bytes`, :meth:`~pathlib.Path.write_text`, and :meth:`~pathlib.Path.write_bytes` instead. +- **Absolute paths only.** :class:`~pathops.ContainerPath` raises :class:`~pathops.RelativePathError` if you try to create a relative path, because Pebble operations require absolute paths. +- **No** ``chmod()`` **method.** Permissions are set at write time via ``mode=``, ``user=``, and ``group=`` keyword arguments on ``write_text()``, ``write_bytes()``, and ``mkdir()``. This matches Pebble's API, where permissions are part of the push/make_dir call. +- **Default file mode is 0o644**, not ``pathlib``'s 0o666. This matches Pebble's default and is a more sensible default for most use cases. + +PathProtocol +------------ + +:class:`~pathops.PathProtocol` is a :class:`typing.Protocol` that defines the interface common to both :class:`~pathops.ContainerPath` and :class:`~pathops.LocalPath`. It exists so that charm authors can write helpers that accept either path type: + +.. code-block:: python + + from charmlibs import pathops + + def write_config(root: pathops.PathProtocol, content: str) -> None: + (root / 'etc' / 'myapp' / 'config.yaml').write_text(content) + +You might think ``LocalPath | ContainerPath`` would work just as well, but in practice it's much less useful. A union type shows the *superset* of methods and arguments from both types in your IDE — including things like :class:`pathlib.PosixPath` methods that don't exist on :class:`~pathops.ContainerPath`. :class:`~pathops.PathProtocol` shows only the *subset* that both types support, so your IDE completions are exactly the methods you can safely call. + +.. note:: + + :class:`~pathops.PathProtocol` is not designed to be implemented by third parties. We don't guarantee compatibility for third-party implementations when we add new methods to the protocol. One example: adding a method to the protocol is only a minor version bump (it's a feature for users of the concrete classes), not a major one (it's a potentially breaking change for implementations of the protocol). Treat the protocol as a convenient type annotation for the ``pathops`` classes, not a compatibility guarantee for other implementations. + +:class:`~pathops.LocalPath` is a concrete subclass of :class:`pathlib.PosixPath` that extends its write methods — :meth:`~pathlib.Path.write_text`, :meth:`~pathlib.Path.write_bytes`, and :meth:`~pathlib.Path.mkdir` — with ``mode=``, ``user=``, and ``group=`` keyword arguments for setting permissions and ownership at write time. This means it inherits all of ``pathlib``'s functionality while matching the permission-setting interface that :class:`~pathops.ContainerPath` uses. :class:`~pathops.PathProtocol` captures the subset of that functionality that both types share. diff --git a/pathops/docs/tutorial.md b/pathops/docs/tutorial.md new file mode 100644 index 000000000..c6f952fba --- /dev/null +++ b/pathops/docs/tutorial.md @@ -0,0 +1,102 @@ +--- +myst: + html_meta: + description: Manage files in a charm workload with pathops. +--- + +# Getting started + +This tutorial shows you how to use ``pathops`` to manage files in a charm workload — writing config, restarting only when something changes, and testing it all with ``ops.testing``. + +## Write a config file with ``ensure_contents`` + +The most common thing charms do with files is write a configuration file and restart the workload if the file changed. ``pathops.ensure_contents`` does exactly this — it compares the file's current contents, permissions, and ownership against what you pass in, only writes if something differs, and returns ``True`` if any changes were made. + +Start with a base charm class that defines the config-changed handler and stubs out ``root`` and ``_restart_workload`` for subclasses to implement: + +```python +import ops +from charmlibs import pathops + + +class MyCharmBase(ops.CharmBase): + + def __init__(self, framework: ops.Framework): + super().__init__(framework) + self.framework.observe(self.on.config_changed, self._on_config_changed) + + @property + def root(self) -> pathops.PathProtocol: + raise NotImplementedError + + def _restart_workload(self) -> None: + raise NotImplementedError + + def _on_config_changed(self, event: ops.ConfigChangedEvent) -> None: + config = f'port: {self.config["port"]}\n' + changed = pathops.ensure_contents(self.root / 'etc' / 'myapp' / 'config.yaml', config) + if changed: + self._restart_workload() +``` + +The event handler doesn't know or care whether it's running on K8s or a machine — it just uses ``self.root`` to build paths with the ``/`` operator, exactly like ``pathlib``. + +Now implement the K8s subclass: + +```python +class MyK8sCharm(MyCharmBase): + + @property + def root(self) -> pathops.ContainerPath: + container = self.unit.get_container('myapp') + return pathops.ContainerPath('/', container=container) + + def _restart_workload(self) -> None: + container = self.unit.get_container('myapp') + container.restart('myapp') +``` + +## Make it work on machines too + +The same pattern works for machine charms — just implement the stubs with ``LocalPath``: + +```python +class MyMachineCharm(MyCharmBase): + + @property + def root(self) -> pathops.LocalPath: + return pathops.LocalPath('/') + + def _restart_workload(self) -> None: + subprocess.run(['systemctl', 'restart', 'myapp'], check=True) +``` + +The ``_on_config_changed`` handler in the base class works unchanged — ``ensure_contents`` accepts any ``PathProtocol``, which both ``ContainerPath`` and ``LocalPath`` satisfy. + +## Test it with ``ops.testing`` + +Because ``pathops`` works through the standard ``ops.Container`` interface, ``ops.testing`` state-transition tests work out of the box — no extra mocking needed: + +```python +from ops import testing + +from charm import MyK8sCharm + + +def test_config_changed_writes_config(): + ctx = testing.Context(MyK8sCharm) + container = testing.Container('myapp', can_connect=True) + state_in = testing.State(containers={container}, config={'port': 8080}) + + state_out = ctx.run(ctx.on.config_changed(), state_in) + + fs = state_out.get_container('myapp').get_filesystem(ctx) + assert (fs / 'etc' / 'myapp' / 'config.yaml').read_text() == 'port: 8080\n' +``` + +``get_filesystem`` returns a ``pathlib.Path`` to the temporary directory that ``ops.testing`` uses to simulate the container filesystem. Files written by ``pathops.ContainerPath`` during the event handler end up there, so you can assert on them with plain ``pathlib``. + +## See also + +- {external+ops:ref}`files-in-containers` +- {external+ops:ref}`write-unit-tests-for-a-charm` From 9342c9ca50b7b3530cf7afb2409440fd39da7b3d Mon Sep 17 00:00:00 2001 From: Kayra Date: Fri, 5 Jun 2026 12:40:38 +0300 Subject: [PATCH 37/65] fix: add modelerror check when loading relation data (#411) This was causing an issue with self-signed-certificates [here](https://github.com/canonical/self-signed-certificates-operator/issues/530). When we check for relations, it's possible that a relation is available but the databag is not accessible. This is possible sometimes during relation-broken or relation-departed events. In those cases we receive a model error, which we now capture to avoid crashing charms unnecessarily. fixes #298 --- interfaces/tls-certificates/CHANGELOG.md | 4 ++++ .../interfaces/tls_certificates/_tls_certificates.py | 3 +++ .../src/charmlibs/interfaces/tls_certificates/_version.py | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/interfaces/tls-certificates/CHANGELOG.md b/interfaces/tls-certificates/CHANGELOG.md index fa25d54ae..a8bb65f22 100644 --- a/interfaces/tls-certificates/CHANGELOG.md +++ b/interfaces/tls-certificates/CHANGELOG.md @@ -1,3 +1,7 @@ +# 1.8.3 - 5 June 2026 + +Add a safety net to ensure expiring certificates are renewed even if the charm fails to trigger the renewal process. + # 1.8.2 - 3 June 2026 Update the library to cache the `private_key`. diff --git a/interfaces/tls-certificates/src/charmlibs/interfaces/tls_certificates/_tls_certificates.py b/interfaces/tls-certificates/src/charmlibs/interfaces/tls_certificates/_tls_certificates.py index 90d199694..39de92cf3 100644 --- a/interfaces/tls-certificates/src/charmlibs/interfaces/tls_certificates/_tls_certificates.py +++ b/interfaces/tls-certificates/src/charmlibs/interfaces/tls_certificates/_tls_certificates.py @@ -2524,6 +2524,9 @@ def _load_provider_certificates(self) -> list[ProviderCertificate]: except DataValidationError: logger.warning("Invalid relation data") return [] + except ModelError: + logger.warning("Relation data not available") + return [] return [ certificate.to_provider_certificate(relation_id=relation.id) for certificate in provider_relation_data.certificates diff --git a/interfaces/tls-certificates/src/charmlibs/interfaces/tls_certificates/_version.py b/interfaces/tls-certificates/src/charmlibs/interfaces/tls_certificates/_version.py index ae8fece9b..a6e2226d8 100644 --- a/interfaces/tls-certificates/src/charmlibs/interfaces/tls_certificates/_version.py +++ b/interfaces/tls-certificates/src/charmlibs/interfaces/tls_certificates/_version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.8.2" +__version__ = "1.8.3" From ff91bb38f680cc8dd35c0cb64934c10de30fdd46 Mon Sep 17 00:00:00 2001 From: James Garner Date: Mon, 15 Jun 2026 10:30:02 +1200 Subject: [PATCH 38/65] docs: sphinx pack migration (#517) This PR migrates our docs source layout and infra to align with the Sphinx pack version 2. --------- Co-authored-by: David Wilding --- .docs/.custom_wordlist.txt | 128 ++++++++++++--- .docs/.gitignore | 13 +- .docs/.sphinx/.wordlist.txt | 64 -------- .docs/.sphinx/get_vale_conf.py | 53 ------ .docs/.sphinx/metrics/build_metrics.sh | 15 -- .docs/.sphinx/metrics/source_metrics.sh | 66 -------- .docs/.sphinx/requirements.txt | 7 - .docs/.sphinx/spellingcheck.yaml | 44 ----- .docs/_dev/get_vale_conf.py | 151 ++++++++++++++++++ .docs/{.sphinx => _dev}/pa11y.json | 0 .docs/_dev/requirements.txt | 34 ++++ .../_static/logos/juju-logo-no-text.png | Bin .../_static/project_specific.css | 0 .../{.sphinx => }/_static/tag_click_search.js | 0 .docs/_templates/footer.html | 38 +++++ .docs/_templates/header.html | 72 +++++++++ .docs/conf.py | 62 +++++-- .docs/how-to/migrate.md | 6 +- .docs/scripts/diataxis_preprocessor.py | 2 +- .docs/tutorial.md | 4 +- .github/workflows/docs.yaml | 4 +- docs.just | 64 ++++++-- pyproject.toml | 2 +- 23 files changed, 506 insertions(+), 323 deletions(-) delete mode 100644 .docs/.sphinx/.wordlist.txt delete mode 100644 .docs/.sphinx/get_vale_conf.py delete mode 100755 .docs/.sphinx/metrics/build_metrics.sh delete mode 100755 .docs/.sphinx/metrics/source_metrics.sh delete mode 100644 .docs/.sphinx/requirements.txt delete mode 100644 .docs/.sphinx/spellingcheck.yaml create mode 100644 .docs/_dev/get_vale_conf.py rename .docs/{.sphinx => _dev}/pa11y.json (100%) create mode 100644 .docs/_dev/requirements.txt rename .docs/{.sphinx => }/_static/logos/juju-logo-no-text.png (100%) rename .docs/{.sphinx => }/_static/project_specific.css (100%) rename .docs/{.sphinx => }/_static/tag_click_search.js (100%) create mode 100644 .docs/_templates/footer.html create mode 100644 .docs/_templates/header.html diff --git a/.docs/.custom_wordlist.txt b/.docs/.custom_wordlist.txt index 5fb8770cc..c6cc0d809 100644 --- a/.docs/.custom_wordlist.txt +++ b/.docs/.custom_wordlist.txt @@ -1,70 +1,146 @@ -# Ensure the last line in this file has a newline at the end, to support concatenation -autocomplete -autocompletions +# Words accepted by Vale's spell checker (Canonical.000-US-spellcheck). +# One entry per line. Entries are treated as case-sensitive regular expressions. +# Ensure the last line in this file has a newline at the end, to support concatenation. +ACME +ACME's +addons +AGPLv +API +APIs args armor +autocomplete +autocompletions +balancer +bugfixes +charmcraft +Charmhub +charmlib +charmlibs +CLI codebase +ContainerPath databag databags +DCO +deserialised dev +Diátaxis discoverability DNS +docstring +docstrings +Dqlite +dropdown +drwxr +EBS +EKS +enablement +favicon filesystem FQDN +Furo +Git +GitHub GPG +Grafana +gRPC hashable HPC HTTPS +IAM implementer implementers +installable instantiation +Jinja +JSON +Juju +Kubeflow +Kubernetes +Launchpad libs +linter +lockfile +LTS +LXD +Makefile +Makefiles +Matrix +Mattermost +MicroCeph +MicroCloud +MicroOVN monorepo +monorepos +Multus +MyST +namespace +namespaces namespacing +Nginx +NodePort +Numbat +observability OCI +OEM OID +OLM +pathlib +pathops PEM +Permalink +podspec posix +pre +preprocessor +PYDEPS +PyPI +pytest pythonic +Quickstart radix +ReadMe +READMEs reimplemented repo requirer requirers +reST +reStructuredText +roadmap RSA +RTD +rw +scaffolded stateful subclasses +subdirectories subdirectory +subfolders subprocess +subtree symlink symlinks templating +TLS +TODO tooltip tooltips -TLS +Ubuntu +UI URI +URIs +userinfo UTF +uv +UUID +validator +validators vendored -yaml - -charmcraft -charmlib -charmlibs -podspec -PYDEPS - -pathlib -pathops -ContainerPath - -gRPC -Jinja -Multus -Nginx -PyPI - -drwxr -rw -xr - vlatest +VM +webhook +xr +yaml +YAML diff --git a/.docs/.gitignore b/.docs/.gitignore index 6d8dd1dcb..538238709 100644 --- a/.docs/.gitignore +++ b/.docs/.gitignore @@ -1,16 +1,15 @@ # Environment *env*/ -.sphinx/venv/ +_dev/venv/ # Sphinx -.sphinx/warnings.txt -.sphinx/.wordlist.dic -.sphinx/.doctrees/ -.sphinx/node_modules/ +_dev/warnings.txt +_dev/.doctrees/ +_dev/node_modules/ # Vale -.sphinx/styles/* -.sphinx/vale.ini +_dev/styles/* +_dev/vale.ini # Build outputs _build diff --git a/.docs/.sphinx/.wordlist.txt b/.docs/.sphinx/.wordlist.txt deleted file mode 100644 index be5021a1f..000000000 --- a/.docs/.sphinx/.wordlist.txt +++ /dev/null @@ -1,64 +0,0 @@ -ACME -ACME's -addons -AGPLv -API -APIs -balancer -Charmhub -CLI -DCO -Diátaxis -Dqlite -dropdown -EBS -EKS -enablement -favicon -Furo -Git -GitHub -Grafana -IAM -installable -JSON -Juju -Kubeflow -Kubernetes -Launchpad -linter -LTS -LXD -Makefile -Makefiles -Matrix -Mattermost -MicroCeph -MicroCloud -MicroOVN -MyST -namespace -namespaces -NodePort -Numbat -observability -OEM -OLM -Permalink -pre -Quickstart -ReadMe -reST -reStructuredText -roadmap -RTD -subdirectories -subfolders -subtree -TODO -Ubuntu -UI -UUID -VM -webhook -YAML diff --git a/.docs/.sphinx/get_vale_conf.py b/.docs/.sphinx/get_vale_conf.py deleted file mode 100644 index 9ee2d0b5f..000000000 --- a/.docs/.sphinx/get_vale_conf.py +++ /dev/null @@ -1,53 +0,0 @@ -#! /usr/bin/env python - -import requests -import os - -DIR = os.getcwd() - - -def main(): - if os.path.exists(f"{DIR}/.sphinx/styles/Canonical"): - print("Vale directory exists") - else: - os.makedirs(f"{DIR}/.sphinx/styles/Canonical") - - url = ( - "https://api.github.com/repos/canonical/praecepta/" - + "contents/styles/Canonical" - ) - r = requests.get(url) - for item in r.json(): - download = requests.get(item["download_url"]) - file = open(".sphinx/styles/Canonical/" + item["name"], "w") - file.write(download.text) - file.close() - - if os.path.exists(f"{DIR}/.sphinx/styles/config/vocabularies/Canonical"): - print("Vocab directory exists") - else: - os.makedirs(f"{DIR}/.sphinx/styles/config/vocabularies/Canonical") - - url = ( - "https://api.github.com/repos/canonical/praecepta/" - + "contents/styles/config/vocabularies/Canonical" - ) - r = requests.get(url) - for item in r.json(): - download = requests.get(item["download_url"]) - file = open( - ".sphinx/styles/config/vocabularies/Canonical/" + item["name"], - "w" - ) - file.write(download.text) - file.close() - config = requests.get( - "https://raw.githubusercontent.com/canonical/praecepta/main/vale.ini" - ) - file = open(".sphinx/vale.ini", "w") - file.write(config.text) - file.close() - - -if __name__ == "__main__": - main() diff --git a/.docs/.sphinx/metrics/build_metrics.sh b/.docs/.sphinx/metrics/build_metrics.sh deleted file mode 100755 index bd1ff1cb4..000000000 --- a/.docs/.sphinx/metrics/build_metrics.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -# shellcheck disable=all - -links=0 -images=0 - -# count number of links -links=$(find . -type d -path './.sphinx' -prune -o -name '*.html' -exec cat {} + | grep -o "
=0.3 -sphinx-sitemap~=2.8 diff --git a/.docs/.sphinx/spellingcheck.yaml b/.docs/.sphinx/spellingcheck.yaml deleted file mode 100644 index c73d69331..000000000 --- a/.docs/.sphinx/spellingcheck.yaml +++ /dev/null @@ -1,44 +0,0 @@ -matrix: - - name: rST files - aspell: - lang: en - d: en_GB - dictionary: - wordlists: - - .sphinx/.wordlist.txt - - .custom_wordlist.txt - output: .sphinx/.wordlist.dic - sources: - - _build/**/*.html|!_build/genindex/index.html - pipeline: - - pyspelling.filters.html: - comments: false - attributes: - - title - - alt - ignores: - - code - - pre - - spellexception - - link - - title - - div.relatedlinks - - strong.command - - div.visually-hidden - - img - - a.p-navigation__link - - a.contributor - - span.pre - - .sig-object - - '#module-apt em' - - '#module-apt strong' - - '#module-apt a.reference' - - a.reference[title^="apt."] - - a[href*="#apt"] - - '#module-pathops em' - - '#module-pathops strong' - - '#module-pathops a.reference' - - a.reference[title^="pathops."] - - a[href*="#pathops"] - - .no-spellcheck - - a.reference.internal[title] diff --git a/.docs/_dev/get_vale_conf.py b/.docs/_dev/get_vale_conf.py new file mode 100644 index 000000000..b09404ae6 --- /dev/null +++ b/.docs/_dev/get_vale_conf.py @@ -0,0 +1,151 @@ +#! /usr/bin/env python + +import os +import shutil +import subprocess +import tempfile +import sys +import logging +import argparse + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' +) + +DEV_DIR = os.path.join(os.getcwd(), "_dev") + +GITHUB_REPO = "canonical/documentation-style-guide" +GITHUB_CLONE_URL = f"https://github.com/{GITHUB_REPO}.git" + +# Source paths to copy from repo +VALE_FILE_LIST = [ + "styles/Canonical", + "styles/config/vocabularies/Canonical", + "styles/config/dictionaries", + "vale.ini" +] + +def clone_repo_and_copy_paths(file_source_dest, overwrite=False): + """ + Clone the repository to a temporary directory and copy required files + + Args: + file_source_dest: dictionary of file paths to copy from the repository, + and their destination paths + overwrite: boolean flag to overwrite existing files in the destination + + Returns: + bool: True if all files were copied successfully, False otherwise + """ + + if not file_source_dest: + logging.error("No files to copy") + return False + + # Create temporary directory on disk for cloning + temp_dir = tempfile.mkdtemp() + logging.info("Cloning repository <%s> to temporary directory: %s", GITHUB_REPO, temp_dir) + clone_cmd = ["git", "clone", "--depth", "1", GITHUB_CLONE_URL, temp_dir] + + try: + result = subprocess.run( + clone_cmd, + capture_output=True, + text=True, + check=True + ) + logging.debug("Git clone output: %s", result.stdout) + except subprocess.CalledProcessError as e: + logging.error("Git clone failed: %s", e.stderr) + return False + + # Copy files from the cloned repository to the destination paths + is_copy_success = True + for source, dest in file_source_dest.items(): + source_path = os.path.join(temp_dir, source) + + if not os.path.exists(source_path): + is_copy_success = False + logging.error("Source path not found: %s", source_path) + continue + + if not copy_files_to_path(source_path, dest, overwrite): + is_copy_success = False + logging.error("Failed to copy %s to %s", source_path, dest) + + # Clean up temporary directory + logging.info("Cleaning up temporary directory: %s", temp_dir) + shutil.rmtree(temp_dir) + + return is_copy_success + +def copy_files_to_path(source_path, dest_path, overwrite=False): + """ + Copy a file or directory from source to destination + + Args: + source_path: Path to the source file or directory + dest_path: Path to the destination + overwrite: Boolean flag to overwrite existing files in the destination + + Returns: + bool: True if copy was successful, False otherwise + """ + # Skip if source file doesn't exist + if not os.path.exists(source_path): + logging.warning("Source path not found: %s", source_path) + return False + + logging.info("Copying %s to %s", source_path, dest_path) + # Handle existing files + if os.path.exists(dest_path): + if overwrite: + logging.info(" Destination exists, overwriting: %s", dest_path) + if os.path.isdir(dest_path): + shutil.rmtree(dest_path) + else: + os.remove(dest_path) + else: + logging.info(" Destination exists, skip copying (use overwrite=True to replace): %s", + dest_path) + return True # Skip copying + + # Copy the source to destination + try: + if os.path.isdir(source_path): + # entire directory + shutil.copytree(source_path, dest_path) + else: + # individual files + shutil.copy2(source_path, dest_path) + return True + except (shutil.Error, OSError) as e: + logging.error("Copy failed: %s", e) + return False + +def parse_arguments(): + parser = argparse.ArgumentParser(description="Download Vale configuration files") + parser.add_argument("--no-overwrite", action="store_true", help="Don't overwrite existing files") + return parser.parse_args() + +def main(): + # Define local directory paths + vale_files_dict = {file: os.path.join(DEV_DIR, file) for file in VALE_FILE_LIST} + + # Parse command line arguments, default to overwrite_enabled = True + overwrite_enabled = not parse_arguments().no_overwrite + + # Download into /tmp through git clone + if not clone_repo_and_copy_paths(vale_files_dict, overwrite=overwrite_enabled): + logging.error("Failed to download files from repository") + return 1 + + logging.info("Download complete") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) # Keep return code diff --git a/.docs/.sphinx/pa11y.json b/.docs/_dev/pa11y.json similarity index 100% rename from .docs/.sphinx/pa11y.json rename to .docs/_dev/pa11y.json diff --git a/.docs/_dev/requirements.txt b/.docs/_dev/requirements.txt new file mode 100644 index 000000000..daeb7bad6 --- /dev/null +++ b/.docs/_dev/requirements.txt @@ -0,0 +1,34 @@ +# Canonical theme (still needed for Furo theme and custom templates) +canonical-sphinx~=0.6 + +# Extensions previously auto-loaded by canonical-sphinx +myst-parser~=4.0 # v5.0.0 causes version conflicts +sphinx-autobuild +sphinx-design==0.6.1 +sphinx-notfound-page~=1.1 +sphinx-reredirects==0.1.6 +sphinx-tabs~=3.5 +sphinxcontrib-jquery~=4.1 +sphinxext-opengraph~=0.13 +sphinx-rerediraffe>=0.0.3, <1.0.0 + +# Extra extensions, previously bundled as canonical-sphinx-extensions +sphinx-config-options~=0.1 +sphinx-contributor-listing~=0.1 +sphinx-filtered-toctree~=0.1 +sphinx-related-links~=0.1 +sphinx-roles~=0.1 +sphinx-terminal~=1.0 +sphinx-ubuntu-images~=0.1 +sphinx-youtube-links~=0.1 + +# Other dependencies +packaging~=26.1 +sphinxcontrib-svg2pdfconverter[CairoSVG]~=2.1 +sphinx-last-updated-by-git~=0.3 +sphinx-sitemap~=2.9 +sphinx-llm~=0.4 + +# charmlibs-specific extensions (not in the upstream pack) +sphinxcontrib-mermaid +sphinx-datatables>=0.3 diff --git a/.docs/.sphinx/_static/logos/juju-logo-no-text.png b/.docs/_static/logos/juju-logo-no-text.png similarity index 100% rename from .docs/.sphinx/_static/logos/juju-logo-no-text.png rename to .docs/_static/logos/juju-logo-no-text.png diff --git a/.docs/.sphinx/_static/project_specific.css b/.docs/_static/project_specific.css similarity index 100% rename from .docs/.sphinx/_static/project_specific.css rename to .docs/_static/project_specific.css diff --git a/.docs/.sphinx/_static/tag_click_search.js b/.docs/_static/tag_click_search.js similarity index 100% rename from .docs/.sphinx/_static/tag_click_search.js rename to .docs/_static/tag_click_search.js diff --git a/.docs/_templates/footer.html b/.docs/_templates/footer.html new file mode 100644 index 000000000..e3cce4a55 --- /dev/null +++ b/.docs/_templates/footer.html @@ -0,0 +1,38 @@ +
+
+ {%- if show_copyright %} + + {%- endif %} + {%- if license and license.name -%} + {%- if license.url -%} +
+ This project is licensed under {{ license.name }} +
+ {%- else -%} +
+ This project is licensed under {{ license.name }} +
+ {%- endif -%} + {%- endif -%} + + {%- if last_updated -%} +
+ {% trans last_updated=last_updated|e -%} + Last updated on {{ last_updated }} + {%- endtrans -%} +
+ {%- endif %} + +
+ +
diff --git a/.docs/_templates/header.html b/.docs/_templates/header.html new file mode 100644 index 000000000..1c4e18b2a --- /dev/null +++ b/.docs/_templates/header.html @@ -0,0 +1,72 @@ + diff --git a/.docs/conf.py b/.docs/conf.py index e5b1058b7..6a9b59ae9 100644 --- a/.docs/conf.py +++ b/.docs/conf.py @@ -1,6 +1,8 @@ import datetime +import os import pathlib import sys +import textwrap # local extensions sys.path.insert(0, str(pathlib.Path(__file__).parent / 'extensions')) @@ -60,10 +62,17 @@ "repo_folder": "/.docs/", "display_contributors": False, 'github_issues': 'enabled', # Required for feedback button + # Passes the top-level 'author' value to the theme + "author": author, + # Documentation license information + "license": { + "name": "CC-BY-SA 4.0", + "url": "https://creativecommons.org/licenses/by-sa/4.0/", + }, } # Template and asset locations -html_static_path = [".sphinx/_static"] -templates_path = [".sphinx/_templates"] +html_static_path = ["_static"] +templates_path = ["_templates"] # Sitemap configuration: https://sphinx-sitemap.readthedocs.io/ html_baseurl = 'https://documentation.ubuntu.com/charmlibs/' @@ -92,6 +101,24 @@ redirects = {} +############################ +# sphinx-llm configuration # +############################ + +# This description is included in llms.txt to provide some initial context for the +# documentation. +llms_txt_description = textwrap.dedent( + """\ + This is the documentation for charmlibs, the home of Canonical's charm libraries: + reusable Python packages for Juju charms, published on PyPI from the charmlibs + monorepo. + """ +) + +# The base URL for references built by sphinx-markdown-builder. +if os.environ.get("READTHEDOCS"): + markdown_http_base = html_baseurl + ########################### # Link checker exceptions # ########################### @@ -116,21 +143,24 @@ ######################## # NOTE: The canonical_sphinx extension is required for the starter pack. -# It automatically enables the following extensions: -# - custom-rst-roles -# - myst_parser -# - notfound.extension -# - related-links -# - sphinx_copybutton -# - sphinx_design -# - sphinx_reredirects -# - sphinx_tabs.tabs -# - sphinxcontrib.jquery -# - sphinxext.opengraph -# - terminal-output -# - youtube-links +# Since Sphinx Stack 2.0 (canonical-sphinx ~=0.6), it no longer bundles the +# extensions that were previously auto-enabled by canonical-sphinx[full]; +# only myst_parser is still activated automatically. Every other extension +# must now be declared explicitly below (and installed via +# _dev/requirements.txt). extensions = [ "canonical_sphinx", + # Previously auto-enabled by canonical-sphinx[full] + "notfound.extension", + "sphinx_design", + "sphinx_reredirects", + "sphinxcontrib.jquery", + "sphinxext.opengraph", + # Previously bundled as canonical-sphinx-extensions + "sphinx_related_links", + # llms.txt generation (Sphinx Stack 2.0) + "sphinx_llm.txt", + # charmlibs-specific extensions "sphinxcontrib.cairosvgconverter", "sphinxcontrib.mermaid", "sphinx_last_updated_by_git", @@ -167,11 +197,13 @@ # Adds custom CSS files, located under 'html_static_path' html_css_files = [ + "https://assets.ubuntu.com/v1/d86746ef-cookie_banner.css", "project_specific.css", ] # Adds custom JavaScript files, located under 'html_static_path' html_js_files = [ + "https://assets.ubuntu.com/v1/287a5e8f-bundle.js", "tag_click_search.js", ] diff --git a/.docs/how-to/migrate.md b/.docs/how-to/migrate.md index 3912c84fb..309382e50 100644 --- a/.docs/how-to/migrate.md +++ b/.docs/how-to/migrate.md @@ -175,7 +175,7 @@ This part is a bit trickier. With any luck, your library was previously developed in a placeholder charm that exists purely for library distribution. If your library's development and testing was tightly coupled to a real charm, this step will be more involved. You'll need to consider which tests can live alongside the library, and which only make sense with the charm. -You might want to add a simplified dummy charm to run some of the tests against. +You might want to add a simplified placeholder charm to run some of the tests against. ```{warning} Don't add `pytest` to your `pyproject.toml`. @@ -191,7 +191,7 @@ If your library wasn't tightly coupled to a real charm, these steps should be su 1. Add any unit test dependencies to the `unit` dependency group in your `pyproject.toml` (using `just add`). 2. Copy any relevant contents of your `conftest.py` to `tests/unit/conftest.py`. -3. Copy your library's existing unit test files to `tests/unit/`, along with any data files, dummy charms, and so on. +3. Copy your library's existing unit test files to `tests/unit/`, along with any data files, placeholder charms, and so on. 4. Correct the imports in those files. Replace imports like this: @@ -263,7 +263,7 @@ By default each test is treated as compatible with both substrates. Your library's reference documentation is automatically built from its docstrings and source code. On top of that, you can add tutorials, how-to guides, and explanations under `/docs`, and they'll be included in this documentation site under the respective categories. -Read more: {ref}`how-to-add-library-docs` +Read more: {ref}`how-to-add-library-docs` ```{note} There is no `reference` docs category -- reference documentation is generated from your library's docstrings. If your existing docs include hand-written reference material, fold the relevant content into your docstrings, or rework it into an explanation or how-to guide. diff --git a/.docs/scripts/diataxis_preprocessor.py b/.docs/scripts/diataxis_preprocessor.py index f9a959433..2ca09d6aa 100755 --- a/.docs/scripts/diataxis_preprocessor.py +++ b/.docs/scripts/diataxis_preprocessor.py @@ -149,7 +149,7 @@ def _build_sphinx_map(packages: list[dict[str, Any]]) -> dict[pathlib.PurePath, m: dict[pathlib.PurePath, str] = {} # Static .docs/ pages — walk all .md/.rst files (excluding build artifacts). - skip_dirs = {'_build', '.sphinx', '.save', 'scripts', 'tests', 'extensions'} + skip_dirs = {'_build', '_dev', '.save', 'scripts', 'tests', 'extensions'} for path in _DOCS_DIR.rglob('*'): if path.suffix not in ('.md', '.rst'): continue diff --git a/.docs/tutorial.md b/.docs/tutorial.md index 1fd8ba5a5..4a36d0beb 100644 --- a/.docs/tutorial.md +++ b/.docs/tutorial.md @@ -122,7 +122,7 @@ In principle we can put all our library code here, but it's good practice to use In Python, names prefixed with a single underscore are private. This isn't enforced technically (a user who knows your library layout can import private symbols), but semantically there are no stability guarantees when using private variables. -`charmlibs` follow semantic verisoning, so if we expose something publicly, we're promising to support it until at least our next major verson. +`charmlibs` follow semantic versioning, so if we expose something publicly, we're promising to support it until at least our next major version. Let's add a private module where our feature implementation will live. Create the file `src/charmlibs/uptime/_uptime.py`, then copy the copyright header from `src/charmlibs/uptime/__init__.py` to `_uptime.py`. Next, add the following code to `_uptime.py`: @@ -269,7 +269,7 @@ def test_hostname(): Now run `just functional uptime` to verify that our new functional test passes. -Realisticaly, our `uptime` function isn't really a good fit for functional tests, as its interaction with the external world is limited to fast and reliable system calls. +Realistically, our `uptime` function isn't really a good fit for functional tests, as its interaction with the external world is limited to fast and reliable system calls. In this case, the library's core functionality would be well exercised by unit tests alone. We should still use full Juju integration tests to ensure everything is working correctly in the charm context, but in this case we could drop the functional tests altogether by removing the `tests/functional` directory. The functional tests would then show as skipped in CI. diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 53b954ccd..98320079b 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -43,9 +43,7 @@ jobs: id: spellcheck-step if: success() || failure() continue-on-error: true - uses: canonical/documentation-workflows/spellcheck@6f9c05eae99ca9fff05250810bba2a71a380ebd1 # main - with: - working-directory: .docs + run: uvx --from=rust-just just docs spelling # Disable link checking until we get a more robust solution. # The current approach often fails due to rate limiting and timeouts. # - name: Links diff --git a/docs.just b/docs.just index 9875cead2..d818c5b61 100644 --- a/docs.just +++ b/docs.just @@ -10,8 +10,8 @@ Build the docs, (re)generating reference docs for all packages, or just those sp `just docs html interfaces/foo`. """)] html *packages: _diataxis_docs (_packages packages) - uvx --with-requirements=.sphinx/requirements.txt --from=sphinx \ - sphinx-build -b dirhtml -T -W --keep-going -d .sphinx/.doctrees -D language=en . '{{build_dir}}/html' + uvx --with-requirements=_dev/requirements.txt --from=sphinx \ + sphinx-build -b dirhtml -T -W --keep-going -d _dev/.doctrees -D language=en . '{{build_dir}}/html' [doc('Copy per-library diataxis docs into the Sphinx source tree and generate toctree include files.')] _diataxis_docs: @@ -37,14 +37,18 @@ _packages *packages: cmd = [ 'uvx', '--from', 'sphinx', - '--with-requirements', '.sphinx/requirements.txt', + '--with-requirements', '_dev/requirements.txt', '--with', str(ROOT / package), 'sphinx-build', '-T', '-W', '--keep-going', '-b', 'dirhtml', - '-d', '.sphinx/.doctrees', + '-d', '_dev/.doctrees', '-D', 'language=en', '-D', f'package={package}', + # llms.txt is only generated in the final combined pass (the `html` recipe). + # Leaving it enabled here triggers a nested base-mode build that regenerates the + # interface reference docs, deleting the placeholder this package pass depends on. + '-D', 'llms_txt_enabled=0', '-D', 'suppress_warnings=ref.ref,ref.doc,myst.xref_missing', '.', '{{build_dir}}/html', ] @@ -57,25 +61,53 @@ help: [doc('Watch, build, and serve the docs -- does not rebuild package reference docs automatically.')] run: _packages - uvx --with-requirements=.sphinx/requirements.txt --from=sphinx \ - sphinx-autobuild --watch .. --ignore '**/generated/*' -b dirhtml . '{{build_dir}}/html' + uvx --with-requirements=_dev/requirements.txt --from=sphinx \ + sphinx-autobuild --watch .. --ignore '**/generated/*' -b dirhtml -D llms_txt_enabled=0 . '{{build_dir}}/html' [doc('Check links.')] linkcheck *sphinx_args: _packages - uvx --with-requirements=.sphinx/requirements.txt --from=sphinx \ + uvx --with-requirements=_dev/requirements.txt --from=sphinx \ sphinx-build -b linkcheck . '{{build_dir}}' {{sphinx_args}} -[doc('Check spelling.')] -spelling: html - uvx pyspelling -c .sphinx/spellingcheck.yaml -j $(nproc) +[doc('Check spelling with Vale (flags US-English misspellings not in the custom wordlist).')] +spelling *paths: (_vale '.Name=="Canonical.000-US-spellcheck"' paths) + +[doc('Check Canonical style guide compliance (errors only) with Vale.')] +vale *paths: (_vale '.Level=="error" and .Name!="Canonical.500-Repeated-words" and .Name!="Canonical.000-US-spellcheck"' paths) + +[doc('Check for non-inclusive language with Vale.')] +woke *paths: (_vale '.Name=="Canonical.400-Enforce-inclusive-terms"' paths) + +# Run Vale over the tracked .md/.rst files (or those matching the given pathspecs), +# applying the custom wordlist and the given filter. +# Downloads the Canonical Vale style guide (styles + vale.ini) if needed. +# Generated reference docs are untracked, so they are excluded automatically. +_vale filter *paths: + #!/usr/bin/env bash + set -xeuo pipefail + [ -f _dev/vale.ini ] || python3 _dev/get_vale_conf.py + accept='_dev/styles/config/vocabularies/Canonical/accept.txt' + cp "$accept" "$accept.bak" + trap 'mv "$accept.bak" "$accept"' EXIT + cat .custom_wordlist.txt >> "$accept" + if [ -n "{{paths}}" ]; then + files=$(git ls-files -- {{paths}} | grep -E '\.(md|rst)$' || true) + else + files=$(git ls-files '*.md' '*.rst') + fi + if [ -z "$files" ]; then + echo 'No tracked .md/.rst files to check.' + exit 0 + fi + echo "$files" | xargs uvx --from 'vale~=3.13' vale --config=_dev/vale.ini --filter='{{filter}}' [doc('Remove files created by building the docs.')] clean: git clean -fx '{{build_dir}}' - rm -rf .sphinx/.doctrees - rm -rf .sphinx/node_modules/ - rm -rf .sphinx/styles - rm -rf .sphinx/vale.ini + rm -rf _dev/.doctrees + rm -rf _dev/node_modules/ + rm -rf _dev/styles + rm -rf _dev/vale.ini # generated reference docs rm -rf reference/generated # package reference docs @@ -92,11 +124,11 @@ clean: [doc('Run `pyright` for local sphinx extensions.')] ext-static *pyrightargs: - cd tests && uvx --with-requirements=../.sphinx/requirements.txt --with=pytest \ + cd tests && uvx --with-requirements=../_dev/requirements.txt --with=pytest \ pyright {{pyrightargs}} [doc('Run unit tests with `coverage` for local sphinx extensions.')] ext-unit +flags='-rA': - uvx --with-requirements=.sphinx/requirements.txt --with=pytest \ + uvx --with-requirements=_dev/requirements.txt --with=pytest \ coverage run --omit='test_*.py' --source=extensions,scripts -m pytest --tb=native -vv {{flags}} tests uvx coverage report diff --git a/pyproject.toml b/pyproject.toml index 006f1420f..c9e89448f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,7 @@ exclude = [ "__pycache__", "*.egg_info", "htmlcov", - ".docs/.sphinx", + ".docs/_dev", ".docs/conf.py", ".template/\\{\\{ cookiecutter.project_slug \\}\\}", ] From ab459a408be74f31b85f2b86e3ee64ae71e11024 Mon Sep 17 00:00:00 2001 From: James Garner Date: Mon, 15 Jun 2026 14:26:35 +1200 Subject: [PATCH 39/65] ci: thin justfile (#518) This PR moves the implementation of the repository's developer tooling from inline `justfile` recipes to a separate `.scripts/just.py` script. This allows us to cover the recipes with `ruff` and `pyright`, and makes many things less tortuous (having a unified approach to `uv run`, for example), and several things that were previously too painful to write are now possible and have been implemented: - Groups are only added to `uv run` if the library defines them -- this means that CI won't break if a library deletes an unused dependency group. - The default Python version is not statically defined, but determined based on the library's minimum `requires-python`, which resolves #449 - The separate `interface.just` module is no longer required for `just interface init`, instead we can write `just init --interface`, since recipes can now trivially define boolean flags. - Accept an explicit `--unsafe-fixes` flag for the `format` recipe, forwarded to `ruff check`. - We only run `ruff check --diff` to print the diff if `ruff check` fails during `fast-lint`. - Detailed help for each recipe and argument. The `justfile` remains in place for compatibility and because it provides the useful feature that `just ` from anywhere in the repository will execute (and run from the repository root). Each recipe is now just a single line call to `just.py`. Resolves #405 --------- Co-authored-by: Tony Meyer --- .docs/how-to/migrate.md | 4 +- .docs/tutorial.md | 2 +- .example/tests/integration/pack.sh | 4 +- .github/workflows/ci.yaml | 6 +- .github/workflows/test-package.yaml | 14 +- .scripts/just.py | 579 ++++++++++++++++++ .scripts/ls.py | 2 +- .scripts/pyproject.toml | 5 + .scripts/tests/test_just.py | 232 +++++++ .template/hooks/post_gen_project.py | 2 +- .../tests/integration/pack.sh | 4 +- .tutorial/tests/integration/pack.sh | 4 +- .workshop/jammy.yaml | 2 +- .workshop/noble.yaml | 2 +- .workshop/resolute.yaml | 2 +- AGENTS.md | 4 +- CONTRIBUTING.md | 2 +- interface.just | 10 - interfaces/.example/tests/integration/pack.sh | 4 +- justfile | 243 ++------ pathops/CONTRIBUTING.md | 8 +- 21 files changed, 905 insertions(+), 230 deletions(-) create mode 100755 .scripts/just.py create mode 100644 .scripts/pyproject.toml create mode 100644 .scripts/tests/test_just.py delete mode 100644 interface.just diff --git a/.docs/how-to/migrate.md b/.docs/how-to/migrate.md index 309382e50..b8bb16727 100644 --- a/.docs/how-to/migrate.md +++ b/.docs/how-to/migrate.md @@ -32,7 +32,7 @@ just init :sync: interface ```bash -just interface init +just init --interface ``` ```` ````` @@ -80,7 +80,7 @@ If `init` fails because the directory already exists, take a look at the directo It may be that the interface definitions are already hosted in the repo under `interfaces//interface`. In this case: - Temporarily move the `` directory. -- Re-run `just interface init`. +- Re-run `just init --interface`. - Then add the `interface` subdirectory to your newly generated project. You'll also want to check for any config files under the old `` directory (for example, a `ruff.toml` file), and incorporate any applicable settings into your project's `pyproject.toml`. diff --git a/.docs/tutorial.md b/.docs/tutorial.md index 4a36d0beb..6b6000f48 100644 --- a/.docs/tutorial.md +++ b/.docs/tutorial.md @@ -68,7 +68,7 @@ Get started by running `just init`, and provide the requested information intera - The author information to display on PyPI (e.g. `The team at Canonical`). ````{tip} -If you're working on an interface library, run `just interface init` instead. +If you're working on an interface library, run `just init --interface` instead. This will create the library directory under the `interfaces/` directory and set it up to use the `charmlibs.interfaces` namespace. ```` diff --git a/.example/tests/integration/pack.sh b/.example/tests/integration/pack.sh index b5c7113ff..76789d6f9 100755 --- a/.example/tests/integration/pack.sh +++ b/.example/tests/integration/pack.sh @@ -9,8 +9,8 @@ # Environment variables: # $CHARMLIBS_SUBSTRATE will have the value 'k8s' or 'machine' (set by pack-k8s or pack-machine) # In CI, $CHARMLIBS_TAG is set based on pyproject.toml:tool.charmlibs.integration.tags -# For local testing, set $CHARMLIBS_TAG directly or use the tag variable. For example: -# just tag=24.04 pack-k8s some extra args +# For local testing, set $CHARMLIBS_TAG directly or pass --tag. For example: +# just pack-k8s --tag 24.04 some extra args set -xueo pipefail TMP_DIR=".tmp" # clean temporary directory where charms will be packed diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 807046f3c..0339e30dc 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -133,7 +133,9 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@v7 - name: Run unit tests for the repository tooling scripts - run: uvx --from rust-just just scripts-unit + run: uvx --from rust-just just _scripts-unit + - name: Run static analysis for the repository tooling scripts + run: uvx --from rust-just just _scripts-static template-and-example-in-sync: runs-on: ubuntu-latest @@ -161,7 +163,7 @@ jobs: - uses: astral-sh/setup-uv@v7 - working-directory: interfaces run: | - uvx --from rust-just just interface init --no-input project_slug=example-interface + uvx --from rust-just just init --interface --no-input project_slug=example-interface rm -rf .example mv example-interface .example # check if tracked files changed, or any new files were added diff --git a/.github/workflows/test-package.yaml b/.github/workflows/test-package.yaml index ed7ad0281..206274742 100644 --- a/.github/workflows/test-package.yaml +++ b/.github/workflows/test-package.yaml @@ -97,7 +97,7 @@ jobs: substrates=() for substrate in k8s machine; do set +e # don't abort if the next process has a non-zero exit code - uvx --from rust-just just python="$PYTHON" integration-$substrate "$PACKAGE" --collect-only -q + uvx --from rust-just just integration-$substrate --python "$PYTHON" "$PACKAGE" --collect-only -q case $? in 0) substrates+=($substrate);; 5) echo "No tests for $substrate";; @@ -141,7 +141,7 @@ jobs: - name: Run static analysis and other checks env: PYTHON: ${{ matrix.python }} - run: uvx --from rust-just just python="$PYTHON" lint "$PACKAGE" + run: uvx --from rust-just just lint --python "$PYTHON" "$PACKAGE" unit: needs: init @@ -162,7 +162,7 @@ jobs: - name: Run unit tests env: PYTHON: ${{ matrix.python }} - run: uvx --from rust-just just python="$PYTHON" unit "$PACKAGE" + run: uvx --from rust-just just unit --python "$PYTHON" "$PACKAGE" functional: needs: init @@ -195,11 +195,11 @@ jobs: - name: Run functional tests if: matrix.sudo == 'no-sudo' - run: uvx --from rust-just just python=python3 functional "$PACKAGE" + run: uvx --from rust-just just functional --python python3 "$PACKAGE" - name: Run functional tests with sudo if: matrix.sudo != 'no-sudo' - run: sudo env "PATH=$PATH" uvx --from rust-just just python=python3 functional "$PACKAGE" + run: sudo env "PATH=$PATH" uvx --from rust-just just functional --python python3 "$PACKAGE" integration: needs: init @@ -235,7 +235,7 @@ jobs: env: TAG: ${{ matrix.tag }} SUBSTRATE: ${{ matrix.substrate }} - run: uvx --from rust-just just tag="$TAG" "pack-$SUBSTRATE" "$PACKAGE" + run: uvx --from rust-just just "pack-$SUBSTRATE" --tag "$TAG" "$PACKAGE" - name: Run Juju integration tests id: integration @@ -243,7 +243,7 @@ jobs: PYTHON: ${{ needs.init.outputs.min_python_version }} RECIPE: integration-${{ matrix.substrate }} TAG: ${{ matrix.tag }} - run: uvx --from rust-just just python="$PYTHON" tag="$TAG" "$RECIPE" "$PACKAGE" + run: uvx --from rust-just just "$RECIPE" --python "$PYTHON" --tag "$TAG" "$PACKAGE" - name: Upload Charmcraft logs if: ${{ !cancelled() && (steps.pack.conclusion != 'skipped' || steps.integration.conclusion != 'skipped') }} diff --git a/.scripts/just.py b/.scripts/just.py new file mode 100755 index 000000000..bb2f40eb8 --- /dev/null +++ b/.scripts/just.py @@ -0,0 +1,579 @@ +#!/usr/bin/env -S uv run --script --no-project + +# /// script +# requires-python = ">=3.12" +# dependencies = [] +# /// + +# ruff: noqa: I001 # tomllib is first-party in 3.11+ + +# Copyright 2026 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Developer tooling for charmlibs; run `just` for quickstart info.""" + +from __future__ import annotations + +import argparse +import os +import pathlib +import re +import shlex +import shutil +import subprocess +import sys +import textwrap +import tomllib +import typing + +if typing.TYPE_CHECKING: + from collections.abc import Callable, Sequence + from types import FunctionType + from typing import IO + + _F = typing.TypeVar('_F', bound=FunctionType) + +# `.scripts/just.py` -> repo root is two parents up. +REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent +TEST_REQUIREMENTS = REPO_ROOT / 'test-requirements.txt' + +DEFAULT_PYTHON = '3.10' + +# ANSI escape codes for formatting the messages. +BOLD = '\033[1m' +NORMAL = '\033[0m' +CYAN = '\033[36m' +# Quick start message printed when called with no arguments. +QUICK_START = f""" +{BOLD}Charmlibs is Canonical's charm library monorepo{NORMAL} + +{BOLD}List all commands with {CYAN}just help{NORMAL}{BOLD}, or:{NORMAL} +- Create a new package: {CYAN}just init{NORMAL} or {CYAN}just init --interface{NORMAL} +- Add a dependency to a package: {CYAN}just add {NORMAL} +- Lint, unit test, and build docs for a package: {CYAN}just check {NORMAL} +- Run {CYAN}ruff{NORMAL} for all packages: {CYAN}just fast-lint{NORMAL} + +{BOLD}Run individual checks for a package:{NORMAL} +- {CYAN}just lint {NORMAL} (fix errors with {CYAN}just format {NORMAL}) +- {CYAN}just unit {NORMAL} +- {CYAN}just functional {NORMAL} (may require extra software like {CYAN}pebble{NORMAL}) + +{BOLD}Run integration tests{NORMAL} (requires a Juju controller and a cloud): +- Pack: {CYAN}just pack-k8s {NORMAL} or {CYAN}just pack-machine {NORMAL} +- Run: {CYAN}just integration-k8s {NORMAL} or {CYAN}just integration-machine{NORMAL} + +{BOLD}Build the docs: {CYAN}just docs{NORMAL} +- For specific packages only: {CYAN}just docs html {NORMAL} +""".strip() + +# Mapping of recipe names to their functions, populated by the `_register` decorator. +# `just help` prints the recipes in registration order. +RECIPES: dict[str, Callable[[list[str]], int]] = {} + + +def _register(fn: _F) -> _F: + RECIPES[fn.__name__.removeprefix('_').replace('_', '-')] = fn + return fn + + +def main(argv: list[str]) -> int: + """Dispatch `argv` to the named recipe, returning its exit code.""" + if not argv or argv[0] in ('-h', '--help'): + print(QUICK_START) + return 0 + name, *args = argv + func = RECIPES.get(name) + if func is None: + print(f'Unknown recipe: {name!r}\n', file=sys.stderr) + print(QUICK_START) + return 2 + return func(args) + + +@_register +def help(argv: list[str]) -> int: # noqa: A001 + """Describe usage and list the available recipes.""" + _parser(help).parse_args(argv) + print('All recipes require `uv` to be available.\n') + print('Available recipes:') + width = max(len(name) for name in RECIPES) + for name, func in RECIPES.items(): + if func.__name__.startswith('_'): + continue # Skip private recipes. + summary = (func.__doc__ or '').splitlines()[0] if func.__doc__ else '' + print(f' {name:<{width}} {summary}') + return 0 + + +@_register +def init(argv: list[str]) -> int: + """Scaffold a new charmlibs package interactively (forwards extra args to cookiecutter).""" + parser = _parser(init) + parser.add_argument( + '--interface', + action='store_true', + help='Scaffold a charmlibs.interfaces package instead of a general charmlibs package.', + ) + args, cookiecutter_args = parser.parse_known_args(argv) + if args.interface: + print( + f'✨{BOLD}IMPORTANT{NORMAL}✨ The project name should be the canonical interface' + f' name, as used in {CYAN}charmcraft.yaml{NORMAL} files.' + ) + else: + print( + f'✨{BOLD}IMPORTANT{NORMAL}✨ The project name should be the import package name,' + f' without the {CYAN}charmlibs.{NORMAL} namespace.' + ) + print('You can press enter to accept the default, shown in brackets.') + template = REPO_ROOT / '.template' + cmd = ['uvx', 'cookiecutter'] + if args.interface: + cmd.extend(['--output-dir', 'interfaces']) + cmd.append(template.name) + if args.interface: + cmd.append('_interface=True') + cmd.extend(cookiecutter_args) + _run(cmd, env={**os.environ, 'CHARMLIBS_TEMPLATE': str(template.resolve())}) + return 0 + + +@_register +def add(argv: list[str]) -> int: + """Run `uv add` for a package, respecting repo-level version constraints. + + Example: `add pathops 'pydantic>=2'` adds a constrained dependency to pathops. + """ + parser = _parser(add) + parser.add_argument('package', help='Path from the repo root to the package, e.g. `pathops`.') + args, uv_args = parser.parse_known_args(argv) + cmd = ['uv', 'add', '--constraints', TEST_REQUIREMENTS, *uv_args] + _run(cmd, cwd=REPO_ROOT / args.package) + return 0 + + +@_register +def check(argv: list[str]) -> int: + """`lint`, `unit` test, and build the `docs` for a package.""" + args = _package_parser(check).parse_args(argv) + python = _resolve_python(args.package, args.python) + if failures := lint([args.package, '--python', python]): + sys.exit(failures) + for cmd in _coverage_cmds(REPO_ROOT / args.package, 'unit', python, ['-rA']): + _run(cmd, cwd=REPO_ROOT / args.package, env=_coverage_env()) + _run(['just', 'docs', 'html', args.package]) + return 0 + + +@_register +def format(argv: list[str]) -> int: # noqa: A001 + """Run `ruff check --fix` and `ruff format`, modifying files in place.""" + parser = _parser(format) + parser.add_argument( + 'path', nargs='?', default='.', help='Path to format, defaults to the repo.' + ) + parser.add_argument( + '--unsafe-fixes', + action='store_true', + help='Forward `--unsafe-fixes` to `ruff check`, applying fixes marked as unsafe.', + ) + args = parser.parse_args(argv) + ruff = ['uv', 'run', '--only-group=fast-lint', 'ruff'] + _run([*ruff, 'format', args.path]) + check = [*ruff, 'check', '--fix'] + if args.unsafe_fixes: + check.append('--unsafe-fixes') + _run([*check, args.path]) + return 0 + + +# --- Linting recipes --------------------------------------------------------------------------- + + +@_register +def lint(argv: list[str]) -> int: + """Run fast linting (`ruff`) and static analysis (`pyright`) for a package.""" + args, pyright_args = _package_parser(lint).parse_known_args(argv) + python = _resolve_python(args.package, args.python) + failures = _fast_lint(args.package) + if _static(args.package, python, pyright_args) != 0: + failures += 1 + return failures + + +@_register +def fast_lint(argv: list[str]) -> int: + """Run `ruff`, failing afterwards if any errors are found.""" + parser = _parser(fast_lint) + parser.add_argument('path', nargs='?', default='.', help='Path to lint, defaults to the repo.') + args = parser.parse_args(argv) + return _fast_lint(args.path) + + +@_register +def static(argv: list[str]) -> int: + """Run `pyright` static analysis for a package.""" + args, pyright_args = _package_parser(static).parse_known_args(argv) + python = _resolve_python(args.package, args.python) + return _static(args.package, python, pyright_args) + + +def _fast_lint(path: str) -> int: + """Run `ruff check` and `ruff format --diff`, returning the number of failing commands. + + The `ruff check --diff` output (the fixes that would resolve `ruff check` issues) is printed + for information, but never counts as a failure. + """ + ruff = ['uv', 'run', '--only-group=fast-lint', 'ruff'] + failures = 0 + if _run([*ruff, 'check', path], check=False) != 0: + _run([*ruff, 'check', '--diff', path], check=False) + failures += 1 + if _run([*ruff, 'format', '--diff', path], check=False) != 0: + failures += 1 + return failures + + +def _static(package: str, python: str, pyright_args: Sequence[str]) -> int: + """Run `pyright` for a package against the given Python version, returning its exit code.""" + pkg_dir = REPO_ROOT / package + cmd = ['--with', 'pytest-interface-tester'] if pkg_dir.parent.name == 'interfaces' else [] + cmd.extend(['pyright', f'--pythonversion={python}', *pyright_args]) + groups = ['lint', 'unit', 'functional', 'integration'] + return _uv_run(cmd, pkg_dir=pkg_dir, python=python, groups=groups, check=False) + + +# --- Coverage recipes --------------------------------------------------------------------------- + + +@_register +def unit(argv: list[str]) -> int: + """Run unit tests with `coverage` for a package.""" + args, pytest_args = _package_parser(unit).parse_known_args(argv) + python = _resolve_python(args.package, args.python) + cmds = _coverage_cmds(REPO_ROOT / args.package, 'unit', python, pytest_args or ['-rA']) + for cmd in cmds: + _run(cmd, cwd=REPO_ROOT / args.package, env=_coverage_env()) + return 0 + + +@_register +def functional(argv: list[str]) -> int: + """Run functional tests with `coverage` for a package.""" + args, pytest_args = _package_parser(functional).parse_known_args(argv) + python = _resolve_python(args.package, args.python) + cmds = _coverage_cmds(REPO_ROOT / args.package, 'functional', python, pytest_args or ['-rA']) + joined = ' && '.join(shlex.join(str(part) for part in cmd) for cmd in cmds) + script = textwrap.dedent( + f""" + set -xueo pipefail + if [ -e tests/functional/setup.sh ]; then + source ./tests/functional/setup.sh + fi + set +e # Allow the command to fail. + {joined} + returncode=$? + set -e # Exit on error again. + if [ -e tests/functional/teardown.sh ]; then + source ./tests/functional/teardown.sh + fi + exit "$returncode" + """ + ).strip() + _run(['bash', '-c', script], cwd=REPO_ROOT / args.package, env=_coverage_env()) + return 0 + + +def _coverage_cmds(pkg_dir: pathlib.Path, suite: str, python: str, pytest_args: list[str]): + """Return cmds for `coverage run -m pytest` and `coverage report` for package and suite.""" + data_file_arg = f'--data-file=.report/coverage-{suite}-{python}.db' + run = [ + *('coverage', 'run', data_file_arg, '--source=src', '-m'), + *('pytest', '--tb=native', '-vv', f'tests/{suite}', *pytest_args), + ] + run_cmd = _uv_cmd(run, pkg_dir=pkg_dir, python=python, groups=[suite]) + report = ['coverage', 'report', data_file_arg] + report_cmd = _uv_cmd(report, pkg_dir=pkg_dir, python=python, groups=[suite]) + return run_cmd, report_cmd + + +@_register +def combine_coverage(argv: list[str]) -> int: + """Combine a package's `coverage` reports.""" + args = _package_parser(combine_coverage).parse_args(argv) + pkg_dir = REPO_ROOT / args.package + python = _resolve_python(args.package, args.python) + env = _coverage_env() + data_files: list[str] = [] + for test_id in 'unit', 'functional': + file = f'.report/coverage-{test_id}-{python}.db' + if (pkg_dir / file).exists(): + data_files.append(file) + + def uv(cmd: list[str]) -> None: + _uv_run(cmd, pkg_dir=pkg_dir, python=python, env=env) + + # Combine reports and generate XML. + data_file_arg = f'--data-file=.report/coverage-all-{python}.db' + uv(['coverage', 'combine', '--keep', data_file_arg, *data_files]) + uv(['coverage', 'xml', data_file_arg, '-o', f'.report/coverage-all-{python}.xml']) + # Rebuild the HTML report from scratch (let coverage recreate the directory). + html_dir = f'.report/htmlcov-all-{python}' + shutil.rmtree(pkg_dir / html_dir, ignore_errors=True) + uv(['coverage', 'html', data_file_arg, '--show-contexts', f'--directory={html_dir}']) + # Print the report last. + uv(['coverage', 'report', data_file_arg]) + return 0 + + +def _coverage_env() -> dict[str, str]: + """Return the environment with `COVERAGE_RCFILE` pointing at the repo `pyproject.toml`.""" + return {**os.environ, 'COVERAGE_RCFILE': str(REPO_ROOT / 'pyproject.toml')} + + +# --- Pack recipes ------------------------------------------------------------------------------ + + +@_register +def pack_k8s(argv: list[str]) -> int: + """Pack Kubernetes charm(s) for a package's Juju integration tests.""" + return _pack(pack_k8s, 'k8s', argv) + + +@_register +def pack_machine(argv: list[str]) -> int: + """Pack machine charm(s) for a package's Juju integration tests.""" + return _pack(pack_machine, 'machine', argv) + + +def _pack(fn: FunctionType, substrate: str, argv: list[str]) -> int: + """Pack the package's integration-test charm(s) for the given substrate.""" + parser = _parser(fn) + parser.add_argument( + '--tag', + default=os.environ.get('CHARMLIBS_TAG', ''), + help='Value for the CHARMLIBS_TAG environment var (defaults to $CHARMLIBS_TAG).', + ) + parser.add_argument('package', help='Path from the repo root to the package, e.g. `pathops`.') + args, pack_args = parser.parse_known_args(argv) + integration_dir = REPO_ROOT / args.package / 'tests' / 'integration' + env = {**os.environ, 'CHARMLIBS_SUBSTRATE': substrate, 'CHARMLIBS_TAG': args.tag} + return _run(['./pack.sh', *pack_args], cwd=integration_dir, env=env) + + +# --- Integration recipes ------------------------------------------------------------------------ + + +@_register +def integration_k8s(argv: list[str]) -> int: + """Run a package's Kubernetes Juju integration tests.""" + return _integration(integration_k8s, 'k8s', argv) + + +@_register +def integration_machine(argv: list[str]) -> int: + """Run a package's machine Juju integration tests.""" + return _integration(integration_machine, 'machine', argv) + + +def _integration(fn: FunctionType, substrate: str, argv: list[str]) -> int: + """Run the package's Juju integration tests for the given substrate.""" + parser = _package_parser(fn) + parser.add_argument( + '--tag', + default=os.environ.get('CHARMLIBS_TAG', ''), + help='Value for the CHARMLIBS_TAG environment var (defaults to $CHARMLIBS_TAG).', + ) + args, pytest_args = parser.parse_known_args(argv) + return _uv_run( + [ + *('pytest', '--tb=native', '-vv'), + *('-m', {'k8s': 'not machine_only', 'machine': 'not k8s_only'}[substrate]), + 'tests/integration', + *(pytest_args or ['-rA']), + ], + pkg_dir=REPO_ROOT / args.package, + python=_resolve_python(args.package, args.python), + groups=['integration'], + env={**os.environ, 'CHARMLIBS_SUBSTRATE': substrate, 'CHARMLIBS_TAG': args.tag}, + ) + + +# --- Other recipes ----------------------------------------------------------------------------- + + +@_register +def interfaces_json(argv: list[str]) -> int: + """Generate `interfaces/index.json` from the interface libraries.""" + _parser(interfaces_json).parse_args(argv) # supports `-h` + outputs = [ + 'name', + 'version', + 'lib', + 'lib_url', + 'docs_url', + 'summary', + 'description', + 'tags', + 'status', + ] + cmd: list[str | pathlib.Path] = ['.scripts/ls.py', 'interfaces', '--indent-json'] + for output in outputs: + cmd.extend(['--output', output]) + with (REPO_ROOT / 'interfaces' / 'index.json').open('w') as f: + _run(cmd, stdout=f) + return 0 + + +@_register +def _scripts_unit(argv: list[str]) -> int: + """Run the unit tests for the repository tooling in `.scripts/`.""" + _parser(_scripts_unit).parse_args(argv) + _run([ + *('uv', 'run', '--with-requirements', TEST_REQUIREMENTS, '--python', '3.12'), + *('pytest', '--tb=native', '-vv', '.scripts/tests', *(argv or ['-rA'])), + ]) + return 0 + + +@_register +def _scripts_static(argv: list[str]) -> int: + """Run `pyright` static analysis for the repository tooling in `.scripts/`.""" + _parser(_scripts_static).parse_args(argv) + cmd = [ + 'uv', + 'run', + '--no-project', + f'--with-requirements={TEST_REQUIREMENTS}', + '--with=pyyaml', + '--python=3.12', + 'pyright', + ] + _run(cmd, cwd=REPO_ROOT / '.scripts') + return 0 + + +# --- Parser ------------------------------------------------------------------------------------ + + +def _parser(fn: FunctionType) -> argparse.ArgumentParser: + """Return an `ArgumentParser` for recipe `fn`, deriving prog and description from it.""" + prog = f'just {fn.__name__.replace("_", "-")}' + return argparse.ArgumentParser(prog=prog, description=fn.__doc__) + + +def _package_parser(fn: FunctionType) -> argparse.ArgumentParser: + """Return an `ArgumentParser` with the common `--python` and `package` arguments.""" + parser = _parser(fn) + parser.add_argument( + '--python', + default=None, + help="Python version to use, e.g. `3.12` (defaults to the package's minimum).", + ) + parser.add_argument('package', help='Path from the repo root to the package, e.g. `pathops`.') + return parser + + +# --- Resolve Python version -------------------------------------------------------------------- + + +def _resolve_python(package: str, python: str | None) -> str: + """Return the Python version to test `package` with. + + If `python` is `None`, return the higher of `DEFAULT_PYTHON` + and the package's minimum Python version. + """ + if python: + return python + minimum = _requires_python_minimum(REPO_ROOT / package) + return max(DEFAULT_PYTHON, minimum, key=lambda s: tuple(int(p) for p in s.split('.'))) + + +def _requires_python_minimum(pkg_dir: pathlib.Path) -> str: + """Return the `major.minor` lower bound of `pkg_dir`'s `requires-python`, e.g. `'3.14159'`.""" + pyproject_toml = tomllib.loads((pkg_dir / 'pyproject.toml').read_text()) + requires_python = pyproject_toml['project']['requires-python'] + regex = ( + r'(?:>=|~=)' # a `>=` or `~=` operator: the ones that set a lower bound + r'\s*' # optional whitespace between the operator and the version + r'(\d+\.\d+)' # capture just `major.minor`, stopping before any patch component + ) + match = re.search(regex, requires_python) + if match is None: + raise ValueError(f"Couldn't find minimum version in requires-python: {requires_python!r}") + return match.group(1) + + +# --- uv run ------------------------------------------------------------------------------------ + + +def _uv_run( + args: Sequence[str | pathlib.Path], + *, + pkg_dir: pathlib.Path, + python: str, + groups: Sequence[str] = (), + env: dict[str, str] | None = None, + check: bool = True, + stdout: IO[str] | None = None, +) -> int: + """Run `uv run ... ` in `pkg_dir`, returning the exit code.""" + cmd = _uv_cmd(args, pkg_dir=pkg_dir, python=python, groups=groups) + return _run(cmd, cwd=pkg_dir, env=env, check=check, stdout=stdout) + + +def _run( + cmd: Sequence[str | pathlib.Path], + *, + cwd: pathlib.Path = REPO_ROOT, + env: dict[str, str] | None = None, + check: bool = True, + stdout: IO[str] | None = None, +) -> int: + """Echo and run a command, returning its exit code.""" + env = dict(os.environ if env is None else env) + env.pop('VIRTUAL_ENV', None) # Don't propagate script's ephemeral venv. + print([str(part) for part in cmd], file=sys.stderr, flush=True) + returncode = subprocess.call(cmd, cwd=cwd, env=env, stdout=stdout) + if check and returncode != 0: + sys.exit(returncode) + return returncode + + +def _uv_cmd( + args: Sequence[str | pathlib.Path], + *, + pkg_dir: pathlib.Path, + python: str, + groups: Sequence[str] = (), +) -> list[str | pathlib.Path]: + """Build a `uv run ... ` command list to be executed in `pkg_dir`.""" + cmd = ['uv', 'run', '--with-requirements', TEST_REQUIREMENTS, '--python', python] + if (pkg_dir / 'uv.lock').exists(): + cmd.append('--locked') + available = _dependency_groups(pkg_dir) + for group in groups: + if group in available: + cmd.extend(['--group', group]) + return [*cmd, *args] + + +def _dependency_groups(pkg_dir: pathlib.Path) -> set[str]: + """Return the PEP 735 dependency group names declared in `pyproject.toml`.""" + pyproject_toml = tomllib.loads((pkg_dir / 'pyproject.toml').read_text()) + return set(pyproject_toml.get('dependency-groups', ())) + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/.scripts/ls.py b/.scripts/ls.py index d7ccd547a..5283dae34 100755 --- a/.scripts/ls.py +++ b/.scripts/ls.py @@ -66,7 +66,7 @@ class Info: description: str = '' status: str = '' schema_path: str = '' - docs: dict[str, list[str]] = dataclasses.field(default_factory=dict) + docs: dict[str, list[str]] = dataclasses.field(default_factory=dict[str, list[str]]) tags: list[str] = dataclasses.field(default_factory=list[str]) def to_dict(self, *fields: str) -> dict[str, object]: diff --git a/.scripts/pyproject.toml b/.scripts/pyproject.toml new file mode 100644 index 000000000..2a93171c7 --- /dev/null +++ b/.scripts/pyproject.toml @@ -0,0 +1,5 @@ +[tool.pyright] +typeCheckingMode = "strict" +reportPrivateUsage = false +reportUnnecessaryTypeIgnoreComment = "error" + diff --git a/.scripts/tests/test_just.py b/.scripts/tests/test_just.py new file mode 100644 index 000000000..0fc0f4ff1 --- /dev/null +++ b/.scripts/tests/test_just.py @@ -0,0 +1,232 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ruff: noqa: D103 (function docstrings) + +"""Unit tests for the just script.""" + +import pathlib +from unittest.mock import patch + +import just +import pytest + + +class TestUVCmd: + test_reqs = pathlib.Path(__file__).parent.parent.parent / 'test-requirements.txt' + + @pytest.mark.parametrize( + ('groups', 'args'), + [ + (['one'], ['--group', 'one']), + (['one', 'two'], ['--group', 'one', '--group', 'two']), + ([], []), + (['missing'], []), + (['one', 'two', 'missing'], ['--group', 'one', '--group', 'two']), + ], + ) + def test_ok(self, tmp_path: pathlib.Path, groups: list[str], args: list[str]): + (tmp_path / 'pyproject.toml').write_text('[dependency-groups]\none = []\ntwo = []') + assert just._uv_cmd([], pkg_dir=tmp_path, python='fakepy', groups=groups) == [ + 'uv', + 'run', + '--with-requirements', + self.test_reqs, + '--python', + 'fakepy', + *args, + ] + + def test_with_uv_lock(self, tmp_path: pathlib.Path): + (tmp_path / 'pyproject.toml').touch() + (tmp_path / 'uv.lock').touch() + result = just._uv_cmd(['pytest'], pkg_dir=tmp_path, python='3.12', groups=[]) + assert '--locked' in result + assert result == [ + 'uv', + 'run', + '--with-requirements', + self.test_reqs, + '--python', + '3.12', + '--locked', + 'pytest', + ] + + def test_with_python(self, tmp_path: pathlib.Path): + (tmp_path / 'pyproject.toml').touch() + result = just._uv_cmd(['pytest'], pkg_dir=tmp_path, python='foo', groups=[]) + assert '--python' in result + assert result == [ + 'uv', + 'run', + '--with-requirements', + self.test_reqs, + '--python', + 'foo', + 'pytest', + ] + + +class TestDependencyGroups: + def test_ok(self, tmp_path: pathlib.Path): + (tmp_path / 'pyproject.toml').write_text(""" +[dependency-groups] +foo = [] +bar = ["pytest"] +baz = [] +""") + assert just._dependency_groups(tmp_path) == {'foo', 'bar', 'baz'} + + def test_no_groups_table(self, tmp_path: pathlib.Path): + (tmp_path / 'pyproject.toml').touch() + assert just._dependency_groups(tmp_path) == set() + + def test_empty_groups_table(self, tmp_path: pathlib.Path): + (tmp_path / 'pyproject.toml').write_text('[dependency-groups]\n') + assert just._dependency_groups(tmp_path) == set() + + +class TestRequiresPythonMinimum: + @pytest.mark.parametrize( + ('requires', 'expected'), + [ + ('>=3.11', '3.11'), + ('~=3.10.2', '3.10'), + ('>=3.10,<4.0', '3.10'), + ('>=3.9,!=3.9.*,<4.0', '3.9'), # No resolution, just regex. + ], + ) + def test_ok(self, tmp_path: pathlib.Path, requires: str, expected: str): + (tmp_path / 'pyproject.toml').write_text(f'[project]\nrequires-python = "{requires}"') + assert just._requires_python_minimum(tmp_path) == expected + + def test_no_minimum(self, tmp_path: pathlib.Path): + (tmp_path / 'pyproject.toml').write_text('[project]\nrequires-python = "<3.12"') + with pytest.raises(ValueError, match='minimum'): + just._requires_python_minimum(tmp_path) + + +class TestResolvePython: + def test_explicit_immediately_returns(self): + assert just._resolve_python('', 'foo') == 'foo' + + @pytest.mark.parametrize( + ('minimum', 'default', 'expected'), + [ + ('1.0', '0.0', '1.0'), + ('0.42', '2.0', '2.0'), + ('3.10', '3.10', '3.10'), + ], + ) + def test_resolve_python_from_minimum(self, minimum: str, default: str, expected: str): + with ( + patch('just._requires_python_minimum', return_value=minimum), + patch('just.DEFAULT_PYTHON', default), + ): + assert just._resolve_python('', None) == expected + + +class TestCoverageCmds: + def test_ok(self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(just, '_uv_cmd', lambda cmd, *_, **__: cmd) # type: ignore + (tmp_path / 'pyproject.toml').touch() + run_cmd, report_cmd = just._coverage_cmds( + tmp_path, 'fakesuite', 'fakepy', ['--fake-opt', 'fake-val'] + ) + assert run_cmd[:2] == ['coverage', 'run'] + assert '--data-file=.report/coverage-fakesuite-fakepy.db' in run_cmd + assert 'tests/fakesuite' in run_cmd + assert '--fake-opt' in run_cmd + assert 'fake-val' in run_cmd + assert report_cmd[:2] == ['coverage', 'report'] + assert '--data-file=.report/coverage-fakesuite-fakepy.db' in report_cmd + + +class TestMain: + def test_ok(self): + result = just.main(['help']) + assert result == 0 + + def test_no_args(self): + result = just.main([]) + assert result == 0 + + def test_unknown_recipe(self): + result = just.main(['unknown-command']) + assert result > 0 + + +class TestParser: + def test_ok(self): + parser = just._parser(just.help) + assert parser.prog == 'just help' + + +class TestPackageParser: + def test_ok(self): + """_package_parser adds --python and package positional arg.""" + parser = just._package_parser(just.unit) + args = parser.parse_args(['--python', 'py', 'foo']) + assert args.python == 'py' + assert args.package == 'foo' + + def test_defaults(self): + """_package_parser defaults --python to None and package to first arg.""" + parser = just._package_parser(just.unit) + args = parser.parse_args(['foo']) + assert args.python is None + assert args.package == 'foo' + + +class TestRun: + def test_ok(self): + with patch('subprocess.call', return_value=0): + assert just._run([], check=True) == 0 + + def test_check_exits_on_failure(self): + with patch('subprocess.call', return_value=1), pytest.raises(SystemExit): + just._run([], check=True) + + def test_check_false_returns_code(self): + with patch('subprocess.call', return_value=42): + assert just._run([], check=False) == 42 + + def test_removes_virtual_env(self): + with patch('subprocess.call', return_value=0) as mock_call: + just._run([], env={'VIRTUAL_ENV': 'fake-env'}) + call_kwargs = mock_call.call_args.kwargs + assert 'VIRTUAL_ENV' not in call_kwargs['env'] + + +class TestUVRun: + def test_ok(self, tmp_path: pathlib.Path): + (tmp_path / 'pyproject.toml').touch() + with patch('just._run', return_value=0) as mock_run: + assert just._uv_run(['fake-cmd'], pkg_dir=tmp_path, python='3.12') == 0 + uv_run_cmd = mock_run.call_args.args[0] + assert uv_run_cmd[:2] == ['uv', 'run'] + assert 'fake-cmd' in uv_run_cmd + + +class TestCoverageEnv: + def test_ok(self): + env = just._coverage_env() + assert 'COVERAGE_RCFILE' in env + assert env['COVERAGE_RCFILE'].endswith('pyproject.toml') + + def test_preserves_existing(self): + with patch.dict('os.environ', {'EXISTING': 'value'}, clear=False): + env = just._coverage_env() + assert env['EXISTING'] == 'value' diff --git a/.template/hooks/post_gen_project.py b/.template/hooks/post_gen_project.py index 8f3514fa5..d05fc9aa7 100644 --- a/.template/hooks/post_gen_project.py +++ b/.template/hooks/post_gen_project.py @@ -25,7 +25,7 @@ ############################################################################## # evaluated with jinja2 by cookiecutter -# False by default, set to True by `just interface init` +# False by default, set to True by `just init --interface` if {{cookiecutter._interface}}: # noqa: F821 # Move src/charmlibs/* -> src/charmlibs/interfaces/* charmlibs = pathlib.Path('src', 'charmlibs') diff --git a/.template/{{ cookiecutter.project_slug }}/tests/integration/pack.sh b/.template/{{ cookiecutter.project_slug }}/tests/integration/pack.sh index b5c7113ff..76789d6f9 100755 --- a/.template/{{ cookiecutter.project_slug }}/tests/integration/pack.sh +++ b/.template/{{ cookiecutter.project_slug }}/tests/integration/pack.sh @@ -9,8 +9,8 @@ # Environment variables: # $CHARMLIBS_SUBSTRATE will have the value 'k8s' or 'machine' (set by pack-k8s or pack-machine) # In CI, $CHARMLIBS_TAG is set based on pyproject.toml:tool.charmlibs.integration.tags -# For local testing, set $CHARMLIBS_TAG directly or use the tag variable. For example: -# just tag=24.04 pack-k8s some extra args +# For local testing, set $CHARMLIBS_TAG directly or pass --tag. For example: +# just pack-k8s --tag 24.04 some extra args set -xueo pipefail TMP_DIR=".tmp" # clean temporary directory where charms will be packed diff --git a/.tutorial/tests/integration/pack.sh b/.tutorial/tests/integration/pack.sh index b5c7113ff..76789d6f9 100755 --- a/.tutorial/tests/integration/pack.sh +++ b/.tutorial/tests/integration/pack.sh @@ -9,8 +9,8 @@ # Environment variables: # $CHARMLIBS_SUBSTRATE will have the value 'k8s' or 'machine' (set by pack-k8s or pack-machine) # In CI, $CHARMLIBS_TAG is set based on pyproject.toml:tool.charmlibs.integration.tags -# For local testing, set $CHARMLIBS_TAG directly or use the tag variable. For example: -# just tag=24.04 pack-k8s some extra args +# For local testing, set $CHARMLIBS_TAG directly or pass --tag. For example: +# just pack-k8s --tag 24.04 some extra args set -xueo pipefail TMP_DIR=".tmp" # clean temporary directory where charms will be packed diff --git a/.workshop/jammy.yaml b/.workshop/jammy.yaml index b471a2687..6987b3eb3 100644 --- a/.workshop/jammy.yaml +++ b/.workshop/jammy.yaml @@ -5,4 +5,4 @@ sdks: actions: functional: | # Override Python with: workshop run --env PYTHON=python3.12 jammy -- functional ... - sudo just python="${PYTHON:-python3}" functional "$@" + sudo just functional --python "${PYTHON:-python3}" "$@" diff --git a/.workshop/noble.yaml b/.workshop/noble.yaml index 783f40579..b627b5938 100644 --- a/.workshop/noble.yaml +++ b/.workshop/noble.yaml @@ -5,4 +5,4 @@ sdks: actions: functional: | # Override Python with: workshop run --env PYTHON=python3.12 noble -- functional ... - sudo just python="${PYTHON:-python3}" functional "$@" + sudo just functional --python "${PYTHON:-python3}" "$@" diff --git a/.workshop/resolute.yaml b/.workshop/resolute.yaml index 35cb8b094..16e253196 100644 --- a/.workshop/resolute.yaml +++ b/.workshop/resolute.yaml @@ -5,4 +5,4 @@ sdks: actions: functional: | # Override Python with: workshop run --env PYTHON=python3.13 resolute -- functional ... - sudo just python="${PYTHON:-python3}" functional "$@" + sudo just functional --python "${PYTHON:-python3}" "$@" diff --git a/AGENTS.md b/AGENTS.md index 6654b5080..62775ea20 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -100,7 +100,7 @@ The `` argument is the path from the repo root, e.g. `pathops` or `inte | `just docs` | Alias for `just docs html` | | `just add ` | Add a dependency to a library | | `just init` | Scaffold a new general library | -| `just interface init` | Scaffold a new interface library | +| `just init --interface` | Scaffold a new interface library | Extra arguments to `just unit`, `just functional`, and `just integration-*` are passed through to pytest: @@ -206,7 +206,7 @@ Interface libraries manage the structured data that charms exchange over a Juju - Source under `src/charmlibs/interfaces//`. - Usually include a `testing/` subdirectory with a separate `charmlibs-interfaces--testing` package. This provides `relation_for_provider()` and `relation_for_requirer()` helpers for charm unit tests. - Typically have unit and integration tests but no functional tests (all meaningful interaction is through Juju). -- Use `just interface init` to scaffold. +- Use `just init --interface` to scaffold. Read more: [how to design relation interfaces](https://documentation.ubuntu.com/charmlibs/how-to/design-relation-interfaces/), [how to provide relation data for charm tests](https://documentation.ubuntu.com/charmlibs/how-to/provide-relation-data-for-charm-tests/). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index be9b852bf..c82743e4f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,7 +17,7 @@ Then run `just` or `just help` from anywhere in the repository for usage. # Adding a new library -Run `just init` to create a new general library, or `just interface init` for a new interface library. +Run `just init` to create a new general library, or `just init --interface` for a new interface library. We recommend following the [tutorial](https://documentation.ubuntu.com/charmlibs/tutorial) to learn how to add your library to the `charmlibs` monorepo. If you're migrating a library that was published elsewhere, read the [how-to guide for migrating an existing library to this repository](https://documentation.ubuntu.com/charmlibs/how-to/migrate/). diff --git a/interface.just b/interface.just deleted file mode 100644 index 5cb908c99..000000000 --- a/interface.just +++ /dev/null @@ -1,10 +0,0 @@ -# this is the first recipe in the file, so it will run if just is called without a recipe -[doc('Show help for the interface recipes.')] -help: - @just --list interface --unsorted - -[doc('Create the files for a new charmlibs.interfaces package interactively.')] -init *args: - @echo '✨{{BOLD}}IMPORTANT{{NORMAL}}✨ The project name should be the canonical interface name, as used in {{CYAN}}charmcraft.yaml{{NORMAL}} files.' - @echo 'You can press enter to accept the default, shown in brackets.' - @env CHARMLIBS_TEMPLATE=$(realpath .template) uvx cookiecutter --output-dir interfaces .template _interface=True {{args}} diff --git a/interfaces/.example/tests/integration/pack.sh b/interfaces/.example/tests/integration/pack.sh index b5c7113ff..76789d6f9 100755 --- a/interfaces/.example/tests/integration/pack.sh +++ b/interfaces/.example/tests/integration/pack.sh @@ -9,8 +9,8 @@ # Environment variables: # $CHARMLIBS_SUBSTRATE will have the value 'k8s' or 'machine' (set by pack-k8s or pack-machine) # In CI, $CHARMLIBS_TAG is set based on pyproject.toml:tool.charmlibs.integration.tags -# For local testing, set $CHARMLIBS_TAG directly or use the tag variable. For example: -# just tag=24.04 pack-k8s some extra args +# For local testing, set $CHARMLIBS_TAG directly or pass --tag. For example: +# just pack-k8s --tag 24.04 some extra args set -xueo pipefail TMP_DIR=".tmp" # clean temporary directory where charms will be packed diff --git a/justfile b/justfile index f8f6f4a5b..b45b8cf16 100644 --- a/justfile +++ b/justfile @@ -1,195 +1,62 @@ -mod interface # load interface module to expose interface subcommands mod docs # load docs module to expose docs subcommands set ignore-comments # don't print comment lines in recipes +set positional-arguments # forward recipe args to scripts as argv ("$@"), so quoting is preserved -# set on the commandline as needed, e.g. `just package=pathops python=3.10 unit` -python := '3.10' -# for integration tests, e.g. `just tag=24.04 pack-k8s` `just tag=foo integration-machine` -tag := env('CHARMLIBS_TAG', '') +# this is the first recipe in the file, so it will run if just is called without a recipe +_quick_start: + @.scripts/just.py -_uv_run_with_test_requirements := 'uv run --with-requirements ' + quote(join(justfile_dir(), 'test-requirements.txt')) + ' --python ' + python +help *args: + @.scripts/just.py help "$@" -# this is the first recipe in the file, so it will run if just is called without a recipe -_short_help: - @echo '{{BOLD}}Charmlibs is the Canonical charm libraries monorepo{{NORMAL}}' - @echo '' - @echo '{{BOLD}}List all commands with {{CYAN}}just help{{NORMAL}}{{BOLD}}, or:{{NORMAL}}' - @echo '- Create a new package: {{CYAN}}just init{{NORMAL}} or {{CYAN}}just interface init{{NORMAL}}' - @echo '- Run {{CYAN}}ruff{{NORMAL}} for all packages: {{CYAN}}just fast-lint{{NORMAL}}' - @echo '- Lint, unit test, and build docs for a package: {{CYAN}}just check {{NORMAL}}' - @echo '' - @echo '{{BOLD}}Run individual checks for a package:{{NORMAL}}' - @echo '- {{CYAN}}just lint {{NORMAL}} (fix errors with {{CYAN}}just format {{NORMAL}})' - @echo '- {{CYAN}}just unit {{NORMAL}}' - @echo '- {{CYAN}}just functional {{NORMAL}} (may require additional software like {{CYAN}}pebble{{NORMAL}})' - @echo '' - @echo '{{BOLD}}Run integration tests{{NORMAL}} (requires a Juju controller and a cloud):' - @echo '- Pack: {{CYAN}}just pack-k8s {{NORMAL}} or {{CYAN}}just pack-machine {{NORMAL}}' - @echo '- Run: {{CYAN}}just integration-k8s {{NORMAL}} or {{CYAN}}just integration-machine {{NORMAL}}' - @echo '' - @echo '{{BOLD}}Build the docs: {{CYAN}}just docs{{NORMAL}}' - @echo '- For specific packages only: {{CYAN}}just docs html {{NORMAL}}' - -[doc('Describe usage and list the available recipes.')] -help: - @echo 'All recipes require {{CYAN}}`uv`{{NORMAL}} to be available.' - @just --list --unsorted --list-submodules - -[doc('Create the files for a new charmlibs package interactively.')] init *args: - @echo '✨{{BOLD}}IMPORTANT{{NORMAL}}✨ The project name should be the import package name, without the {{CYAN}}charmlibs.{{NORMAL}} namespace.' - @echo 'You can press enter to accept the default, shown in brackets.' - @env CHARMLIBS_TEMPLATE=$(realpath .template) uvx cookiecutter .template {{args}} - -[doc('Run `ruff`, failing afterwards if any errors are found.')] -fast-lint path='.': - #!/usr/bin/env -S bash -xueo pipefail - FAILURES=0 - uv run --only-group=fast-lint ruff check '{{path}}' || ((FAILURES+=1)) - uv run --only-group=fast-lint ruff check --diff '{{path}}' || : 'Printed diff of changes to fix `ruff check` issues.' - uv run --only-group=fast-lint ruff format --diff '{{path}}' || ((FAILURES+=1)) - : "$FAILURES command(s) failed." - exit $FAILURES - -[doc('`lint`, `unit` test, and build the `docs` for a package.')] -check package: (lint package) (unit package) (docs::html package) - -[doc('Run `ruff check --fix` and `ruff --format`, modifying files in place.')] -format package='.': - uv run --only-group=fast-lint ruff format '{{package}}' - uv run --only-group=fast-lint ruff check --fix '{{package}}' - -[doc("Run `uv add` for package, respecting repo-level version constraints, e.g. `just add pathops 'pydantic>=2'`.")] -[positional-arguments] # pass recipe args to recipe script positionally (so we can get correct quoting) -add package +args: - #!/usr/bin/env -S bash -xueo pipefail - shift 1 # drop $1 (package) from $@ it's just +args - cd '{{package}}' - uv add --constraints {{quote(join(justfile_dir(), 'test-requirements.txt'))}} "${@}" - -[doc('Run linting and static analysis for a specific package, e.g. `just python=3.10 lint interfaces/tls-certificates`.')] -[positional-arguments] # pass recipe args to recipe script positionally (so we can get correct quoting) -lint package *pyright_args: - #!/usr/bin/env -S bash -xueo pipefail - shift 1 # drop $1 (package) from $@ it's just *args - FAILURES=0 - just --justfile='{{justfile()}}' python='{{python}}' fast-lint '{{package}}' || ((FAILURES+=$?)) - just --justfile='{{justfile()}}' python='{{python}}' static '{{package}}' "${@}" || ((FAILURES+=1)) - : "$FAILURES command(s) failed." - exit $FAILURES - -[doc('Run package specific static analysis only, e.g. `just python=3.10 static pathops`.')] -[positional-arguments] # pass recipe args to recipe script positionally (so we can get correct quoting) -static package *args: - #!/usr/bin/env -S bash -xueo pipefail - shift 1 # drop $1 (package) from $@ it's just *args - cd '{{package}}' - if [ -f uv.lock ]; then LOCKED='--locked'; else LOCKED=''; fi - {{_uv_run_with_test_requirements}} $LOCKED \ - --group lint --group unit --group functional --group integration \ - --with pytest-interface-tester \ - pyright --pythonversion='{{python}}' "${@}" - -[doc("Run unit tests with `coverage`, e.g. `just python=3.10 unit pathops`.")] -unit package +flags='-rA': (_coverage package 'unit' flags) - -[doc("Run functional tests with `coverage`, e.g. `just python=3.10 functional pathops`.")] -[positional-arguments] # pass recipe args to recipe script positionally to enable correct quoting when forwarding variadic args -functional package +flags='-rA': - #!/usr/bin/env -S bash -xueo pipefail - shift 1 # drop $1 (package) from $@ it's just +flags - cd '{{package}}' - if [ -e tests/functional/setup.sh ]; then - source ./tests/functional/setup.sh - fi - set +e # don't exit if the tests fail - just --justfile='{{justfile()}}' python='{{python}}' _coverage '{{package}}' functional "${@}" - EXITCODE=$? - set -e # do exit if anything goes wrong now - if [ -e tests/functional/teardown.sh ]; then - source ./tests/functional/teardown.sh - fi - exit $EXITCODE - -[doc("Use uv to install and run coverage for the specified package's tests.")] -_coverage package test_suite +flags: - #!/usr/bin/env -S bash -xueo pipefail - cd '{{package}}' - if [ -f uv.lock ]; then LOCKED='--locked'; else LOCKED=''; fi - export COVERAGE_RCFILE='{{justfile_directory()}}/pyproject.toml' - DATA_FILE=".report/coverage-$(basename {{test_suite}})-{{python}}.db" - {{_uv_run_with_test_requirements}} $LOCKED --group {{test_suite}} \ - coverage run --data-file="$DATA_FILE" --source='src' \ - -m pytest --tb=native -vv {{flags}} 'tests/{{test_suite}}' - {{_uv_run_with_test_requirements}} $LOCKED --group {{test_suite}} \ - coverage report --data-file="$DATA_FILE" - -[doc("Combine `coverage` reports, e.g. `just python=3.10 combine-coverage pathops`.")] -combine-coverage package: - #!/usr/bin/env -S bash -xueo pipefail - : 'Collect the coverage data files that exist for this package.' - cd '{{package}}' - if [ -f uv.lock ]; then LOCKED='--locked'; else LOCKED=''; fi - data_files=() - for test_id in unit functional juju; do - data_file=".report/coverage-$test_id-{{python}}.db" - if [ -e "$data_file" ]; then - data_files+=("$data_file") - fi - done - : 'Combine coverage.' - export COVERAGE_RCFILE='{{justfile_directory()}}/pyproject.toml' - DATA_FILE='.report/coverage-all-{{python}}.db' - HTML_DIR='.report/htmlcov-all-{{python}}' - {{_uv_run_with_test_requirements}} $LOCKED coverage combine --keep --data-file="$DATA_FILE" "${data_files[@]}" - {{_uv_run_with_test_requirements}} $LOCKED coverage xml --data-file="$DATA_FILE" -o '.report/coverage-all-{{python}}.xml' - rm -rf "$HTML_DIR" # let coverage create html directory from scratch - {{_uv_run_with_test_requirements}} $LOCKED coverage html --data-file="$DATA_FILE" --show-contexts --directory="$HTML_DIR" - {{_uv_run_with_test_requirements}} $LOCKED coverage report --data-file="$DATA_FILE" - -[doc("Execute pack script to pack Kubernetes charm(s) for Juju integration tests.")] -pack-k8s package *args: (_pack package 'k8s' args) - -[doc("Execute pack script to pack machine charm(s) for Juju integration tests.")] -pack-machine package *args: (_pack package 'machine' args) - -[doc("Execute the pack script for the given package, setting CHARMLIBS_SUBSTRATE and CHARMLIBS_TAG.")] -_pack package substrate *args: - #!/usr/bin/env -S bash -xueo pipefail - cd '{{package}}/tests/integration' - CHARMLIBS_SUBSTRATE='{{substrate}}' CHARMLIBS_TAG='{{tag}}' ./pack.sh {{args}} - -[doc("Run juju integration tests for packed k8s charm(s), setting CHARMLIBS_SUBSTRATE and CHARMLIBS_TAG, and selecting 'not machine_only'.")] -integration-k8s package +flags='-rA': (_integration package 'k8s' 'not machine_only' flags) - -[doc("Run juju integration tests for packed machine charm(s), setting CHARMLIBS_SUBSTRATE and CHARMLIBS_TAG, and selecting 'not k8s_only'.")] -integration-machine package +flags='-rA': (_integration package 'machine' 'not k8s_only' flags) - -[doc("Run juju integration tests. Requires `juju`.")] -_integration package substrate label +flags: - #!/usr/bin/env -S bash -xueo pipefail - cd '{{package}}' - if [ -f uv.lock ]; then LOCKED='--locked'; else LOCKED=''; fi - CHARMLIBS_SUBSTRATE={{substrate}} CHARMLIBS_TAG='{{tag}}' {{_uv_run_with_test_requirements}} $LOCKED --group integration \ - pytest --tb=native -vv -m '{{label}}' tests/integration {{flags}} - -[doc("Make .interfaces.json file.")] -interfaces-json: - .scripts/ls.py interfaces \ - --output name \ - --output version \ - --output lib \ - --output lib_url \ - --output docs_url \ - --output summary \ - --output description \ - --output tags \ - --output status \ - --indent-json \ - > interfaces/index.json - -[doc('Run unit tests for the repository tooling scripts in `.scripts/`.')] -scripts-unit +flags='-rA': - {{_uv_run_with_test_requirements}} \ - pytest --tb=native -vv {{flags}} .scripts/tests + @.scripts/just.py init "$@" + +fast-lint *args: + @.scripts/just.py fast-lint "$@" + +check *args: + @.scripts/just.py check "$@" + +format *args: + @.scripts/just.py format "$@" + +add *args: + @.scripts/just.py add "$@" + +lint *args: + @.scripts/just.py lint "$@" + +static *args: + @.scripts/just.py static "$@" + +unit *args: + @.scripts/just.py unit "$@" + +functional *args: + @.scripts/just.py functional "$@" + +combine-coverage *args: + @.scripts/just.py combine-coverage "$@" + +pack-k8s *args: + @.scripts/just.py pack-k8s "$@" + +pack-machine *args: + @.scripts/just.py pack-machine "$@" + +integration-k8s *args: + @.scripts/just.py integration-k8s "$@" + +integration-machine *args: + @.scripts/just.py integration-machine "$@" + +interfaces-json *args: + @.scripts/just.py interfaces-json "$@" + +_scripts-unit *args: + @.scripts/just.py scripts-unit "$@" + +_scripts-static *args: + @.scripts/just.py scripts-static "$@" \ No newline at end of file diff --git a/pathops/CONTRIBUTING.md b/pathops/CONTRIBUTING.md index 692221c91..924366a70 100644 --- a/pathops/CONTRIBUTING.md +++ b/pathops/CONTRIBUTING.md @@ -39,14 +39,14 @@ just check pathops Pathops is very concerned with `pathlib` compatibility across Python versions. Unit tests should be run with the Python versions supported by Ubuntu LTS releases (which charms will run with). Run the unit tests against these versions with: ```bash -just python=3.10 unit pathops -just python=3.12 unit pathops -just python=3.14 unit pathops +just unit --python 3.10 pathops +just unit --python 3.12 pathops +just unit --python 3.14 pathops ``` Also run static analysis the same way to ensure typing compatibility is preserved as intended. ```bash -just python= static pathops +just static --python pathops ``` Unit tests live under `tests/unit/`. They test `ContainerPath`, `LocalPath`, and `ensure_contents` without a real Pebble instance. `ContainerPath` is constructed using a dummy `ops.Container` backend (see `tests/unit/conftest.py`), and Pebble API calls are monkeypatched using the helpers in `tests/unit/utils.py`. From ad7ce579a2fa40f1e5e2f46211bb84dcba9a7a5a Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 15 Jun 2026 18:18:14 +0200 Subject: [PATCH 40/65] feat(istio-metadata): adds the istio-metadata library (#522) ## Add istio-metadata interface library Migrates the `istio-metadata` interface libraries from [`istio-k8s-operator`](https://github.com/canonical/istio-k8s-operator/) into charmlibs. ### New packages - **`istio-metadata`** (`charmlibs.interfaces.istio_metadata`): Interface for sharing istio control plane metadata (root namespace for eg) between the istio charm and consuming charms. ### What is this library for? This library provides the provider and requirer sides of the ``istio-metadata`` relation interface, used to transfer information about an instance of Istio (such as its root namespace) to charms that need to interface with Istio. It might be necessary for complex charms the spin up dynamic namespaces and mesh related resources autonomously from the istio charms. For eg. the charmed kubeflow stack, Currently its used by the [kiali-k8s-operator](https://github.com/canonical/kiali-k8s-operator/) ### Context This library is already in use so this would be a pure migration. --- .docs/reference/libs.yaml | 24 ++ CODEOWNERS | 3 + interfaces/index.json | 13 + interfaces/istio_metadata/CHANGELOG.md | 5 + interfaces/istio_metadata/README.md | 11 + .../istio_metadata/interface/v0/README.md | 40 +++ .../interface/v0/interface.yaml | 18 + .../istio_metadata/interface/v0/schema.py | 1 + interfaces/istio_metadata/pyproject.toml | 74 ++++ .../interfaces/istio_metadata/__init__.py | 90 +++++ .../istio_metadata/_istio_metadata.py | 145 ++++++++ .../interfaces/istio_metadata/_schema.py | 36 ++ .../interfaces/istio_metadata/_version.py | 15 + .../interfaces/istio_metadata/py.typed | 0 .../tests/unit/test_istio_metadata.py | 160 +++++++++ interfaces/istio_metadata/uv.lock | 322 ++++++++++++++++++ 16 files changed, 957 insertions(+) create mode 100644 interfaces/istio_metadata/CHANGELOG.md create mode 100644 interfaces/istio_metadata/README.md create mode 100644 interfaces/istio_metadata/interface/v0/README.md create mode 100644 interfaces/istio_metadata/interface/v0/interface.yaml create mode 120000 interfaces/istio_metadata/interface/v0/schema.py create mode 100644 interfaces/istio_metadata/pyproject.toml create mode 100644 interfaces/istio_metadata/src/charmlibs/interfaces/istio_metadata/__init__.py create mode 100644 interfaces/istio_metadata/src/charmlibs/interfaces/istio_metadata/_istio_metadata.py create mode 100644 interfaces/istio_metadata/src/charmlibs/interfaces/istio_metadata/_schema.py create mode 100644 interfaces/istio_metadata/src/charmlibs/interfaces/istio_metadata/_version.py create mode 100644 interfaces/istio_metadata/src/charmlibs/interfaces/istio_metadata/py.typed create mode 100644 interfaces/istio_metadata/tests/unit/test_istio_metadata.py create mode 100644 interfaces/istio_metadata/uv.lock diff --git a/.docs/reference/libs.yaml b/.docs/reference/libs.yaml index a81496922..155766b59 100644 --- a/.docs/reference/libs.yaml +++ b/.docs/reference/libs.yaml @@ -1830,6 +1830,30 @@ interfaces: description: Deprecated in favour of ``charmlibs.interfaces.gateway_metadata``. tags: - networking +- name: charmlibs.interfaces.istio_metadata + status: recommended + url: https://pypi.org/project/charmlibs-interfaces-istio-metadata + docs: https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio_metadata + src: https://github.com/canonical/charmlibs/tree/main/interfaces/istio_metadata + kind: PyPI + rel_name: istio_metadata + rel_url_charmhub: '' + rel_url_schema: https://documentation.ubuntu.com/charmlibs/reference/interfaces/istio_metadata/v0/ + description: Share Istio installation metadata (e.g. root namespace) between charms. + tags: + - networking +- name: charms.istio_k8s.istio_metadata + status: legacy + url: https://charmhub.io/istio-k8s/libraries/istio_metadata + docs: '' + src: https://github.com/canonical/istio-k8s-operator + kind: Charmhub + rel_name: istio_metadata + rel_url_charmhub: '' + rel_url_schema: '' + description: Deprecated in favour of ``charmlibs.interfaces.istio_metadata``. + tags: + - networking - name: dpcharmlibs.interfaces status: '' url: https://pypi.org/project/dpcharmlibs-interfaces diff --git a/CODEOWNERS b/CODEOWNERS index b3ae9479e..6559eeb7d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -92,6 +92,9 @@ # /interfaces/istio-ingress-route/ /interfaces/istio-ingress-route @canonical/service-mesh +# /interfaces/istio_metadata/ +/interfaces/istio_metadata @canonical/service-mesh + # /interfaces/istio-request-auth/ /interfaces/istio-request-auth/ @canonical/service-mesh diff --git a/interfaces/index.json b/interfaces/index.json index 3212139ca..18d3f5721 100644 --- a/interfaces/index.json +++ b/interfaces/index.json @@ -342,6 +342,19 @@ "tags": [], "status": "draft" }, + { + "name": "istio_metadata", + "version": "0", + "lib": "charmlibs.interfaces.istio_metadata", + "lib_url": "https://pypi.org/project/charmlibs-interfaces-istio-metadata", + "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/istio_metadata/", + "summary": "Share Istio installation metadata between charms.", + "description": "The `istio_metadata` interface allows an Istio control plane charm to share information about its installation (such as the root namespace) with other charms that need to interoperate with Istio.\nThe provider charm (the Istio control plane) publishes the metadata, and requirer charms read it to configure their own integration with Istio.", + "tags": [ + "networking" + ], + "status": "draft" + }, { "name": "jwt", "version": "0", diff --git a/interfaces/istio_metadata/CHANGELOG.md b/interfaces/istio_metadata/CHANGELOG.md new file mode 100644 index 000000000..668eae022 --- /dev/null +++ b/interfaces/istio_metadata/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 1.0.0 - 8 June 2026 + +Initial release. Migrated from `charms.istio_k8s.v0.istio_metadata` (v0.2). diff --git a/interfaces/istio_metadata/README.md b/interfaces/istio_metadata/README.md new file mode 100644 index 000000000..cc300b48a --- /dev/null +++ b/interfaces/istio_metadata/README.md @@ -0,0 +1,11 @@ +# charmlibs.interfaces.istio_metadata + +The `istio_metadata` interface library. + +To install, add `charmlibs-interfaces-istio-metadata` to your Python dependencies. Then in your Python code, import as: + +```py +from charmlibs.interfaces import istio_metadata +``` + +See the [reference documentation](https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio_metadata) for more. diff --git a/interfaces/istio_metadata/interface/v0/README.md b/interfaces/istio_metadata/interface/v0/README.md new file mode 100644 index 000000000..1fca386d9 --- /dev/null +++ b/interfaces/istio_metadata/interface/v0/README.md @@ -0,0 +1,40 @@ +# `istio_metadata/v0` + +## Overview + +The `istio_metadata` interface lets an **Istio control plane charm** share information about its installation with charms that need to interoperate with Istio. The provider publishes a small piece of metadata (currently the root namespace where Istio is installed); requirers read it to configure their integration with Istio. + +## Direction + +This is a unidirectional interface where the provider sends data and the requirer only receives data. There is no data sent back from the requirer side. + +```mermaid +flowchart TD + Provider -- root_namespace --> Requirer +``` + +## Behavior + +- **Provider (Istio control plane charm)**: Publishes the metadata to the application data bag, typically on `leader_elected` and on this relation's `relation_joined` events. Only the leader unit may write the application data bag. +- **Requirer**: Consumes the metadata when the relation is populated. The requirer is expected to use `limit: 1` for this relation, since the metadata describes a single Istio installation. + +## Relation Data + +[\[Pydantic Schema\]](./schema.py) + +On the provider side, the application data bag must contain the following field: + +- `root_namespace` (string, required): The root namespace where Istio is installed (for example, `istio-system`). + +### Provider + +#### Example + +```yaml + application-data: + root_namespace: istio-system +``` + +### Requirer + +N/A diff --git a/interfaces/istio_metadata/interface/v0/interface.yaml b/interfaces/istio_metadata/interface/v0/interface.yaml new file mode 100644 index 000000000..736311253 --- /dev/null +++ b/interfaces/istio_metadata/interface/v0/interface.yaml @@ -0,0 +1,18 @@ +name: istio_metadata +version: 0 +status: draft +lib: charmlibs.interfaces.istio_metadata +summary: Share Istio installation metadata between charms. +description: | + The `istio_metadata` interface allows an Istio control plane charm to share information about its installation (such as the root namespace) with other charms that need to interoperate with Istio. + The provider charm (the Istio control plane) publishes the metadata, and requirer charms read it to configure their own integration with Istio. + +requirers: + - name: kiali-k8s-operator + url: https://github.com/canonical/kiali-k8s-operator + +providers: + - name: istio-k8s-operator + url: https://github.com/canonical/istio-k8s-operator + +maintainer: service-mesh diff --git a/interfaces/istio_metadata/interface/v0/schema.py b/interfaces/istio_metadata/interface/v0/schema.py new file mode 120000 index 000000000..b2f1aead0 --- /dev/null +++ b/interfaces/istio_metadata/interface/v0/schema.py @@ -0,0 +1 @@ +../../src/charmlibs/interfaces/istio_metadata/_schema.py \ No newline at end of file diff --git a/interfaces/istio_metadata/pyproject.toml b/interfaces/istio_metadata/pyproject.toml new file mode 100644 index 000000000..e95f323c1 --- /dev/null +++ b/interfaces/istio_metadata/pyproject.toml @@ -0,0 +1,74 @@ +[project] +name = "charmlibs-interfaces-istio-metadata" +description = "The charmlibs.interfaces.istio_metadata package." +readme = "README.md" +requires-python = ">=3.10" +authors = [ + {name="Service Mesh"}, +] +license = "Apache-2.0" +classifiers = [ + "Programming Language :: Python :: 3", + "Intended Audience :: Developers", + "Operating System :: POSIX :: Linux", + "Development Status :: 5 - Production/Stable", +] +dynamic = ["version"] +dependencies = [ + "ops>=2.23.1,<4", + "pydantic>=2,<3", +] + +[dependency-groups] +lint = [ # installed for `just lint interfaces/istio_metadata` (unit, functional, and integration are also installed) + # "typing_extensions", +] +unit = [ # installed for `just unit interfaces/istio_metadata` + "ops[testing]", +] +functional = [ # installed for `just functional interfaces/istio_metadata` +] +integration = [ # installed for `just integration interfaces/istio_metadata` + "jubilant", +] + +[project.urls] +"Documentation" = "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio_metadata" +"Repository" = "https://github.com/canonical/charmlibs/tree/main/interfaces/istio_metadata" +"Issues" = "https://github.com/canonical/charmlibs/issues" +"Changelog" = "https://github.com/canonical/charmlibs/blob/main/interfaces/istio_metadata/CHANGELOG.md" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/charmlibs"] + +[tool.hatch.version] +path = "src/charmlibs/interfaces/istio_metadata/_version.py" + +[tool.ruff] +extend = "../../pyproject.toml" +src = ["src", "tests/unit"] # correctly sort local imports in tests + +[tool.ruff.lint.extend-per-file-ignores] +# add additional per-file-ignores here to avoid overriding repo-level config +"tests/**/*" = [ + # "E501", # line too long +] + +[tool.pyright] +extends = "../../pyproject.toml" +include = ["src", "tests"] +pythonVersion = "3.10" # check no python > 3.10 features are used + +[tool.charmlibs.functional] +ubuntu = [] # ubuntu versions to run functional tests with, e.g. "24.04" (defaults to just "latest") +pebble = [] # pebble versions to run functional tests with, e.g. "v1.0.0", "master" (defaults to no pebble versions) +sudo = false # whether to run functional tests with sudo (defaults to false) + +[tool.charmlibs.integration] +# tags to run integration tests with (defaults to running once with no tag, i.e. tags = ['']) +# Available in CI in tests/integration/pack.sh and integration tests as CHARMLIBS_TAG +tags = [] # Not used by the pack.sh and integration tests generated by the template diff --git a/interfaces/istio_metadata/src/charmlibs/interfaces/istio_metadata/__init__.py b/interfaces/istio_metadata/src/charmlibs/interfaces/istio_metadata/__init__.py new file mode 100644 index 000000000..9d29e6641 --- /dev/null +++ b/interfaces/istio_metadata/src/charmlibs/interfaces/istio_metadata/__init__.py @@ -0,0 +1,90 @@ +# Copyright 2026 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Istio metadata interface library. + +This library provides the provider and requirer sides of the ``istio_metadata`` +relation interface, used to transfer information about an instance of Istio +(such as its root namespace) to charms that need to interface with Istio. + +Provider usage:: + + from charmlibs.interfaces.istio_metadata import IstioMetadataProvider + + class FooCharm(CharmBase): + def __init__(self, framework): + super().__init__(framework) + self.istio_metadata = IstioMetadataProvider( + charm=self, + relation_mapping=self.model.relations, + app=self.app, + ) + + self.framework.observe(self.on.leader_elected, self._publish) + self.framework.observe( + self.on['istio-metadata'].relation_joined, self._publish + ) + + def _publish(self, _): + self.istio_metadata.publish(root_namespace='istio-system') + +The provider's ``charmcraft.yaml`` should declare:: + + provides: + istio-metadata: + interface: istio_metadata + +Requirer usage:: + + from charmlibs.interfaces.istio_metadata import IstioMetadataRequirer + + class FooCharm(CharmBase): + def __init__(self, framework): + super().__init__(framework) + self.istio_metadata = IstioMetadataRequirer( + self.model.relations, 'istio-metadata', + ) + + self.framework.observe( + self.on['istio-metadata'].relation_changed, self._on_changed + ) + self.framework.observe( + self.on['istio-metadata'].relation_broken, self._on_changed + ) + + def _on_changed(self, _): + data = self.istio_metadata.get_data() + ... + +The requirer's ``charmcraft.yaml`` should declare (with ``limit: 1``, since +``IstioMetadataRequirer`` is designed for relating to a single application):: + + requires: + istio-metadata: + limit: 1 + interface: istio_metadata +""" + +from ._istio_metadata import ( + IstioMetadataAppData, + IstioMetadataProvider, + IstioMetadataRequirer, +) +from ._version import __version__ as __version__ + +__all__ = [ + 'IstioMetadataAppData', + 'IstioMetadataProvider', + 'IstioMetadataRequirer', +] diff --git a/interfaces/istio_metadata/src/charmlibs/interfaces/istio_metadata/_istio_metadata.py b/interfaces/istio_metadata/src/charmlibs/interfaces/istio_metadata/_istio_metadata.py new file mode 100644 index 000000000..0862050fe --- /dev/null +++ b/interfaces/istio_metadata/src/charmlibs/interfaces/istio_metadata/_istio_metadata.py @@ -0,0 +1,145 @@ +# Copyright 2026 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Istio metadata interface implementation. + +Migrated from `charms.istio_k8s.v0.istio_metadata` in the istio-k8s-operator +repository. +""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + +from ._schema import IstioMetadataAppData + +if TYPE_CHECKING: + from ops import Application, CharmBase, RelationMapping + +log = logging.getLogger(__name__) + +DEFAULT_RELATION_NAME = 'istio-metadata' + + +class IstioMetadataRequirer: + """Endpoint wrapper for the requirer side of the istio-metadata relation.""" + + def __init__( + self, + relation_mapping: RelationMapping, + relation_name: str = DEFAULT_RELATION_NAME, + ) -> None: + """Initialize the IstioMetadataRequirer object. + + This object is for accessing data from relations that use the istio_metadata + interface. It **does not** autonomously handle the events associated with + that relation. It is up to the charm using this object to observe those + events as they see fit. Typically, that charm should observe this + relation's relation-changed event. + + This object is for interacting with a relation that has limit=1 set in + charmcraft.yaml. In particular, the get_data method will raise if more + than one related application is available. + + Args: + relation_mapping: The RelationMapping of a charm (typically + `self.model.relations` from within a charm object). + relation_name: The name of the wrapped relation. + """ + self._charm_relation_mapping = relation_mapping + self._relation_name = relation_name + + @property + def relations(self): + """Return relation instances for applications related to us on the monitored relation.""" + return self._charm_relation_mapping.get(self._relation_name, ()) + + def get_data(self) -> IstioMetadataAppData | None: + """Return data for at most one related application, raising if more than one is available. + + Useful for charms that always expect exactly one related application. It is + recommended that those charms also set limit=1 for that relation in + charmcraft.yaml. Returns None if no data is available (either because no + applications are related to us, or because the related application has not + sent data). + """ + relations = self.relations + if len(relations) == 0: + return None + if len(relations) > 1: + raise ValueError('Cannot get_info when more than one application is related.') + + # Being a little cautious here using getattr and get, since some funny things + # have happened with relation data in the past. + raw_data_dict = getattr(relations[0], 'data', {}).get(relations[0].app) + if not raw_data_dict: + return None + + return IstioMetadataAppData.model_validate(raw_data_dict) + + +class IstioMetadataProvider: + """The provider side of the istio-metadata relation.""" + + def __init__( + self, + charm: CharmBase, + relation_mapping: RelationMapping, + app: Application, + relation_name: str = DEFAULT_RELATION_NAME, + ): + """Initialize the IstioMetadataProvider object. + + This object is for serializing and sending data to a relation that uses the + istio_metadata interface - it does not automatically observe any events for + that relation. It is up to the charm using this to call publish when it is + appropriate to do so, typically on at least the charm's leader_elected event + and this relation's relation_joined event. + + Args: + charm: The charm instantiating this object. + relation_mapping: The RelationMapping of a charm (typically + `self.model.relations` from within a charm object). + app: This application. + relation_name: The name of the relation. + """ + self._charm = charm + self._charm_relation_mapping = relation_mapping + self._app = app + self._relation_name = relation_name + + @property + def relations(self): + """Return the applications related to us under the monitored relation.""" + return self._charm_relation_mapping.get(self._relation_name, ()) + + def publish(self, root_namespace: str): + """Post istio-metadata to all related applications. + + This method writes to the relation's app data bag, and thus should never be + called by a unit that is not the leader otherwise ops will raise an exception. + + Args: + root_namespace: The root namespace of the Istio deployment. + """ + # Only the leader unit can update the application data bag + if self._charm.unit.is_leader(): + data = IstioMetadataAppData(root_namespace=root_namespace).model_dump( + mode='json', by_alias=True, exclude_defaults=True, round_trip=True + ) + + for relation in self.relations: + databag = relation.data[self._app] + databag.update(data) diff --git a/interfaces/istio_metadata/src/charmlibs/interfaces/istio_metadata/_schema.py b/interfaces/istio_metadata/src/charmlibs/interfaces/istio_metadata/_schema.py new file mode 100644 index 000000000..8a5838f9e --- /dev/null +++ b/interfaces/istio_metadata/src/charmlibs/interfaces/istio_metadata/_schema.py @@ -0,0 +1,36 @@ +# Copyright 2026 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Schema models for the istio_metadata interface. + +This module is the source of truth for the databag wire format of the +``istio_metadata`` interface, and is symlinked from +``interface/v0/schema.py`` so it can be discovered as the canonical schema +on Charmhub. + +Its only third-party dependency is Pydantic. +""" + +from __future__ import annotations + +from pydantic import BaseModel, Field + + +class IstioMetadataAppData(BaseModel): + """Data model for the istio_metadata interface.""" + + root_namespace: str = Field( + description='The root namespace for the Istio installation.', + examples=['istio-system'], + ) diff --git a/interfaces/istio_metadata/src/charmlibs/interfaces/istio_metadata/_version.py b/interfaces/istio_metadata/src/charmlibs/interfaces/istio_metadata/_version.py new file mode 100644 index 000000000..a5443891f --- /dev/null +++ b/interfaces/istio_metadata/src/charmlibs/interfaces/istio_metadata/_version.py @@ -0,0 +1,15 @@ +# Copyright 2026 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = '1.0.0' diff --git a/interfaces/istio_metadata/src/charmlibs/interfaces/istio_metadata/py.typed b/interfaces/istio_metadata/src/charmlibs/interfaces/istio_metadata/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/interfaces/istio_metadata/tests/unit/test_istio_metadata.py b/interfaces/istio_metadata/tests/unit/test_istio_metadata.py new file mode 100644 index 000000000..cbea67062 --- /dev/null +++ b/interfaces/istio_metadata/tests/unit/test_istio_metadata.py @@ -0,0 +1,160 @@ +# Copyright 2026 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for the istio_metadata lib requirer and provider classes. + +Ported from `tests/unit/test_istio_metadata_lib.py` in the istio-k8s-operator +repository. +""" + +from __future__ import annotations + +from typing import ClassVar + +import ops +import pytest +from ops import CharmBase +from ops.testing import Context, Relation, State + +from charmlibs.interfaces.istio_metadata import ( + IstioMetadataAppData, + IstioMetadataProvider, + IstioMetadataRequirer, +) + +RELATION_NAME = 'app-data-relation' +INTERFACE_NAME = 'app-data-interface' + +# Note: if this is changed, the IstioMetadataAppData concrete classes below need to change +# their constructors to match. +SAMPLE_APP_DATA = IstioMetadataAppData(root_namespace='root-namespace') +SAMPLE_APP_DATA_2 = IstioMetadataAppData(root_namespace='root-namespace-2') + + +class IstioMetadataProviderCharm(CharmBase): + META: ClassVar = { + 'name': 'provider', + 'provides': {RELATION_NAME: {'interface': INTERFACE_NAME}}, + } + + def __init__(self, framework: ops.Framework): + super().__init__(framework) + self.relation_provider = IstioMetadataProvider( + charm=self, + relation_mapping=self.model.relations, + app=self.app, + relation_name=RELATION_NAME, + ) + + +@pytest.fixture +def istio_metadata_provider_context() -> Context[IstioMetadataProviderCharm]: + return Context(charm_type=IstioMetadataProviderCharm, meta=IstioMetadataProviderCharm.META) + + +class IstioMetadataRequirerCharm(CharmBase): + META: ClassVar = { + 'name': 'requirer', + 'requires': {RELATION_NAME: {'interface': INTERFACE_NAME}}, + } + + def __init__(self, framework: ops.Framework): + super().__init__(framework) + self.relation_requirer = IstioMetadataRequirer( + self.model.relations, relation_name=RELATION_NAME + ) + + +@pytest.fixture +def istio_metadata_requirer_context() -> Context[IstioMetadataRequirerCharm]: + return Context(charm_type=IstioMetadataRequirerCharm, meta=IstioMetadataRequirerCharm.META) + + +@pytest.mark.parametrize('data', [SAMPLE_APP_DATA, SAMPLE_APP_DATA_2]) +def test_istio_metadata_provider_sends_data_correctly( + data: IstioMetadataAppData, + istio_metadata_provider_context: Context[IstioMetadataProviderCharm], +): + """Tests that a charm using IstioMetadataProvider sends the correct data during publish.""" + # Arrange + istio_metadata_relation = Relation(RELATION_NAME, INTERFACE_NAME, local_app_data={}) + relations = [istio_metadata_relation] + state = State(relations=relations, leader=True) + + # Act + with istio_metadata_provider_context( + # construct a charm using an event that won't trigger anything here + istio_metadata_provider_context.on.update_status(), + state=state, + ) as manager: + # Manually do a .publish() to simulate the publish, but also do .run() to generate + # the state_out that we need to inspect the relation data. + manager.charm.relation_provider.publish(**data.model_dump()) + state_out = manager.run() + + # Assert + actual = IstioMetadataAppData.model_validate( + dict(state_out.get_relation(istio_metadata_relation.id).local_app_data) + ) + assert actual == data + + +@pytest.mark.parametrize( + ('relations', 'expected_data'), + [ + # no relations + ([], None), + # one empty relation + ( + [Relation(RELATION_NAME, INTERFACE_NAME, remote_app_data={})], + None, + ), + # one populated relation + ( + [ + Relation( + RELATION_NAME, + INTERFACE_NAME, + remote_app_data=SAMPLE_APP_DATA.model_dump(mode='json'), + ) + ], + SAMPLE_APP_DATA, + ), + ], +) +def test_istio_metadata_requirer_get_data( + relations: list[Relation], + expected_data: IstioMetadataAppData | None, + istio_metadata_requirer_context: Context[IstioMetadataRequirerCharm], +): + """Tests that IstioMetadataRequirer.get_data() returns correctly.""" + state = State(relations=relations, leader=False) + + with istio_metadata_requirer_context( + istio_metadata_requirer_context.on.update_status(), state=state + ) as manager: + charm = manager.charm + data = charm.relation_requirer.get_data() + assert _are_app_data_equal(data, expected_data) + + +def _are_app_data_equal( + data1: IstioMetadataAppData | None, data2: IstioMetadataAppData | None +) -> bool: + """Compare two IstioMetadataAppData objects, tolerating when one or both is None.""" + if data1 is None and data2 is None: + return True + if data1 is None or data2 is None: + return False + return data1.model_dump() == data2.model_dump() diff --git a/interfaces/istio_metadata/uv.lock b/interfaces/istio_metadata/uv.lock new file mode 100644 index 000000000..8888bdfe1 --- /dev/null +++ b/interfaces/istio_metadata/uv.lock @@ -0,0 +1,322 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "charmlibs-interfaces-istio-metadata" +source = { editable = "." } +dependencies = [ + { name = "ops" }, + { name = "pydantic" }, +] + +[package.dev-dependencies] +integration = [ + { name = "jubilant" }, +] +unit = [ + { name = "ops", extra = ["testing"] }, +] + +[package.metadata] +requires-dist = [ + { name = "ops", specifier = ">=2.23.1,<4" }, + { name = "pydantic", specifier = ">=2,<3" }, +] + +[package.metadata.requires-dev] +functional = [] +integration = [{ name = "jubilant" }] +lint = [] +unit = [{ name = "ops", extras = ["testing"] }] + +[[package]] +name = "jubilant" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/4b/faf53677e40ce2bf836d53d33f97b14a2a5083e25984115fa28a0dd33bb9/jubilant-1.10.0.tar.gz", hash = "sha256:c0040c5e897108ea4efb65bfd3d4bfa0ff62ef91f2a4c6ff6d6dbad8665cc992", size = 33147, upload-time = "2026-05-28T03:54:42.62Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/af/3ee025257fc62c0285833fa8c2c099a86a65a1d9e37eb4f41e301180deac/jubilant-1.10.0-py3-none-any.whl", hash = "sha256:af437ca48eddb6b9eace888ec60a299b003eb425b20009c21c2f49c86d126741", size = 34744, upload-time = "2026-05-28T03:54:41.074Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.42.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/1c/125e1c936c0873796771b7f04f6c93b9f1bf5d424cea90fda94a99f61da8/opentelemetry_api-1.42.1.tar.gz", hash = "sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716", size = 72296, upload-time = "2026-05-21T16:32:49.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/ca/9520cc1f3dfbbd03ac5903bbf55833e257bc64b1cf30fa8b0d6df374d821/opentelemetry_api-1.42.1-py3-none-any.whl", hash = "sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714", size = 61311, upload-time = "2026-05-21T16:32:28.822Z" }, +] + +[[package]] +name = "ops" +version = "3.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "pyyaml" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/60/ad398d889fd03b1b4f950fad54ad4d0cf7a81fde21f9866a8139c8f03684/ops-3.7.1.tar.gz", hash = "sha256:1765bf6d1cff376ea27608542e183b055c89f2c5f54bca602072bcc817195abc", size = 582424, upload-time = "2026-05-28T04:13:43.906Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/20/742e720af65f9ad23ae0cfe140502a10b2e74da6c63773f3999dad793661/ops-3.7.1-py3-none-any.whl", hash = "sha256:559dc6770e551da5f4b9686a5605ed927c140182eb38bcc31c1c531c2f98923a", size = 212723, upload-time = "2026-05-28T04:13:39.845Z" }, +] + +[package.optional-dependencies] +testing = [ + { name = "ops-scenario" }, +] + +[[package]] +name = "ops-scenario" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ops" }, + { name = "pyyaml" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/06/705b452b9145107978eb0400a1e751839ed1ec59dc152fa45a2e5fba0ea3/ops_scenario-8.7.1.tar.gz", hash = "sha256:25b24c7c612b8e089ad05f24a47c138999d1cc0a2683e0f7c74e0520c7bb6043", size = 78576, upload-time = "2026-05-28T04:13:45.292Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/45/c78501e1b8203207b7c43f62233b9fea3be5015f5230ce53d7deceb5d7a3/ops_scenario-8.7.1-py3-none-any.whl", hash = "sha256:01a8cee03c57a826b2dd9692571731d6307327cace7ac7865d9420e1136e7860", size = 69401, upload-time = "2026-05-28T04:13:41.39Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, + { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, + { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, + { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "websocket-client" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, +] From 6770499392fc82db4f25f15a1be792118379a95e Mon Sep 17 00:00:00 2001 From: James Garner Date: Tue, 16 Jun 2026 17:29:42 +1200 Subject: [PATCH 41/65] docs: update project URLs for Charm Tech libs and release (#528) This PR updates the project URLs for Charm Tech's libs to reflect current practices: - Documentation link to the package reference docs. - Deep repository link to the appropriate directory. - Changelog link to file in repository. It also updates the changelogs and library versions for release. --- apt/CHANGELOG.md | 4 ++++ apt/pyproject.toml | 2 +- apt/src/charmlibs/apt/__init__.py | 2 +- passwd/CHANGELOG.md | 4 ++++ passwd/pyproject.toml | 4 +++- passwd/src/charmlibs/passwd/_version.py | 2 +- pathops/CHANGELOG.md | 4 ++++ pathops/pyproject.toml | 2 +- pathops/src/charmlibs/pathops/_version.txt | 2 +- snap/CHANGELOG.md | 4 ++++ snap/pyproject.toml | 2 +- snap/src/charmlibs/snap/_version.py | 2 +- sysctl/CHANGELOG.md | 4 ++++ sysctl/pyproject.toml | 4 +++- sysctl/src/charmlibs/sysctl/_version.py | 2 +- systemd/CHANGELOG.md | 4 ++++ systemd/pyproject.toml | 4 ++-- systemd/src/charmlibs/systemd/_version.py | 2 +- 18 files changed, 41 insertions(+), 13 deletions(-) diff --git a/apt/CHANGELOG.md b/apt/CHANGELOG.md index 3844e25ff..1e47ca1ae 100644 --- a/apt/CHANGELOG.md +++ b/apt/CHANGELOG.md @@ -1,3 +1,7 @@ +# 1.0.0.post1 - 16 June 2026 + +Update project URLs. + # 1.0.0.post0 - 14 October 2025 Update project URLs. diff --git a/apt/pyproject.toml b/apt/pyproject.toml index ccfefe9ca..290a90547 100644 --- a/apt/pyproject.toml +++ b/apt/pyproject.toml @@ -26,7 +26,7 @@ integration = [] [project.urls] "Documentation" = "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/apt/" -"Repository" = "https://github.com/canonical/charmlibs" +"Repository" = "https://github.com/canonical/charmlibs/tree/main/apt" "Issues" = "https://github.com/canonical/charmlibs/issues" "Changelog" = "https://github.com/canonical/charmlibs/blob/main/apt/CHANGELOG.md" diff --git a/apt/src/charmlibs/apt/__init__.py b/apt/src/charmlibs/apt/__init__.py index 4ef5a5758..016b12b38 100644 --- a/apt/src/charmlibs/apt/__init__.py +++ b/apt/src/charmlibs/apt/__init__.py @@ -117,7 +117,7 @@ import opentelemetry.trace -__version__ = '1.0.0.post0' +__version__ = '1.0.0.post1' logger = logging.getLogger(__name__) tracer = opentelemetry.trace.get_tracer(__name__) diff --git a/passwd/CHANGELOG.md b/passwd/CHANGELOG.md index be8351558..f9fbe273b 100644 --- a/passwd/CHANGELOG.md +++ b/passwd/CHANGELOG.md @@ -1,3 +1,7 @@ +# 1.0.1.post0 - 16 June 2026 + +Update project URLs. + # 1.0.1 - 17 March 2026 Fix the `TypeError` messages generated when an invalid type of user/group was used. diff --git a/passwd/pyproject.toml b/passwd/pyproject.toml index ac49d28b8..b2f7fac81 100644 --- a/passwd/pyproject.toml +++ b/passwd/pyproject.toml @@ -32,8 +32,10 @@ integration = [ # installed for `just integration passwd` ] [project.urls] -"Repository" = "https://github.com/canonical/charmlibs" +"Documentation" = "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/passwd/" +"Repository" = "https://github.com/canonical/charmlibs/tree/main/passwd" "Issues" = "https://github.com/canonical/charmlibs/issues" +"Changelog" = "https://github.com/canonical/charmlibs/blob/main/passwd/CHANGELOG.md" [build-system] requires = ["hatchling"] diff --git a/passwd/src/charmlibs/passwd/_version.py b/passwd/src/charmlibs/passwd/_version.py index fe9c6b4a6..3e7ba0657 100644 --- a/passwd/src/charmlibs/passwd/_version.py +++ b/passwd/src/charmlibs/passwd/_version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = '1.0.1' +__version__ = '1.0.1.post0' diff --git a/pathops/CHANGELOG.md b/pathops/CHANGELOG.md index 5a11442e9..9b804a237 100644 --- a/pathops/CHANGELOG.md +++ b/pathops/CHANGELOG.md @@ -1,3 +1,7 @@ +# 1.3.0.post0 - 16 June 2026 + +Update project URLs. + # 1.3.0 - 2 June 2026 `PathProtocol.glob` and `LocalPath.glob` now accept a `str | os.PathLike[str]` pattern, matching `ContainerPath.glob` and `pathlib.Path.glob` on Python 3.13+. diff --git a/pathops/pyproject.toml b/pathops/pyproject.toml index 5783af3f1..d0fea5a04 100644 --- a/pathops/pyproject.toml +++ b/pathops/pyproject.toml @@ -30,7 +30,7 @@ integration = [ [project.urls] "Documentation" = "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/pathops/" -"Repository" = "https://github.com/canonical/charmlibs" +"Repository" = "https://github.com/canonical/charmlibs/tree/main/pathops" "Issues" = "https://github.com/canonical/charmlibs/issues" "Changelog" = "https://github.com/canonical/charmlibs/blob/main/pathops/CHANGELOG.md" diff --git a/pathops/src/charmlibs/pathops/_version.txt b/pathops/src/charmlibs/pathops/_version.txt index f0bb29e76..b8b5ec2c7 100644 --- a/pathops/src/charmlibs/pathops/_version.txt +++ b/pathops/src/charmlibs/pathops/_version.txt @@ -1 +1 @@ -1.3.0 +1.3.0.post0 diff --git a/snap/CHANGELOG.md b/snap/CHANGELOG.md index 50e407b73..558aa3b67 100644 --- a/snap/CHANGELOG.md +++ b/snap/CHANGELOG.md @@ -1,3 +1,7 @@ +# 1.0.1.post0 - 16 June 2026 + +Update project URLs. + # 1.0.1 - 4 November 2025 Update the type annotation of `log`'s `num_lines` parameter to indicate that `'all'` is accepted. diff --git a/snap/pyproject.toml b/snap/pyproject.toml index dbab1ce79..03b709db7 100644 --- a/snap/pyproject.toml +++ b/snap/pyproject.toml @@ -28,7 +28,7 @@ integration = [] [project.urls] "Documentation" = "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/snap/" -"Repository" = "https://github.com/canonical/charmlibs" +"Repository" = "https://github.com/canonical/charmlibs/tree/main/snap" "Issues" = "https://github.com/canonical/charmlibs/issues" "Changelog" = "https://github.com/canonical/charmlibs/blob/main/snap/CHANGELOG.md" diff --git a/snap/src/charmlibs/snap/_version.py b/snap/src/charmlibs/snap/_version.py index fba7cb9a9..70fc0159a 100644 --- a/snap/src/charmlibs/snap/_version.py +++ b/snap/src/charmlibs/snap/_version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = '1.0.1' +__version__ = '1.0.1.post0' diff --git a/sysctl/CHANGELOG.md b/sysctl/CHANGELOG.md index 81dcd3787..bb7f70d04 100644 --- a/sysctl/CHANGELOG.md +++ b/sysctl/CHANGELOG.md @@ -1,3 +1,7 @@ +# 1.0.0.post0 - 16 June 2026 + +Update project URLs. + # 1.0.0 - 13 February 2026 Initial release of migrated `operator_libs_linux.v0.sysctl` library (patch version 4) with minor modifications to satisfy new linter rules. diff --git a/sysctl/pyproject.toml b/sysctl/pyproject.toml index 1aa2a960e..2bbba5341 100644 --- a/sysctl/pyproject.toml +++ b/sysctl/pyproject.toml @@ -32,8 +32,10 @@ integration = [ # installed for `just integration sysctl` ] [project.urls] -"Repository" = "https://github.com/canonical/charmlibs" +"Documentation" = "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/sysctl/" +"Repository" = "https://github.com/canonical/charmlibs/tree/main/sysctl" "Issues" = "https://github.com/canonical/charmlibs/issues" +"Changelog" = "https://github.com/canonical/charmlibs/blob/main/sysctl/CHANGELOG.md" [build-system] requires = ["hatchling"] diff --git a/sysctl/src/charmlibs/sysctl/_version.py b/sysctl/src/charmlibs/sysctl/_version.py index a5443891f..06354d8f9 100644 --- a/sysctl/src/charmlibs/sysctl/_version.py +++ b/sysctl/src/charmlibs/sysctl/_version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = '1.0.0' +__version__ = '1.0.0.post0' diff --git a/systemd/CHANGELOG.md b/systemd/CHANGELOG.md index c041d8dc1..7e9b0a2ca 100644 --- a/systemd/CHANGELOG.md +++ b/systemd/CHANGELOG.md @@ -1,3 +1,7 @@ +# 1.0.0.post0 - 16 June 2026 + +Update project URLs. + # 1.0.0 - 19 November 2025 Initial release of migrated `operator_libs_linux.v1.systemd` library (patch version 4) diff --git a/systemd/pyproject.toml b/systemd/pyproject.toml index 036770494..6a1c02caa 100644 --- a/systemd/pyproject.toml +++ b/systemd/pyproject.toml @@ -25,9 +25,9 @@ functional = [] integration = [] [project.urls] -"Repository" = "https://github.com/canonical/charmlibs" -"Issues" = "https://github.com/canonical/charmlibs/issues" "Documentation" = "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/systemd/" +"Repository" = "https://github.com/canonical/charmlibs/tree/main/systemd" +"Issues" = "https://github.com/canonical/charmlibs/issues" "Changelog" = "https://github.com/canonical/charmlibs/blob/main/systemd/CHANGELOG.md" [build-system] diff --git a/systemd/src/charmlibs/systemd/_version.py b/systemd/src/charmlibs/systemd/_version.py index 8c5a447db..fb0199b45 100644 --- a/systemd/src/charmlibs/systemd/_version.py +++ b/systemd/src/charmlibs/systemd/_version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = '1.0.0' +__version__ = '1.0.0.post0' From a3436ab4d30c7ad9c5d5af14562ae3c675d55162 Mon Sep 17 00:00:00 2001 From: James Garner Date: Tue, 16 Jun 2026 17:36:49 +1200 Subject: [PATCH 42/65] ci: also export library docs URLs for interfaces (#525) This PR updates `.scripts/ls.py` and `.scripts/just.py` to export both URLs to the interface library docs and the low-level interface docs (currently we only export the URLs for the interface docs). The goal is to have the library docs URLs surfaced on Charmhub on the interface pages. --- .scripts/just.py | 1 + .scripts/ls.py | 88 +++++++++++++++++++++++++++++++------------ interfaces/index.json | 68 +++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 25 deletions(-) diff --git a/.scripts/just.py b/.scripts/just.py index bb2f40eb8..139db7189 100755 --- a/.scripts/just.py +++ b/.scripts/just.py @@ -422,6 +422,7 @@ def interfaces_json(argv: list[str]) -> int: 'version', 'lib', 'lib_url', + 'lib_docs_url', 'docs_url', 'summary', 'description', diff --git a/.scripts/ls.py b/.scripts/ls.py index 5283dae34..f1f3d40f9 100755 --- a/.scripts/ls.py +++ b/.scripts/ls.py @@ -61,7 +61,8 @@ class Info: version: str = '' lib: str = '' lib_url: str = '' - docs_url: str = '' + docs_url: str = '' # Library docs URL for packages; interface docs URL for interfaces. + lib_docs_url: str = '' # Identical to docs_url for packages. summary: str = '' description: str = '' status: str = '' @@ -188,6 +189,8 @@ def _ls( info.lib_url = _lib_urls().get(_get_lib_name(category, root, path), '') if 'docs_url' in output: info.docs_url = _get_docs_url(category, root, path) + if 'lib_docs_url' in output: + info.lib_docs_url = _get_lib_docs_url(category, root, path) if 'status' in output: info.status = _get_status(category, root, path) if 'schema_path' in output: @@ -361,6 +364,7 @@ def _get_lib_name(category: str, root: pathlib.Path, path: pathlib.Path) -> str: def _get_docs_url(category: str, root: pathlib.Path, path: pathlib.Path) -> str: + """Return URL to the library docs for this package, or interface docs for this interface.""" if category == 'packages': url = _pyproject_toml(path, root=root)['project']['urls'].get('Documentation', '') return url.strip() @@ -368,7 +372,21 @@ def _get_docs_url(category: str, root: pathlib.Path, path: pathlib.Path) -> str: return f'https://documentation.ubuntu.com/charmlibs/reference/interfaces/{path.name}/' +def _get_lib_docs_url(category: str, root: pathlib.Path, path: pathlib.Path) -> str: + """Return URL to the docs for this library (or the library for this interface).""" + if category == 'packages': + return _get_docs_url(category, root, path) + assert category == 'interfaces' + lib = _get_lib_name('interfaces', root, path) + if not lib: + return '' + if lib.startswith('charmlibs.'): + return _get_docs_url('packages', root, path) + return _lib_docs_urls().get(lib, '') + + def _get_status(category: str, root: pathlib.Path, path: pathlib.Path) -> str: + """Return the 'draft' or 'published' status of this interface ('' for packages or missing).""" if category == 'packages': return '' assert category == 'interfaces' @@ -401,6 +419,7 @@ def _get_docs(root: pathlib.Path, path: pathlib.Path) -> dict[str, list[str]]: def _get_schema_path_str(category: str, root: pathlib.Path, path: pathlib.Path) -> str: + """Return the path to the schema.py file for this interface ('' for libs or missing).""" if category == 'packages': return '' assert category == 'interfaces' @@ -411,11 +430,14 @@ def _get_schema_path_str(category: str, root: pathlib.Path, path: pathlib.Path) def _get_dist_name(package: pathlib.Path, root: pathlib.Path = _REPO_ROOT) -> str: """Load distribution package name from pyproject.toml and normalize it.""" name = _pyproject_toml(package, root=root)['project']['name'] - return _normalize(name.strip()) + # Normalize distribution package name according to PyPI rules. + # https://packaging.python.org/en/latest/specifications/name-normalization/#name-normalization + return re.sub(r'[-_.]+', '-', name).lower().strip() @functools.cache def _get_package_version(package: pathlib.Path, root: pathlib.Path = _REPO_ROOT) -> str: + """Return the runtime version of the package.""" name = _get_dist_name(package, root=root) script = f'import importlib.metadata; print(importlib.metadata.version("{name}"))' cmd = ['uv', 'run', '--no-project', '--with', root / package, 'python', '-c', script] @@ -441,9 +463,43 @@ def _interface_yaml(path: pathlib.Path, root: pathlib.Path = _REPO_ROOT): return yaml.safe_load(f) +############# +# libs.yaml # +############# + + +@dataclasses.dataclass +class _LibEntry: + name: str + status: str + url: str + docs: str + src: str + kind: str + description: str + tags: list[str] + + +@functools.cache def _lib_urls() -> dict[str, str]: - result: dict[str, str] = {} - for entry in (*_libs_yaml()['general'], *_libs_yaml()['interfaces']): + return {name: entry.url for name, entry in _all_libs_by_name().items()} + + +@functools.cache +def _lib_docs_urls() -> dict[str, str]: + return {name: entry.docs for name, entry in _all_libs_by_name().items()} + + +@functools.cache +def _lib_tags() -> dict[str, list[str]]: + return {name: sorted(entry.tags) for name, entry in _all_libs_by_name().items()} + + +@functools.cache +def _all_libs_by_name() -> dict[str, _LibEntry]: + libs_yaml = yaml.safe_load((_REPO_ROOT / '.docs' / 'reference' / 'libs.yaml').read_text()) + result: dict[str, _LibEntry] = {} + for entry in (*libs_yaml['general'], *libs_yaml['interfaces']): # Library names should be unique, but we currently have an entry for # charms.data_platform_libs.data_interfaces for each interface it supports # This doesn't break our lookups though, since they all have the same metadata @@ -452,29 +508,11 @@ def _lib_urls() -> dict[str, str]: entry['name'] not in result or entry['name'] == 'charms.data_platform_libs.data_interfaces' ) - result[entry['name']] = entry['url'] + result[entry['name']] = _LibEntry(**{ + k.name: entry[k.name] for k in dataclasses.fields(_LibEntry) + }) return result -def _lib_tags() -> dict[str, list[str]]: - return { - entry['name']: sorted(entry.get('tags') or []) - for entry in (*_libs_yaml()['general'], *_libs_yaml()['interfaces']) - } - - -@functools.cache -def _libs_yaml(): - return yaml.safe_load((_REPO_ROOT / '.docs' / 'reference' / 'libs.yaml').read_text()) - - -def _normalize(name: str) -> str: - """Normalize distribution package name according to PyPI rules. - - https://packaging.python.org/en/latest/specifications/name-normalization/#name-normalization - """ - return re.sub(r'[-_.]+', '-', name).lower() - - if __name__ == '__main__': _main() diff --git a/interfaces/index.json b/interfaces/index.json index 18d3f5721..95a1a3903 100644 --- a/interfaces/index.json +++ b/interfaces/index.json @@ -4,6 +4,7 @@ "version": "0", "lib": "charms.oathkeeper.auth_proxy", "lib_url": "https://charmhub.io/oathkeeper/libraries/auth_proxy", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/auth_proxy/", "summary": "Configure an Identity and Access Proxy.", "description": "The `auth_proxy` interface allows charms to configure an Identity and Access Proxy.\nThe requirer supplies the configuration, such as protected URLs, allowed endpoints, and headers.\nThe provider is responsible for transforming the configuration into access rules and forwarding relevant configuration to the proxy.", @@ -17,6 +18,7 @@ "version": "0", "lib": "charms.data_platform_libs.data_interfaces", "lib_url": "https://charmhub.io/data-platform-libs/libraries/data_interfaces", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/azure_service_principal/", "summary": "Interact with Microsoft Entra ID using service principal credentials.", "description": "The `azure_service_principal` interface allows charms to receive the service principal credential needed to access Entra ID objects such as Azure storage.\nRequirer charms can specify which fields should be transferred as Juju secrets, and the provider charm will deliver the credentials as requested.", @@ -31,6 +33,7 @@ "version": "0", "lib": "", "lib_url": "", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/azure_storage/", "summary": "Access Azure Blob Storage and Data Lake Storage (Gen 2).", "description": "The `azure_storage` interface allows charms to access Azure Blob Storage and Data Lake Storage (Gen 2) with appropriate credentials and connection details.\nThe provider charm supplies storage account credentials and configuration, while the requirer charm specifies the container to access.", @@ -42,6 +45,7 @@ "version": "1", "lib": "charms.certificate_transfer_interface.certificate_transfer", "lib_url": "https://charmhub.io/certificate-transfer-interface/libraries/certificate_transfer", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/certificate_transfer/", "summary": "Receive public or CA certificates.", "description": "The `certificate_transfer` interface allows charms to receive public and/or CA certificates.\nThe provider charm shares its certificates with the requirer.", @@ -55,6 +59,7 @@ "version": "0", "lib": "charms.cloudflare_configurator.cloudflared_route", "lib_url": "https://charmhub.io/cloudflare-configurator/libraries/cloudflared_route", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/cloudflared_route/", "summary": "Configure a cloudflared tunnel.", "description": "The `cloudflared_route` interface allows information to be exchanged between the cloudflared tunnel configurator charm and the cloudflared tunnel charm.", @@ -69,6 +74,7 @@ "version": "0", "lib": "charms.data_platform_libs.data_interfaces", "lib_url": "https://charmhub.io/data-platform-libs/libraries/data_interfaces", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/connect_client/", "summary": "Interact with Kafka Connect clusters.", "description": "The `connect_client` interface allows charms to interact with Kafka Connect clusters for connector plugin and task management.\nThe provider charm manages the Kafka Connect cluster and deploys connector plugins from URLs supplied by the requirer, while the requirer charm manages its connectors and tasks using the provider's REST endpoints.", @@ -83,6 +89,7 @@ "version": "0", "lib": "charms.grafana_agent.cos_agent", "lib_url": "https://charmhub.io/grafana-agent/libraries/cos_agent", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/cos_agent/", "summary": "Machine-charm-specific interface for managing multiple observability-related data streams.", "description": "The `cos_agent` interface allows machine charms to send telemetry \u2014 such as metrics, logs, dashboards, and alert rules \u2014 to Opentelemetry Colector or Grafana Agent charms.\n\nThis interface is designed specifically for machine charms, where the requirer is typically the [grafana-agent](https://charmhub.io/grafana-agent) and the [opentelemetry-collector](https://github.com/canonical/opentelemetry-collector-operator/) subordinate charms.", @@ -96,6 +103,7 @@ "version": "0", "lib": "", "lib_url": "", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/database_backup/", "summary": "Manage database backup and restore operations.", "description": "The `database_backup` interface allows a database backup manager charm to coordinate backup and restore operations with database charms.\nThe provider charm (database) executes backup and restore jobs based on requirer requests and reports their status, while the requirer charm (backup manager) requests jobs, manages storage configuration, and stores backup artifacts.", @@ -107,6 +115,7 @@ "version": "0", "lib": "", "lib_url": "", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/dns_record/", "summary": "Create and manage DNS records.", "description": "", @@ -118,6 +127,7 @@ "version": "0", "lib": "charms.data_platform_libs.data_interfaces", "lib_url": "https://charmhub.io/data-platform-libs/libraries/data_interfaces", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/etcd_client/", "summary": "Access etcd clusters with credentials and TLS configuration.", "description": "The `etcd_client` interface allows charms to access etcd clusters with user-specific credentials and TLS configuration.\nThe provider charm creates etcd users and roles based on requirer requests, while the requirer charm shares certificate information and key access prefixes.", @@ -132,6 +142,7 @@ "version": "0", "lib": "charms.filesystem_client.filesystem_info", "lib_url": "https://charmhub.io/filesystem-client/libraries/filesystem_info", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/filesystem_info/", "summary": "Export mount information about shared filesystems.", "description": "The `filesystem_info` interface allows charms that export shared filesystems to expose the required mount information.\nThe provider charm exports mount information for the requirer charm.", @@ -145,6 +156,7 @@ "version": "0", "lib": "charms.sdcore_nms_k8s.fiveg_core_gnb", "lib_url": "https://charmhub.io/sdcore-nms-k8s/libraries/fiveg_core_gnb", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/fiveg_core_gnb/", "summary": "", "description": "", @@ -158,6 +170,7 @@ "version": "0", "lib": "", "lib_url": "", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/fiveg_f1/", "summary": "", "description": "", @@ -169,6 +182,7 @@ "version": "0", "lib": "", "lib_url": "", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/fiveg_gnb_identity/", "summary": "", "description": "", @@ -180,6 +194,7 @@ "version": "0", "lib": "charms.sdcore_amf_k8s.fiveg_n2", "lib_url": "https://charmhub.io/sdcore-amf-k8s/libraries/fiveg_n2", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/fiveg_n2/", "summary": "", "description": "", @@ -193,6 +208,7 @@ "version": "0", "lib": "", "lib_url": "", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/fiveg_n3/", "summary": "", "description": "", @@ -204,6 +220,7 @@ "version": "0", "lib": "charms.sdcore_upf_k8s.fiveg_n4", "lib_url": "https://charmhub.io/sdcore-upf-k8s/libraries/fiveg_n4", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/fiveg_n4/", "summary": "", "description": "", @@ -217,6 +234,7 @@ "version": "0", "lib": "charms.sdcore_nrf_k8s.fiveg_nrf", "lib_url": "https://charmhub.io/sdcore-nrf-k8s/libraries/fiveg_nrf", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/fiveg_nrf/", "summary": "", "description": "", @@ -230,6 +248,7 @@ "version": "0", "lib": "", "lib_url": "", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/fiveg_rfsim/", "summary": "", "description": "", @@ -241,6 +260,7 @@ "version": "0", "lib": "charms.oathkeeper.forward_auth", "lib_url": "https://charmhub.io/oathkeeper/libraries/forward_auth", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/forward_auth/", "summary": "", "description": "", @@ -254,6 +274,7 @@ "version": "0", "lib": "charms.grafana_k8s.grafana_auth", "lib_url": "https://charmhub.io/grafana-k8s/libraries/grafana_auth", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/grafana_auth/", "summary": "Provide authentication configuration for Grafana.", "description": "The `grafana_auth` interface supports configuring Grafana authentication.\nThe provider sends the authentication mode and its configuration.\nThe requirer configures authentication with Grafana and replies with its publicly reachable URL.", @@ -268,6 +289,7 @@ "version": "0", "lib": "charms.grafana_k8s.grafana_source", "lib_url": "https://charmhub.io/grafana-k8s/libraries/grafana_source", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/grafana_datasource/", "summary": "Provide Grafana data source servers.", "description": "The `grafana_source` interface supports exposing servers implementing the Grafana source HTTP API.\nThe provider publishes the endpoint URL for the servers, one per unit.\nThe requirer replies with unique IDs for the Grafana application and a mapping of unique IDs for the units.", @@ -281,6 +303,7 @@ "version": "0", "lib": "cosl", "lib_url": "https://pypi.org/project/cosl/", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/grafana_datasource_exchange/", "summary": "Share UIDs for Grafana datasources between charms.", "description": "The `grafana_datasource_exchange` interface allows charms that generate telemetry and have a reference to the datasources where said telemetry is queriable, to share those references to other charms for correlation and cross-referencing purposes.\n\nThis is a symmetrical relation, where implementers broadcast the same information regardless of whether they are the provider or requirer.\nTo avoid constraints on integration topology, charms should implement both a requirer and provider endpoint for this interface.", @@ -295,6 +318,7 @@ "version": "0", "lib": "charms.hydra.hydra_endpoints", "lib_url": "https://charmhub.io/hydra/libraries/hydra_endpoints", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/hydra_endpoints/", "summary": "", "description": "", @@ -308,6 +332,7 @@ "version": "2", "lib": "charms.traefik_k8s.ingress", "lib_url": "https://charmhub.io/traefik-k8s/libraries/ingress", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/ingress/", "summary": "", "description": "", @@ -322,6 +347,7 @@ "version": "0", "lib": "charms.traefik_k8s.ingress_per_unit", "lib_url": "https://charmhub.io/traefik-k8s/libraries/ingress_per_unit", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/ingress_per_unit/", "summary": "", "description": "", @@ -336,6 +362,7 @@ "version": "0", "lib": "", "lib_url": "", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/ip_router/", "summary": "", "description": "", @@ -347,6 +374,7 @@ "version": "0", "lib": "charmlibs.interfaces.istio_metadata", "lib_url": "https://pypi.org/project/charmlibs-interfaces-istio-metadata", + "lib_docs_url": "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio_metadata", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/istio_metadata/", "summary": "Share Istio installation metadata between charms.", "description": "The `istio_metadata` interface allows an Istio control plane charm to share information about its installation (such as the root namespace) with other charms that need to interoperate with Istio.\nThe provider charm (the Istio control plane) publishes the metadata, and requirer charms read it to configure their own integration with Istio.", @@ -360,6 +388,7 @@ "version": "0", "lib": "", "lib_url": "", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/jwt/", "summary": "Provide and consume JSON Web Token configuration.", "description": "The `jwt` interface allows charms to provide or require JSON Web Token configuration for secure token validation.\nThe provider charm supplies signing keys and optional validation parameters, while the requirer charm applies these settings to validate incoming JWTs in its workload.", @@ -371,6 +400,7 @@ "version": "0", "lib": "", "lib_url": "", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/k8s-service/", "summary": "", "description": "", @@ -382,6 +412,7 @@ "version": "0", "lib": "charmlibs.interfaces.k8s_backup_target", "lib_url": "", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/k8s_backup_target/", "summary": "Configure Kubernetes backup target specifications.", "description": "The `k8s_backup_target` interface allows client charms to specify Kubernetes backup requirements to a backup integrator charm.\nThe provider charm (client) provides backup specifications including namespaces, resource types, and retention policies, while the requirer charm (backup integrator) receives these specifications and forwards them to a backup operator.", @@ -393,6 +424,7 @@ "version": "0", "lib": "charms.data_platform_libs.data_interfaces", "lib_url": "https://charmhub.io/data-platform-libs/libraries/data_interfaces", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/kafka_client/", "summary": "Interact with Kafka clusters as a client.", "description": "The `kafka_client` interface allows charms to interact with Kafka clusters as producers, consumers, or admin clients.\nThe provider charm creates Kafka users with appropriate ACLs and permissions based on requested roles, while the requirer charm specifies its desired topic and role.", @@ -407,6 +439,7 @@ "version": "0", "lib": "", "lib_url": "", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/karapace_client/", "summary": "Access Karapace schema registry services.", "description": "The `karapace_client` interface allows charms to access Karapace schema registry services with user-specific credentials.\nThe provider charm manages Karapace users and subject access controls, while the requirer charm requests access to specific subjects with desired role permissions.", @@ -418,6 +451,7 @@ "version": "0", "lib": "", "lib_url": "", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/kratos_external_idp/", "summary": "", "description": "", @@ -429,6 +463,7 @@ "version": "0", "lib": "charms.kratos.kratos_info", "lib_url": "https://charmhub.io/kratos/libraries/kratos_info", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/kratos_info/", "summary": "", "description": "", @@ -442,6 +477,7 @@ "version": "0", "lib": "", "lib_url": "", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/kubeflow_dashboard_links/", "summary": "", "description": "", @@ -453,6 +489,7 @@ "version": "0", "lib": "charms.glauth_k8s.ldap", "lib_url": "https://charmhub.io/glauth-k8s/libraries/ldap", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/ldap/", "summary": "Configure and connect to a Lightweight Directory Access Protocol (LDAP) server.", "description": "The `ldap` interface supports sharing configuration and connection information for a Lightweight Directory Access Protocol (LDAP) server.\nThe provider is responsible for sharing the LDAP URL and other information needed for the requirer to connect and perform LDAP operations.", @@ -466,6 +503,7 @@ "version": "0", "lib": "litmus-libs", "lib_url": "https://pypi.org/project/litmus-libs/", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/litmus_auth/", "summary": "Exchange gRPC server endpoints for communication between Litmus auth and backend charms.", "description": "The `litmus_auth` interface enables communication between Litmus auth and Litmus backend charms.\nBoth the provider and requirer publish their respective gPRC server's endpoint.", @@ -480,6 +518,7 @@ "version": "0", "lib": "charms.identity_platform_login_ui_operator.login_ui_endpoints", "lib_url": "https://charmhub.io/identity-platform-login-ui-operator/libraries/login_ui_endpoints", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/login_ui_endpoints/", "summary": "", "description": "", @@ -493,6 +532,7 @@ "version": "0", "lib": "", "lib_url": "", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/milter/", "summary": "Provide or consume a mail filter.", "description": "The `milter` interface allows charms to connect to a charm serving as a mail filter (milter).\nThe provider charm shares the information needed to connect.", @@ -504,6 +544,7 @@ "version": "0", "lib": "", "lib_url": "", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/mimir_cluster/", "summary": "Mimir coordinator and worker charm communication.", "description": "The `mimir_cluster` interface supports communication between the [mimir-coordinator-k8s](https://charmhub.io/mimir-coordinator-k8s) and [mimir-worker-k8s](https://charmhub.io/mimir-worker-k8s) charms.", @@ -515,6 +556,7 @@ "version": "0", "lib": "charms.data_platform_libs.data_interfaces", "lib_url": "https://charmhub.io/data-platform-libs/libraries/data_interfaces", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/mongodb_client/", "summary": "Access MongoDB databases with unique credentials.", "description": "The `mongodb_client` interface allows charms to access MongoDB databases with unique, relation-specific credentials transmitted via Juju Secrets.\nThe provider charm creates database users and provides connection details, while the requirer charm requests a database name and optional custom entities with specific role permissions.", @@ -529,6 +571,7 @@ "version": "0", "lib": "charms.data_platform_libs.data_interfaces", "lib_url": "https://charmhub.io/data-platform-libs/libraries/data_interfaces", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/mysql_client/", "summary": "Access MySQL databases with unique credentials.", "description": "The `mysql_client` interface allows charms to access MySQL databases with unique, relation-specific credentials transmitted via Juju Secrets.\nThe provider charm creates database users and provides connection details, while the requirer charm requests a database name and optional custom entities with specific role permissions.", @@ -543,6 +586,7 @@ "version": "0", "lib": "charms.nginx_ingress_integrator.nginx_route", "lib_url": "https://charmhub.io/nginx-ingress-integrator/libraries/nginx_route", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/nginx-route/", "summary": "Create an Nginx ingress resource in a Kubernetes cluster.", "description": "The `nginx_route` interface allows charms to request the creation of an Nginx ingress resource in the Kubernetes cluster.\nThe requirer charm supplies the configuration for the ingress resources.\nThe provider charm creates the resource as requested.", @@ -557,6 +601,7 @@ "version": "0", "lib": "charms.hydra.oauth", "lib_url": "https://charmhub.io/hydra/libraries/oauth", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/oauth/", "summary": "Connect to an OAuth2/OIDC Provider.", "description": "The `oauth` interface allows charms to interface with an OAuth2/OIDC Provider.\nThe requirer acts as an OAuth2 client.\nThe provider exposes an OAuth2/OIDC Provider.", @@ -570,6 +615,7 @@ "version": "0", "lib": "charms.opencti.opencti_connector", "lib_url": "https://charmhub.io/opencti/libraries/opencti_connector", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/opencti_connector/", "summary": "Connect a single OpenCTI provider to one or more OpenCTI connectors.", "description": "The `opencti_connector` interface supports connecting an OpenCTI provider charm to one or more OpenCTI connectors.\nThe provider charm specifies the expected connector type for each connector relation.\nEach requirer charm shares its OpenCTI URL (and token) for the provider to connect to.", @@ -583,6 +629,7 @@ "version": "1", "lib": "charms.openfga_k8s.openfga", "lib_url": "https://charmhub.io/openfga-k8s/libraries/openfga", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/openfga/", "summary": "Set up and connect to an OpenFGA store.", "description": "The `openfga` interface supports setting up an OpenFGA store and connecting to it.\nThe requirer specifies the store name, while the provider creates the store and shares the data needed to connect.", @@ -596,6 +643,7 @@ "version": "0", "lib": "charms.data_platform_libs.data_interfaces", "lib_url": "https://charmhub.io/data-platform-libs/libraries/data_interfaces", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/opensearch_client/", "summary": "Access OpenSearch and ElasticSearch clusters.", "description": "The `opensearch_client` interface allows charms to access OpenSearch and ElasticSearch clusters with user-specific credentials and index permissions transmitted via Juju Secrets.\nThe provider charm creates users and manages index access controls, while the requirer charm requests access to specific indices with optional role permissions.", @@ -610,6 +658,7 @@ "version": "0", "lib": "charms.data_platform_libs.data_interfaces", "lib_url": "https://charmhub.io/data-platform-libs/libraries/data_interfaces", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/postgresql_client/", "summary": "Access PostgreSQL databases with unique credentials.", "description": "The `postgresql_client` interface allows charms to access PostgreSQL databases with unique, relation-specific credentials.\nThe provider charm creates database users and provides connection details, while the requirer charm requests a database name and optional custom entities with specific role permissions. Supports subordinate charms that provide external connectivity.", @@ -624,6 +673,7 @@ "version": "0", "lib": "", "lib_url": "", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/profiling/", "summary": "", "description": "", @@ -635,6 +685,7 @@ "version": "0", "lib": "charms.prometheus_k8s.prometheus_remote_write", "lib_url": "https://charmhub.io/prometheus-k8s/libraries/prometheus_remote_write", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/prometheus_remote_write/", "summary": "Write Prometheus metrics to remote endpoint.", "description": "The `prometheus_remote_write` endpoint supports writing Prometheus metrics to a remote endpoint.\nThe provider publishes one or more endpoints, and emits alerts per rule files.\nThe requirer pushes metrics to the provided endpoints, and sends rule files to the provider.", @@ -648,6 +699,7 @@ "version": "0", "lib": "charms.prometheus_k8s.prometheus_scrape", "lib_url": "https://charmhub.io/prometheus-k8s/libraries/prometheus_scrape", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/prometheus_scrape/", "summary": "Configure Prometheus scraping jobs.", "description": "The `prometheus_scrape` interface supports configuring Prometheus scrape jobs and recording and alerting rules.\nThe provider specifies one or more scrape compatible metrics endpoints, and rule files.\nThe requirer then scrapes the provider's metrics and emits alerts per rule files.", @@ -661,6 +713,7 @@ "version": "0", "lib": "", "lib_url": "", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/pyroscope_cluster/", "summary": "Pyroscope coordinator and worker charm communication.", "description": "The `pyroscope_cluster` interface supports communication between the [pyroscope-coordinator-k8s](https://charmhub.io/pyroscope-coordinator-k8s) and [pyroscope-worker-k8s](https://charmhub.io/pyroscope-worker-k8s) charms.", @@ -672,6 +725,7 @@ "version": "1", "lib": "", "lib_url": "", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/s3/", "summary": "", "description": "", @@ -683,6 +737,7 @@ "version": "0", "lib": "charms.saml_integrator.saml", "lib_url": "https://charmhub.io/saml-integrator/libraries/saml", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/saml/", "summary": "Authenticate users with SAML.", "description": "The `saml` interface allows charms to connect to a SAML Identity Provider for authenticating users.\nThe provider charm is the SAML Identity Provider, and shares the necessary data with requirer charms (SAML Service Providers).", @@ -696,6 +751,7 @@ "version": "0", "lib": "charms.sdcore_nms_k8s.sdcore_config", "lib_url": "https://charmhub.io/sdcore-nms-k8s/libraries/sdcore_config", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/sdcore_config/", "summary": "", "description": "", @@ -709,6 +765,7 @@ "version": "0", "lib": "charms.sdcore_webui_k8s.sdcore_management", "lib_url": "https://charmhub.io/sdcore-webui-k8s/libraries/sdcore_management", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/sdcore_management/", "summary": "", "description": "", @@ -722,6 +779,7 @@ "version": "0", "lib": "charms.smtp_integrator.smtp", "lib_url": "https://charmhub.io/smtp-integrator/libraries/smtp", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/smtp/", "summary": "Connect to a SMTP server.", "description": "The `smtp` interface allows charms to connect to a SMTP server.\nThe provider charm provides the SMTP details so that the requirer can connect.", @@ -735,6 +793,7 @@ "version": "0", "lib": "", "lib_url": "", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/spark_service_account/", "summary": "", "description": "", @@ -746,6 +805,7 @@ "version": "1", "lib": "", "lib_url": "", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/tempo_cluster/", "summary": "Tempo coordinator and worker charm communication.", "description": "The `tempo_cluster` interface supports communication between the [tempo-coordinator-k8s](https://charmhub.io/tempo-coordinator-k8s) and [tempo-worker-k8s](https://charmhub.io/tempo-worker-k8s) charms.", @@ -757,6 +817,7 @@ "version": "1", "lib": "charmlibs.interfaces.tls_certificates", "lib_url": "https://pypi.org/project/charmlibs-interfaces-tls-certificates", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/tls-certificates/", "summary": "Securely request TLS certificates.", "description": "The `tls-certificates` interface allows charms to securely request and receive TLS certificates. The requirer charm is responsible for its private key and defining its certificate signing requests (CSRs). The provider charm delivers the certificate for each request, including the CA chain and CA certificate.", @@ -770,6 +831,7 @@ "version": "2", "lib": "charms.tempo_coordinator_k8s.tracing", "lib_url": "https://charmhub.io/tempo-coordinator-k8s/libraries/tracing", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/tracing/", "summary": "Request endpoints for pushing trace data.", "description": "The `tracing` interface supports push-based tracing.\nThe requirer publishes the list of protocols it wants to use to send traces.\nThe provider replies with an API URL and an endpoint for each protocol, omitting those it does not support.", @@ -783,6 +845,7 @@ "version": "1", "lib": "dpcharmlibs.interfaces", "lib_url": "https://pypi.org/project/dpcharmlibs-interfaces", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/valkey_client/", "summary": "Access Valkey databases with credentials and TLS configuration.", "description": "The `valkey_client` interface allows charms to access Valkey databases with user-specific credentials and TLS configuration.\nThe provider charm creates Valkey users and permissions based on requirer requests, while the requirer charm shares the desired key access prefix.", @@ -796,6 +859,7 @@ "version": "0", "lib": "", "lib_url": "", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/vault-autounseal/", "summary": "", "description": "", @@ -807,6 +871,7 @@ "version": "0", "lib": "charms.vault_k8s.vault_kv", "lib_url": "https://charmhub.io/vault-k8s/libraries/vault_kv", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/vault-kv/", "summary": "Securely share Vault credentials to interact with the Vault Key-Value (KV) backend.", "description": "The `vault-kv` interface allows the charms to securely request and receive the credentials needed to interact with the Key-Value (KV) backend of Vault.\nThe requirer charm requests credentials, and the provider charm supplies them.", @@ -820,6 +885,7 @@ "version": "0", "lib": "", "lib_url": "", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/velero_backup_config/", "summary": "Configure Kubernetes backup requirements for Velero.", "description": "The `velero_backup_config` interface allows client charms to specify Kubernetes backup requirements to a Velero Operator charm.\nThe requirer charm (client) provides backup specifications including namespaces, resource types, and retention policies, while the provider charm (Velero Operator) receives these specifications and uses them to execute backup operations.", @@ -831,6 +897,7 @@ "version": "0", "lib": "", "lib_url": "", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/wazuh_api_client/", "summary": "The `wazuh_api_client` interface allows charms to connect to a Wazuh server.\nThe provider charm manages access to the server, securely providing credentials and endpoints to the requirer charms.", "description": "Connect to a Wazuh server.", @@ -842,6 +909,7 @@ "version": "0", "lib": "", "lib_url": "", + "lib_docs_url": "", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/zookeeper/", "summary": "", "description": "", From 9b688a1b2199095adb1441f8d9df5868ff48186a Mon Sep 17 00:00:00 2001 From: James Garner Date: Tue, 16 Jun 2026 19:11:27 +1200 Subject: [PATCH 43/65] docs(interfaces/istio_metadata): correct docs URL (#531) This PR corrects the docs URL in the project metadata (all URLs in the docs are normalised to use hyphens, and I haven't yet set up infra to redirect underscored versions). It adds a 'post' release bump and changelog entry to push the update to PyPI. --- interfaces/index.json | 2 +- interfaces/istio_metadata/CHANGELOG.md | 4 ++++ interfaces/istio_metadata/pyproject.toml | 2 +- .../src/charmlibs/interfaces/istio_metadata/_version.py | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/interfaces/index.json b/interfaces/index.json index 95a1a3903..33a386f13 100644 --- a/interfaces/index.json +++ b/interfaces/index.json @@ -374,7 +374,7 @@ "version": "0", "lib": "charmlibs.interfaces.istio_metadata", "lib_url": "https://pypi.org/project/charmlibs-interfaces-istio-metadata", - "lib_docs_url": "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio_metadata", + "lib_docs_url": "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio-metadata", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/istio_metadata/", "summary": "Share Istio installation metadata between charms.", "description": "The `istio_metadata` interface allows an Istio control plane charm to share information about its installation (such as the root namespace) with other charms that need to interoperate with Istio.\nThe provider charm (the Istio control plane) publishes the metadata, and requirer charms read it to configure their own integration with Istio.", diff --git a/interfaces/istio_metadata/CHANGELOG.md b/interfaces/istio_metadata/CHANGELOG.md index 668eae022..db9072ecf 100644 --- a/interfaces/istio_metadata/CHANGELOG.md +++ b/interfaces/istio_metadata/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 1.0.0.post0 - 16 June 2026 + +Updated project URLs. + ## 1.0.0 - 8 June 2026 Initial release. Migrated from `charms.istio_k8s.v0.istio_metadata` (v0.2). diff --git a/interfaces/istio_metadata/pyproject.toml b/interfaces/istio_metadata/pyproject.toml index e95f323c1..1b11c860d 100644 --- a/interfaces/istio_metadata/pyproject.toml +++ b/interfaces/istio_metadata/pyproject.toml @@ -33,7 +33,7 @@ integration = [ # installed for `just integration interfaces/istio_metadata` ] [project.urls] -"Documentation" = "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio_metadata" +"Documentation" = "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio-metadata" "Repository" = "https://github.com/canonical/charmlibs/tree/main/interfaces/istio_metadata" "Issues" = "https://github.com/canonical/charmlibs/issues" "Changelog" = "https://github.com/canonical/charmlibs/blob/main/interfaces/istio_metadata/CHANGELOG.md" diff --git a/interfaces/istio_metadata/src/charmlibs/interfaces/istio_metadata/_version.py b/interfaces/istio_metadata/src/charmlibs/interfaces/istio_metadata/_version.py index a5443891f..06354d8f9 100644 --- a/interfaces/istio_metadata/src/charmlibs/interfaces/istio_metadata/_version.py +++ b/interfaces/istio_metadata/src/charmlibs/interfaces/istio_metadata/_version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = '1.0.0' +__version__ = '1.0.0.post0' From 7f5e74bd55ab8bc088c4b5ed44a704b293a270b4 Mon Sep 17 00:00:00 2001 From: Adi Date: Wed, 17 Jun 2026 12:19:30 +0200 Subject: [PATCH 44/65] chore(service-mesh): updates the lib folder names to match charmcraft (#533) A minor update that updates the library path names of all libraries owned by the service mesh team to match the interface name in charmcraft.yaml --- .docs/reference/libs.yaml | 8 ++++---- CODEOWNERS | 16 ++++++++-------- .../CHANGELOG.md | 4 ++++ .../README.md | 2 +- .../pyproject.toml | 12 ++++++------ .../interfaces/gateway_metadata/__init__.py | 4 ++-- .../gateway_metadata/_gateway_metadata.py | 4 ++-- .../interfaces/gateway_metadata}/_version.py | 0 .../interfaces/gateway_metadata/py.typed | 0 .../tests/unit/conftest.py | 0 .../tests/unit/test_gateway_metadata.py | 0 .../uv.lock | 0 .../CHANGELOG.md | 8 ++++++-- .../README.md | 2 +- .../pyproject.toml | 12 ++++++------ .../interfaces/istio_ingress_route/__init__.py | 4 ++-- .../istio_ingress_route/_istio_ingress_route.py | 4 ++-- .../interfaces/istio_ingress_route}/_version.py | 2 +- .../interfaces/istio_ingress_route/py.typed | 0 .../tests/unit/conftest.py | 0 .../tests/unit/test_istio_ingress_route.py | 0 .../uv.lock | 0 .../CHANGELOG.md | 16 +++++++++++----- .../README.md | 2 +- .../pyproject.toml | 12 ++++++------ .../interfaces/istio_request_auth/__init__.py | 4 ++-- .../istio_request_auth/_istio_request_auth.py | 4 ++-- .../interfaces/istio_request_auth}/_version.py | 2 +- .../interfaces/istio_request_auth/py.typed | 0 .../tests/unit/conftest.py | 0 .../tests/unit/test_istio_request_auth.py | 0 .../uv.lock | 0 .../{service-mesh => service_mesh}/CHANGELOG.md | 8 ++++++-- .../{service-mesh => service_mesh}/README.md | 2 +- .../pyproject.toml | 12 ++++++------ .../interfaces/service_mesh/__init__.py | 0 .../interfaces/service_mesh/_service_mesh.py | 2 +- .../interfaces/service_mesh}/_version.py | 2 +- .../charmlibs/interfaces/service_mesh/py.typed | 0 .../tests/unit/conftest.py | 0 .../tests/unit/test_service_mesh.py | 0 .../{service-mesh => service_mesh}/uv.lock | 0 42 files changed, 83 insertions(+), 65 deletions(-) rename interfaces/{gateway-metadata => gateway_metadata}/CHANGELOG.md (76%) rename interfaces/{gateway-metadata => gateway_metadata}/README.md (89%) rename interfaces/{gateway-metadata => gateway_metadata}/pyproject.toml (91%) rename interfaces/{gateway-metadata => gateway_metadata}/src/charmlibs/interfaces/gateway_metadata/__init__.py (97%) rename interfaces/{gateway-metadata => gateway_metadata}/src/charmlibs/interfaces/gateway_metadata/_gateway_metadata.py (97%) rename interfaces/{istio-ingress-route/src/charmlibs/interfaces/istio_ingress_route => gateway_metadata/src/charmlibs/interfaces/gateway_metadata}/_version.py (100%) rename interfaces/{gateway-metadata => gateway_metadata}/src/charmlibs/interfaces/gateway_metadata/py.typed (100%) rename interfaces/{gateway-metadata => gateway_metadata}/tests/unit/conftest.py (100%) rename interfaces/{gateway-metadata => gateway_metadata}/tests/unit/test_gateway_metadata.py (100%) rename interfaces/{gateway-metadata => gateway_metadata}/uv.lock (100%) rename interfaces/{istio-ingress-route => istio_ingress_route}/CHANGELOG.md (74%) rename interfaces/{istio-ingress-route => istio_ingress_route}/README.md (89%) rename interfaces/{istio-ingress-route => istio_ingress_route}/pyproject.toml (91%) rename interfaces/{istio-ingress-route => istio_ingress_route}/src/charmlibs/interfaces/istio_ingress_route/__init__.py (98%) rename interfaces/{istio-ingress-route => istio_ingress_route}/src/charmlibs/interfaces/istio_ingress_route/_istio_ingress_route.py (99%) rename interfaces/{istio-request-auth/src/charmlibs/interfaces/istio_request_auth => istio_ingress_route/src/charmlibs/interfaces/istio_ingress_route}/_version.py (96%) rename interfaces/{istio-ingress-route => istio_ingress_route}/src/charmlibs/interfaces/istio_ingress_route/py.typed (100%) rename interfaces/{istio-ingress-route => istio_ingress_route}/tests/unit/conftest.py (100%) rename interfaces/{istio-ingress-route => istio_ingress_route}/tests/unit/test_istio_ingress_route.py (100%) rename interfaces/{istio-ingress-route => istio_ingress_route}/uv.lock (100%) rename interfaces/{istio-request-auth => istio_request_auth}/CHANGELOG.md (87%) rename interfaces/{istio-request-auth => istio_request_auth}/README.md (89%) rename interfaces/{istio-request-auth => istio_request_auth}/pyproject.toml (91%) rename interfaces/{istio-request-auth => istio_request_auth}/src/charmlibs/interfaces/istio_request_auth/__init__.py (98%) rename interfaces/{istio-request-auth => istio_request_auth}/src/charmlibs/interfaces/istio_request_auth/_istio_request_auth.py (98%) rename interfaces/{service-mesh/src/charmlibs/interfaces/service_mesh => istio_request_auth/src/charmlibs/interfaces/istio_request_auth}/_version.py (96%) rename interfaces/{istio-request-auth => istio_request_auth}/src/charmlibs/interfaces/istio_request_auth/py.typed (100%) rename interfaces/{istio-request-auth => istio_request_auth}/tests/unit/conftest.py (100%) rename interfaces/{istio-request-auth => istio_request_auth}/tests/unit/test_istio_request_auth.py (100%) rename interfaces/{istio-request-auth => istio_request_auth}/uv.lock (100%) rename interfaces/{service-mesh => service_mesh}/CHANGELOG.md (77%) rename interfaces/{service-mesh => service_mesh}/README.md (90%) rename interfaces/{service-mesh => service_mesh}/pyproject.toml (91%) rename interfaces/{service-mesh => service_mesh}/src/charmlibs/interfaces/service_mesh/__init__.py (100%) rename interfaces/{service-mesh => service_mesh}/src/charmlibs/interfaces/service_mesh/_service_mesh.py (99%) rename interfaces/{gateway-metadata/src/charmlibs/interfaces/gateway_metadata => service_mesh/src/charmlibs/interfaces/service_mesh}/_version.py (95%) rename interfaces/{service-mesh => service_mesh}/src/charmlibs/interfaces/service_mesh/py.typed (100%) rename interfaces/{service-mesh => service_mesh}/tests/unit/conftest.py (100%) rename interfaces/{service-mesh => service_mesh}/tests/unit/test_service_mesh.py (100%) rename interfaces/{service-mesh => service_mesh}/uv.lock (100%) diff --git a/.docs/reference/libs.yaml b/.docs/reference/libs.yaml index 155766b59..61f7ea6f9 100644 --- a/.docs/reference/libs.yaml +++ b/.docs/reference/libs.yaml @@ -1636,7 +1636,7 @@ interfaces: status: recommended url: https://pypi.org/project/charmlibs-interfaces-service-mesh docs: https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/service-mesh - src: https://github.com/canonical/charmlibs/tree/main/interfaces/service-mesh + src: https://github.com/canonical/charmlibs/tree/main/interfaces/service_mesh kind: PyPI rel_name: service_mesh rel_url_charmhub: https://charmhub.io/integrations/service_mesh @@ -1784,7 +1784,7 @@ interfaces: status: recommended url: https://pypi.org/project/charmlibs-interfaces-istio-request-auth docs: https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio-request-auth - src: https://github.com/canonical/charmlibs/tree/main/interfaces/istio-request-auth + src: https://github.com/canonical/charmlibs/tree/main/interfaces/istio_request_auth kind: PyPI rel_name: istio-request-auth rel_url_charmhub: '' @@ -1810,7 +1810,7 @@ interfaces: status: recommended url: https://pypi.org/project/charmlibs-interfaces-gateway-metadata docs: https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/gateway-metadata - src: https://github.com/canonical/charmlibs/tree/main/interfaces/gateway-metadata + src: https://github.com/canonical/charmlibs/tree/main/interfaces/gateway_metadata kind: PyPI rel_name: gateway-metadata rel_url_charmhub: '' @@ -1871,7 +1871,7 @@ interfaces: status: recommended url: https://pypi.org/project/charmlibs-interfaces-istio-ingress-route docs: https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio-ingress-route - src: https://github.com/canonical/charmlibs/tree/main/interfaces/istio-ingress-route + src: https://github.com/canonical/charmlibs/tree/main/interfaces/istio_ingress_route kind: PyPI rel_name: istio-ingress-route rel_url_charmhub: '' diff --git a/CODEOWNERS b/CODEOWNERS index 6559eeb7d..8ec238416 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -86,17 +86,17 @@ /interfaces/forward_auth/interface/ @canonical/identity /interfaces/forward_auth/ruff.toml @canonical/identity -# /interfaces/gateway-metadata/ -/interfaces/gateway-metadata @canonical/service-mesh +# /interfaces/gateway_metadata/ +/interfaces/gateway_metadata @canonical/service-mesh -# /interfaces/istio-ingress-route/ -/interfaces/istio-ingress-route @canonical/service-mesh +# /interfaces/istio_ingress_route/ +/interfaces/istio_ingress_route @canonical/service-mesh # /interfaces/istio_metadata/ /interfaces/istio_metadata @canonical/service-mesh -# /interfaces/istio-request-auth/ -/interfaces/istio-request-auth/ @canonical/service-mesh +# /interfaces/istio_request_auth/ +/interfaces/istio_request_auth/ @canonical/service-mesh # /interfaces/grafana_auth/ /interfaces/grafana_auth/interface/ @canonical/observability @@ -224,8 +224,8 @@ # /interfaces/sloth/ /interfaces/sloth/ @canonical/tracing-and-profiling -# /interfaces/service-mesh/ -/interfaces/service-mesh @canonical/service-mesh +# /interfaces/service_mesh/ +/interfaces/service_mesh @canonical/service-mesh # /interfaces/smtp/ /interfaces/smtp/interface/ @canonical/platform-engineering diff --git a/interfaces/gateway-metadata/CHANGELOG.md b/interfaces/gateway_metadata/CHANGELOG.md similarity index 76% rename from interfaces/gateway-metadata/CHANGELOG.md rename to interfaces/gateway_metadata/CHANGELOG.md index 546cdb7d6..27889fc83 100644 --- a/interfaces/gateway-metadata/CHANGELOG.md +++ b/interfaces/gateway_metadata/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 1.0.1 - 16 June 2026 + +- Minor docstring and README updates. + ## 1.0.0.post0 - 16 April 2026 Correct package metadata URLs. diff --git a/interfaces/gateway-metadata/README.md b/interfaces/gateway_metadata/README.md similarity index 89% rename from interfaces/gateway-metadata/README.md rename to interfaces/gateway_metadata/README.md index e4c941ac0..e15ac3e97 100644 --- a/interfaces/gateway-metadata/README.md +++ b/interfaces/gateway_metadata/README.md @@ -1,6 +1,6 @@ # charmlibs.interfaces.gateway_metadata -The `gateway-metadata` interface library. +The `gateway_metadata` interface library. To install, add `charmlibs-interfaces-gateway-metadata` to your Python dependencies. Then in your Python code, import as: diff --git a/interfaces/gateway-metadata/pyproject.toml b/interfaces/gateway_metadata/pyproject.toml similarity index 91% rename from interfaces/gateway-metadata/pyproject.toml rename to interfaces/gateway_metadata/pyproject.toml index d3940f08d..a48391a43 100644 --- a/interfaces/gateway-metadata/pyproject.toml +++ b/interfaces/gateway_metadata/pyproject.toml @@ -20,23 +20,23 @@ dependencies = [ ] [dependency-groups] -lint = [ # installed for `just lint interfaces/gateway-metadata` (unit, functional, and integration are also installed) +lint = [ # installed for `just lint interfaces/gateway_metadata` (unit, functional, and integration are also installed) # "typing_extensions", ] -unit = [ # installed for `just unit interfaces/gateway-metadata` +unit = [ # installed for `just unit interfaces/gateway_metadata` "ops[testing]", ] -functional = [ # installed for `just functional interfaces/gateway-metadata` +functional = [ # installed for `just functional interfaces/gateway_metadata` ] -integration = [ # installed for `just integration interfaces/gateway-metadata` +integration = [ # installed for `just integration interfaces/gateway_metadata` "jubilant", ] [project.urls] "Documentation" = "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/gateway-metadata" -"Repository" = "https://github.com/canonical/charmlibs/tree/main/interfaces/gateway-metadata" +"Repository" = "https://github.com/canonical/charmlibs/tree/main/interfaces/gateway_metadata" "Issues" = "https://github.com/canonical/charmlibs/issues" -"Changelog" = "https://github.com/canonical/charmlibs/blob/main/interfaces/gateway-metadata/CHANGELOG.md" +"Changelog" = "https://github.com/canonical/charmlibs/blob/main/interfaces/gateway_metadata/CHANGELOG.md" [build-system] requires = ["hatchling"] diff --git a/interfaces/gateway-metadata/src/charmlibs/interfaces/gateway_metadata/__init__.py b/interfaces/gateway_metadata/src/charmlibs/interfaces/gateway_metadata/__init__.py similarity index 97% rename from interfaces/gateway-metadata/src/charmlibs/interfaces/gateway_metadata/__init__.py rename to interfaces/gateway_metadata/src/charmlibs/interfaces/gateway_metadata/__init__.py index 9de405562..21d68e8d3 100644 --- a/interfaces/gateway-metadata/src/charmlibs/interfaces/gateway_metadata/__init__.py +++ b/interfaces/gateway_metadata/src/charmlibs/interfaces/gateway_metadata/__init__.py @@ -14,7 +14,7 @@ """Gateway metadata interface library. -This library provides the provider and requirer sides of the ``gateway-metadata`` +This library provides the provider and requirer sides of the ``gateway_metadata`` relation interface for sharing Kubernetes Gateway API metadata (name, namespace, deployment name, service account) between gateway providers and consuming charms. @@ -28,7 +28,7 @@ and other details. This information can be used to create custom resources that attach to the gateway. -The ``gateway-metadata`` interface provides an umbrella relation containing gateway +The ``gateway_metadata`` interface provides an umbrella relation containing gateway information that requiring charms can use to attach custom resources to the gateway managed by the provider charm. While currently used by the ``istio-ingress-k8s`` charm, it is designed for any charm that wraps a Kubernetes Gateway API resource. diff --git a/interfaces/gateway-metadata/src/charmlibs/interfaces/gateway_metadata/_gateway_metadata.py b/interfaces/gateway_metadata/src/charmlibs/interfaces/gateway_metadata/_gateway_metadata.py similarity index 97% rename from interfaces/gateway-metadata/src/charmlibs/interfaces/gateway_metadata/_gateway_metadata.py rename to interfaces/gateway_metadata/src/charmlibs/interfaces/gateway_metadata/_gateway_metadata.py index 0148a4b42..2afbffb03 100644 --- a/interfaces/gateway-metadata/src/charmlibs/interfaces/gateway_metadata/_gateway_metadata.py +++ b/interfaces/gateway_metadata/src/charmlibs/interfaces/gateway_metadata/_gateway_metadata.py @@ -43,7 +43,7 @@ class GatewayMetadata(BaseModel): class GatewayMetadataProvider(Object): - """Provider side of the gateway-metadata interface. + """Provider side of the gateway_metadata interface. The provider publishes metadata about the Gateway workload to related applications. """ @@ -80,7 +80,7 @@ def publish_metadata(self, metadata: GatewayMetadata) -> None: class GatewayMetadataRequirer(Object): - """Requirer side of the gateway-metadata interface. + """Requirer side of the gateway_metadata interface. The requirer receives metadata about the Gateway workload from the provider. """ diff --git a/interfaces/istio-ingress-route/src/charmlibs/interfaces/istio_ingress_route/_version.py b/interfaces/gateway_metadata/src/charmlibs/interfaces/gateway_metadata/_version.py similarity index 100% rename from interfaces/istio-ingress-route/src/charmlibs/interfaces/istio_ingress_route/_version.py rename to interfaces/gateway_metadata/src/charmlibs/interfaces/gateway_metadata/_version.py diff --git a/interfaces/gateway-metadata/src/charmlibs/interfaces/gateway_metadata/py.typed b/interfaces/gateway_metadata/src/charmlibs/interfaces/gateway_metadata/py.typed similarity index 100% rename from interfaces/gateway-metadata/src/charmlibs/interfaces/gateway_metadata/py.typed rename to interfaces/gateway_metadata/src/charmlibs/interfaces/gateway_metadata/py.typed diff --git a/interfaces/gateway-metadata/tests/unit/conftest.py b/interfaces/gateway_metadata/tests/unit/conftest.py similarity index 100% rename from interfaces/gateway-metadata/tests/unit/conftest.py rename to interfaces/gateway_metadata/tests/unit/conftest.py diff --git a/interfaces/gateway-metadata/tests/unit/test_gateway_metadata.py b/interfaces/gateway_metadata/tests/unit/test_gateway_metadata.py similarity index 100% rename from interfaces/gateway-metadata/tests/unit/test_gateway_metadata.py rename to interfaces/gateway_metadata/tests/unit/test_gateway_metadata.py diff --git a/interfaces/gateway-metadata/uv.lock b/interfaces/gateway_metadata/uv.lock similarity index 100% rename from interfaces/gateway-metadata/uv.lock rename to interfaces/gateway_metadata/uv.lock diff --git a/interfaces/istio-ingress-route/CHANGELOG.md b/interfaces/istio_ingress_route/CHANGELOG.md similarity index 74% rename from interfaces/istio-ingress-route/CHANGELOG.md rename to interfaces/istio_ingress_route/CHANGELOG.md index 451f8956c..835743269 100644 --- a/interfaces/istio-ingress-route/CHANGELOG.md +++ b/interfaces/istio_ingress_route/CHANGELOG.md @@ -1,10 +1,14 @@ # Changelog -## 1.0.0 +## 1.0.2 - 16 June 2026 -Initial release. Migrated from `charms.istio_ingress_k8s.v0.istio_ingress_route` (v0.3). +- Minor docstring and README updates. ## 1.0.1 - 29 April 2026 - pins ops to >=2.23.1,<4 - pins pydantic to >=2,<3 + +## 1.0.0 + +Initial release. Migrated from `charms.istio_ingress_k8s.v0.istio_ingress_route` (v0.3). diff --git a/interfaces/istio-ingress-route/README.md b/interfaces/istio_ingress_route/README.md similarity index 89% rename from interfaces/istio-ingress-route/README.md rename to interfaces/istio_ingress_route/README.md index 09bd3038b..1d0354850 100644 --- a/interfaces/istio-ingress-route/README.md +++ b/interfaces/istio_ingress_route/README.md @@ -1,6 +1,6 @@ # charmlibs.interfaces.istio_ingress_route -The `istio-ingress-route` interface library. +The `istio_ingress_route` interface library. To install, add `charmlibs-interfaces-istio-ingress-route` to your Python dependencies. Then in your Python code, import as: diff --git a/interfaces/istio-ingress-route/pyproject.toml b/interfaces/istio_ingress_route/pyproject.toml similarity index 91% rename from interfaces/istio-ingress-route/pyproject.toml rename to interfaces/istio_ingress_route/pyproject.toml index d1418d353..ffd9ebcc1 100644 --- a/interfaces/istio-ingress-route/pyproject.toml +++ b/interfaces/istio_ingress_route/pyproject.toml @@ -20,23 +20,23 @@ dependencies = [ ] [dependency-groups] -lint = [ # installed for `just lint interfaces/istio-ingress-route` (unit, functional, and integration are also installed) +lint = [ # installed for `just lint interfaces/istio_ingress_route` (unit, functional, and integration are also installed) # "typing_extensions", ] -unit = [ # installed for `just unit interfaces/istio-ingress-route` +unit = [ # installed for `just unit interfaces/istio_ingress_route` "ops[testing]", ] -functional = [ # installed for `just functional interfaces/istio-ingress-route` +functional = [ # installed for `just functional interfaces/istio_ingress_route` ] -integration = [ # installed for `just integration interfaces/istio-ingress-route` +integration = [ # installed for `just integration interfaces/istio_ingress_route` "jubilant", ] [project.urls] "Documentation" = "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio-ingress-route" -"Repository" = "https://github.com/canonical/charmlibs/tree/main/interfaces/istio-ingress-route" +"Repository" = "https://github.com/canonical/charmlibs/tree/main/interfaces/istio_ingress_route" "Issues" = "https://github.com/canonical/charmlibs/issues" -"Changelog" = "https://github.com/canonical/charmlibs/blob/main/interfaces/istio-ingress-route/CHANGELOG.md" +"Changelog" = "https://github.com/canonical/charmlibs/blob/main/interfaces/istio_ingress_route/CHANGELOG.md" [build-system] requires = ["hatchling"] diff --git a/interfaces/istio-ingress-route/src/charmlibs/interfaces/istio_ingress_route/__init__.py b/interfaces/istio_ingress_route/src/charmlibs/interfaces/istio_ingress_route/__init__.py similarity index 98% rename from interfaces/istio-ingress-route/src/charmlibs/interfaces/istio_ingress_route/__init__.py rename to interfaces/istio_ingress_route/src/charmlibs/interfaces/istio_ingress_route/__init__.py index d158ba3eb..2946109ad 100644 --- a/interfaces/istio-ingress-route/src/charmlibs/interfaces/istio_ingress_route/__init__.py +++ b/interfaces/istio_ingress_route/src/charmlibs/interfaces/istio_ingress_route/__init__.py @@ -14,7 +14,7 @@ r"""Istio ingress route interface library. -This library provides the provider and requirer sides of the ``istio-ingress-route`` +This library provides the provider and requirer sides of the ``istio_ingress_route`` relation interface for advanced ingress routing through the istio-ingress-k8s charm. What is this library for? @@ -24,7 +24,7 @@ that interface has limitations: it cannot open multiple ports, configure custom path prefixes, apply URL rewrites or redirects, or set up gRPC routing. -The ``istio-ingress-route`` interface fills this gap. It allows requiring charms to +The ``istio_ingress_route`` interface fills this gap. It allows requiring charms to publish rich routing configurations — multi-port listeners, HTTP path matching, gRPC method routing, URL rewrite filters, request redirect filters — that the ``istio-ingress-k8s`` charm translates into Kubernetes Gateway API resources diff --git a/interfaces/istio-ingress-route/src/charmlibs/interfaces/istio_ingress_route/_istio_ingress_route.py b/interfaces/istio_ingress_route/src/charmlibs/interfaces/istio_ingress_route/_istio_ingress_route.py similarity index 99% rename from interfaces/istio-ingress-route/src/charmlibs/interfaces/istio_ingress_route/_istio_ingress_route.py rename to interfaces/istio_ingress_route/src/charmlibs/interfaces/istio_ingress_route/_istio_ingress_route.py index 24b60fc62..09c540141 100644 --- a/interfaces/istio-ingress-route/src/charmlibs/interfaces/istio_ingress_route/_istio_ingress_route.py +++ b/interfaces/istio_ingress_route/src/charmlibs/interfaces/istio_ingress_route/_istio_ingress_route.py @@ -388,7 +388,7 @@ def protocol(self) -> ProtocolType: # Main Config # ------------------------------------------------------------------- class IstioIngressRouteConfig(BaseModel): - """Complete configuration for istio-ingress-route.""" + """Complete configuration for istio_ingress_route.""" model: str = Field(description='The model (namespace) where backend services live') listeners: list[Listener] = Field(default_factory=lambda: list[Listener]()) @@ -564,7 +564,7 @@ def get_config(self, relation: 'Relation') -> IstioIngressRouteConfig | None: # Requirer # ------------------------------------------------------------------- class IstioIngressRouteRequirer(Object): - """Handles the requirer side of the istio-ingress-route interface. + """Handles the requirer side of the istio_ingress_route interface. This class provides an API for publishing routing configurations to the istio-ingress charm through the `istio-ingress-route` relation. diff --git a/interfaces/istio-request-auth/src/charmlibs/interfaces/istio_request_auth/_version.py b/interfaces/istio_ingress_route/src/charmlibs/interfaces/istio_ingress_route/_version.py similarity index 96% rename from interfaces/istio-request-auth/src/charmlibs/interfaces/istio_request_auth/_version.py rename to interfaces/istio_ingress_route/src/charmlibs/interfaces/istio_ingress_route/_version.py index b02d9105e..f500a8ad9 100644 --- a/interfaces/istio-request-auth/src/charmlibs/interfaces/istio_request_auth/_version.py +++ b/interfaces/istio_ingress_route/src/charmlibs/interfaces/istio_ingress_route/_version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = '0.0.2' +__version__ = '1.0.2' diff --git a/interfaces/istio-ingress-route/src/charmlibs/interfaces/istio_ingress_route/py.typed b/interfaces/istio_ingress_route/src/charmlibs/interfaces/istio_ingress_route/py.typed similarity index 100% rename from interfaces/istio-ingress-route/src/charmlibs/interfaces/istio_ingress_route/py.typed rename to interfaces/istio_ingress_route/src/charmlibs/interfaces/istio_ingress_route/py.typed diff --git a/interfaces/istio-ingress-route/tests/unit/conftest.py b/interfaces/istio_ingress_route/tests/unit/conftest.py similarity index 100% rename from interfaces/istio-ingress-route/tests/unit/conftest.py rename to interfaces/istio_ingress_route/tests/unit/conftest.py diff --git a/interfaces/istio-ingress-route/tests/unit/test_istio_ingress_route.py b/interfaces/istio_ingress_route/tests/unit/test_istio_ingress_route.py similarity index 100% rename from interfaces/istio-ingress-route/tests/unit/test_istio_ingress_route.py rename to interfaces/istio_ingress_route/tests/unit/test_istio_ingress_route.py diff --git a/interfaces/istio-ingress-route/uv.lock b/interfaces/istio_ingress_route/uv.lock similarity index 100% rename from interfaces/istio-ingress-route/uv.lock rename to interfaces/istio_ingress_route/uv.lock diff --git a/interfaces/istio-request-auth/CHANGELOG.md b/interfaces/istio_request_auth/CHANGELOG.md similarity index 87% rename from interfaces/istio-request-auth/CHANGELOG.md rename to interfaces/istio_request_auth/CHANGELOG.md index 6a1a6e65f..0e57d15e3 100644 --- a/interfaces/istio-request-auth/CHANGELOG.md +++ b/interfaces/istio_request_auth/CHANGELOG.md @@ -1,3 +1,14 @@ +# Changelog + +## 0.0.3 - 16 June 2026 + +- Minor docstring and README updates. + +## 0.0.2 - 29 April 2026 + +- pins ops to >=2.23.1,<4 +- pins pydantic to >=2,<3 + ## 0.0.1 - 22 April 2026 Initial release. @@ -8,8 +19,3 @@ The initial `istio-request-auth` interface library provides the following featur - Lets charms specify multiple trusted issuers and their corresponding header-claim mappings as a list of jwt_rules - Provides convienience data models for describing JWTRules - Models the top level data bag as datamodel - -## 0.0.2 - 29 April 2026 - -- pins ops to >=2.23.1,<4 -- pins pydantic to >=2,<3 diff --git a/interfaces/istio-request-auth/README.md b/interfaces/istio_request_auth/README.md similarity index 89% rename from interfaces/istio-request-auth/README.md rename to interfaces/istio_request_auth/README.md index 78667d3d3..2b44704c4 100644 --- a/interfaces/istio-request-auth/README.md +++ b/interfaces/istio_request_auth/README.md @@ -1,6 +1,6 @@ # charmlibs.interfaces.istio_request_auth -The `istio-request-auth` interface library. +The `istio_request_auth` interface library. To install, add `charmlibs-interfaces-istio-request-auth` to your Python dependencies. Then in your Python code, import as: diff --git a/interfaces/istio-request-auth/pyproject.toml b/interfaces/istio_request_auth/pyproject.toml similarity index 91% rename from interfaces/istio-request-auth/pyproject.toml rename to interfaces/istio_request_auth/pyproject.toml index 0e40683d2..4d8287125 100644 --- a/interfaces/istio-request-auth/pyproject.toml +++ b/interfaces/istio_request_auth/pyproject.toml @@ -20,23 +20,23 @@ dependencies = [ ] [dependency-groups] -lint = [ # installed for `just lint interfaces/istio-request-auth` (unit, functional, and integration are also installed) +lint = [ # installed for `just lint interfaces/istio_request_auth` (unit, functional, and integration are also installed) # "typing_extensions", ] -unit = [ # installed for `just unit interfaces/istio-request-auth` +unit = [ # installed for `just unit interfaces/istio_request_auth` "ops[testing]", ] -functional = [ # installed for `just functional interfaces/istio-request-auth` +functional = [ # installed for `just functional interfaces/istio_request_auth` ] -integration = [ # installed for `just integration interfaces/istio-request-auth` +integration = [ # installed for `just integration interfaces/istio_request_auth` "jubilant", ] [project.urls] "Documentation" = "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio-request-auth" -"Repository" = "https://github.com/canonical/charmlibs/tree/main/interfaces/istio-request-auth" +"Repository" = "https://github.com/canonical/charmlibs/tree/main/interfaces/istio_request_auth" "Issues" = "https://github.com/canonical/charmlibs/issues" -"Changelog" = "https://github.com/canonical/charmlibs/blob/main/interfaces/istio-request-auth/CHANGELOG.md" +"Changelog" = "https://github.com/canonical/charmlibs/blob/main/interfaces/istio_request_auth/CHANGELOG.md" [build-system] requires = ["hatchling"] diff --git a/interfaces/istio-request-auth/src/charmlibs/interfaces/istio_request_auth/__init__.py b/interfaces/istio_request_auth/src/charmlibs/interfaces/istio_request_auth/__init__.py similarity index 98% rename from interfaces/istio-request-auth/src/charmlibs/interfaces/istio_request_auth/__init__.py rename to interfaces/istio_request_auth/src/charmlibs/interfaces/istio_request_auth/__init__.py index f7056da8c..7a78b3c9b 100644 --- a/interfaces/istio-request-auth/src/charmlibs/interfaces/istio_request_auth/__init__.py +++ b/interfaces/istio_request_auth/src/charmlibs/interfaces/istio_request_auth/__init__.py @@ -14,7 +14,7 @@ """Istio request authentication interface library. -This library provides the provider and requirer sides of the ``istio-request-auth`` +This library provides the provider and requirer sides of the ``istio_request_auth`` relation interface for configuring Istio `RequestAuthentication `_ resources via relation data (JWT rules, JWKS endpoints, claim-to-header mapping). @@ -37,7 +37,7 @@ For applications to take advantage of this feature, they need to tell Istio which issuers they trust and which headers they want the claims in the token to be mapped to. To enable this information exchange and add the appropriate ``RequestAuthentication`` resource, the -``istio-request-auth`` interface library is introduced. +``istio_request_auth`` interface library is introduced. Security note ============= diff --git a/interfaces/istio-request-auth/src/charmlibs/interfaces/istio_request_auth/_istio_request_auth.py b/interfaces/istio_request_auth/src/charmlibs/interfaces/istio_request_auth/_istio_request_auth.py similarity index 98% rename from interfaces/istio-request-auth/src/charmlibs/interfaces/istio_request_auth/_istio_request_auth.py rename to interfaces/istio_request_auth/src/charmlibs/interfaces/istio_request_auth/_istio_request_auth.py index f2f1f5aca..fc94d7107 100644 --- a/interfaces/istio-request-auth/src/charmlibs/interfaces/istio_request_auth/_istio_request_auth.py +++ b/interfaces/istio_request_auth/src/charmlibs/interfaces/istio_request_auth/_istio_request_auth.py @@ -89,7 +89,7 @@ class _RequestAuthData(BaseModel): class IstioRequestAuthProvider(Object): - """Provider side of the istio-request-auth interface. + """Provider side of the istio_request_auth interface. Used by the ingress charm to read JWT authentication rules from all related applications. @@ -181,7 +181,7 @@ def get_data(self) -> dict[str, list[JWTRule]]: class IstioRequestAuthRequirer(Object): - """Requirer side of the istio-request-auth interface. + """Requirer side of the istio_request_auth interface. Used by downstream applications to publish their JWT authentication rules to the ingress charm. diff --git a/interfaces/service-mesh/src/charmlibs/interfaces/service_mesh/_version.py b/interfaces/istio_request_auth/src/charmlibs/interfaces/istio_request_auth/_version.py similarity index 96% rename from interfaces/service-mesh/src/charmlibs/interfaces/service_mesh/_version.py rename to interfaces/istio_request_auth/src/charmlibs/interfaces/istio_request_auth/_version.py index fba7cb9a9..4f3c62524 100644 --- a/interfaces/service-mesh/src/charmlibs/interfaces/service_mesh/_version.py +++ b/interfaces/istio_request_auth/src/charmlibs/interfaces/istio_request_auth/_version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = '1.0.1' +__version__ = '0.0.3' diff --git a/interfaces/istio-request-auth/src/charmlibs/interfaces/istio_request_auth/py.typed b/interfaces/istio_request_auth/src/charmlibs/interfaces/istio_request_auth/py.typed similarity index 100% rename from interfaces/istio-request-auth/src/charmlibs/interfaces/istio_request_auth/py.typed rename to interfaces/istio_request_auth/src/charmlibs/interfaces/istio_request_auth/py.typed diff --git a/interfaces/istio-request-auth/tests/unit/conftest.py b/interfaces/istio_request_auth/tests/unit/conftest.py similarity index 100% rename from interfaces/istio-request-auth/tests/unit/conftest.py rename to interfaces/istio_request_auth/tests/unit/conftest.py diff --git a/interfaces/istio-request-auth/tests/unit/test_istio_request_auth.py b/interfaces/istio_request_auth/tests/unit/test_istio_request_auth.py similarity index 100% rename from interfaces/istio-request-auth/tests/unit/test_istio_request_auth.py rename to interfaces/istio_request_auth/tests/unit/test_istio_request_auth.py diff --git a/interfaces/istio-request-auth/uv.lock b/interfaces/istio_request_auth/uv.lock similarity index 100% rename from interfaces/istio-request-auth/uv.lock rename to interfaces/istio_request_auth/uv.lock diff --git a/interfaces/service-mesh/CHANGELOG.md b/interfaces/service_mesh/CHANGELOG.md similarity index 77% rename from interfaces/service-mesh/CHANGELOG.md rename to interfaces/service_mesh/CHANGELOG.md index 9afabe381..857747f8e 100644 --- a/interfaces/service-mesh/CHANGELOG.md +++ b/interfaces/service_mesh/CHANGELOG.md @@ -1,11 +1,15 @@ # Changelog -## 1.0.0 +## 1.0.2 - 16 June 2026 -Initial release. Migrated from `charms.istio_beacon_k8s.v0.service_mesh` (v0.19). +- Minor docstring and README updates. ## 1.0.1 - 29 April 2026 - pins ops to >=2.23.1,<4 - pins pydantic to >=2,<3 - pins canonical-service-mesh to <1 + +## 1.0.0 + +Initial release. Migrated from `charms.istio_beacon_k8s.v0.service_mesh` (v0.19). diff --git a/interfaces/service-mesh/README.md b/interfaces/service_mesh/README.md similarity index 90% rename from interfaces/service-mesh/README.md rename to interfaces/service_mesh/README.md index d8cd6b46f..293306799 100644 --- a/interfaces/service-mesh/README.md +++ b/interfaces/service_mesh/README.md @@ -1,6 +1,6 @@ # charmlibs.interfaces.service_mesh -The `service-mesh` interface library. +The `service_mesh` interface library. To install, add `charmlibs-interfaces-service-mesh` to your Python dependencies. Then in your Python code, import as: diff --git a/interfaces/service-mesh/pyproject.toml b/interfaces/service_mesh/pyproject.toml similarity index 91% rename from interfaces/service-mesh/pyproject.toml rename to interfaces/service_mesh/pyproject.toml index 2c3dc4f53..7d3c077c6 100644 --- a/interfaces/service-mesh/pyproject.toml +++ b/interfaces/service_mesh/pyproject.toml @@ -21,23 +21,23 @@ dependencies = [ ] [dependency-groups] -lint = [ # installed for `just lint interfaces/service-mesh` (unit, functional, and integration are also installed) +lint = [ # installed for `just lint interfaces/service_mesh` (unit, functional, and integration are also installed) # "typing_extensions", ] -unit = [ # installed for `just unit interfaces/service-mesh` +unit = [ # installed for `just unit interfaces/service_mesh` "ops[testing]", ] -functional = [ # installed for `just functional interfaces/service-mesh` +functional = [ # installed for `just functional interfaces/service_mesh` ] -integration = [ # installed for `just integration interfaces/service-mesh` +integration = [ # installed for `just integration interfaces/service_mesh` "jubilant", ] [project.urls] "Documentation" = "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/service-mesh" -"Repository" = "https://github.com/canonical/charmlibs/tree/main/interfaces/service-mesh" +"Repository" = "https://github.com/canonical/charmlibs/tree/main/interfaces/service_mesh" "Issues" = "https://github.com/canonical/charmlibs/issues" -"Changelog" = "https://github.com/canonical/charmlibs/blob/main/interfaces/service-mesh/CHANGELOG.md" +"Changelog" = "https://github.com/canonical/charmlibs/blob/main/interfaces/service_mesh/CHANGELOG.md" [build-system] requires = ["hatchling"] diff --git a/interfaces/service-mesh/src/charmlibs/interfaces/service_mesh/__init__.py b/interfaces/service_mesh/src/charmlibs/interfaces/service_mesh/__init__.py similarity index 100% rename from interfaces/service-mesh/src/charmlibs/interfaces/service_mesh/__init__.py rename to interfaces/service_mesh/src/charmlibs/interfaces/service_mesh/__init__.py diff --git a/interfaces/service-mesh/src/charmlibs/interfaces/service_mesh/_service_mesh.py b/interfaces/service_mesh/src/charmlibs/interfaces/service_mesh/_service_mesh.py similarity index 99% rename from interfaces/service-mesh/src/charmlibs/interfaces/service_mesh/_service_mesh.py rename to interfaces/service_mesh/src/charmlibs/interfaces/service_mesh/_service_mesh.py index d5817448f..f8eb51504 100644 --- a/interfaces/service-mesh/src/charmlibs/interfaces/service_mesh/_service_mesh.py +++ b/interfaces/service_mesh/src/charmlibs/interfaces/service_mesh/_service_mesh.py @@ -137,7 +137,7 @@ def _validate_unit_policy(self) -> None: class ServiceMeshProviderAppData(pydantic.BaseModel): - """Data provided by the provider side of the service-mesh interface.""" + """Data provided by the provider side of the service_mesh interface.""" labels: dict[str, str] mesh_type: MeshType diff --git a/interfaces/gateway-metadata/src/charmlibs/interfaces/gateway_metadata/_version.py b/interfaces/service_mesh/src/charmlibs/interfaces/service_mesh/_version.py similarity index 95% rename from interfaces/gateway-metadata/src/charmlibs/interfaces/gateway_metadata/_version.py rename to interfaces/service_mesh/src/charmlibs/interfaces/service_mesh/_version.py index fb0199b45..f500a8ad9 100644 --- a/interfaces/gateway-metadata/src/charmlibs/interfaces/gateway_metadata/_version.py +++ b/interfaces/service_mesh/src/charmlibs/interfaces/service_mesh/_version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = '1.0.0.post0' +__version__ = '1.0.2' diff --git a/interfaces/service-mesh/src/charmlibs/interfaces/service_mesh/py.typed b/interfaces/service_mesh/src/charmlibs/interfaces/service_mesh/py.typed similarity index 100% rename from interfaces/service-mesh/src/charmlibs/interfaces/service_mesh/py.typed rename to interfaces/service_mesh/src/charmlibs/interfaces/service_mesh/py.typed diff --git a/interfaces/service-mesh/tests/unit/conftest.py b/interfaces/service_mesh/tests/unit/conftest.py similarity index 100% rename from interfaces/service-mesh/tests/unit/conftest.py rename to interfaces/service_mesh/tests/unit/conftest.py diff --git a/interfaces/service-mesh/tests/unit/test_service_mesh.py b/interfaces/service_mesh/tests/unit/test_service_mesh.py similarity index 100% rename from interfaces/service-mesh/tests/unit/test_service_mesh.py rename to interfaces/service_mesh/tests/unit/test_service_mesh.py diff --git a/interfaces/service-mesh/uv.lock b/interfaces/service_mesh/uv.lock similarity index 100% rename from interfaces/service-mesh/uv.lock rename to interfaces/service_mesh/uv.lock From 7b2e7bcd82e94cbb1d40b2dd067d3dcd31c1c704 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Jun 2026 11:07:43 +0200 Subject: [PATCH 45/65] fix(nginx-k8s): sort upstream server addresses for stable nginx config (#538) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In HA deployments, `_upstreams` iterated over a `set[str]` of pod addresses whose iteration order is non-deterministic. This produced spurious nginx config diffs on every hook run, triggering unnecessary nginx reloads even when the actual upstream topology hadn't changed. ## Changes - **`_config.py`:** Replace `for addr in addresses` with `for addr in sorted(addresses)` so server entries within each upstream block are always emitted in stable lexicographic order. - **`test_config.py`:** Add `test_upstreams_servers_are_sorted` — directly exercises `_upstreams` with an unordered set of addresses and asserts the resulting server directives are sorted. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Pietro Pasotti --- nginx_k8s/CHANGELOG.md | 5 +++++ nginx_k8s/src/charmlibs/nginx_k8s/__init__.py | 2 +- nginx_k8s/src/charmlibs/nginx_k8s/_config.py | 2 +- nginx_k8s/tests/unit/test_config.py | 22 +++++++++++++++++++ 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/nginx_k8s/CHANGELOG.md b/nginx_k8s/CHANGELOG.md index f6ef8c861..c59ff65c9 100644 --- a/nginx_k8s/CHANGELOG.md +++ b/nginx_k8s/CHANGELOG.md @@ -1,3 +1,8 @@ +# 0.1.1 - 19 June 2026 + +This includes a small bugfix only. +- Server locations are now sorted, to prevent noisy config diffs which in turn would trigger spurious workload restarts. + # 0.1.0 - 24 October 2025 This includes a few features that were introduced in the parent library while this one was being reviewed, approved and merged. diff --git a/nginx_k8s/src/charmlibs/nginx_k8s/__init__.py b/nginx_k8s/src/charmlibs/nginx_k8s/__init__.py index ec71a7506..52ce02d02 100644 --- a/nginx_k8s/src/charmlibs/nginx_k8s/__init__.py +++ b/nginx_k8s/src/charmlibs/nginx_k8s/__init__.py @@ -49,4 +49,4 @@ 'TLSConfigManager', ) -__version__ = '0.1.0' +__version__ = '0.1.1' diff --git a/nginx_k8s/src/charmlibs/nginx_k8s/_config.py b/nginx_k8s/src/charmlibs/nginx_k8s/_config.py index ac42392a2..efae66305 100644 --- a/nginx_k8s/src/charmlibs/nginx_k8s/_config.py +++ b/nginx_k8s/src/charmlibs/nginx_k8s/_config.py @@ -482,7 +482,7 @@ def _upstreams(self, upstreams_to_addresses: dict[str, set[str]]) -> list[Any]: 'resolve', ], } - for addr in addresses + for addr in sorted(addresses) ], ], }) diff --git a/nginx_k8s/tests/unit/test_config.py b/nginx_k8s/tests/unit/test_config.py index a0c8a2479..1dd2c7337 100644 --- a/nginx_k8s/tests/unit/test_config.py +++ b/nginx_k8s/tests/unit/test_config.py @@ -428,6 +428,28 @@ def test_generate_nginx_config_with_extra_http_variables(): assert sample_config_path.read_text() == generated_config +def test_upstreams_servers_are_sorted(): + # GIVEN a NginxConfig with one upstream config + with mock_resolv_conf(f'nameserver {sample_dns_ip}'): + nginx = NginxConfig( + 'localhost', + upstream_configs=[NginxUpstream('otlp-grpc', 4317, 'distributor')], + server_ports_to_locations={4317: [NginxLocationConfig(backend='otlp-grpc', path='/')]}, + ) + + # WHEN _upstreams is called with multiple addresses in non-sorted order + addresses = { + 'distributor-2.svc.cluster.local', + 'distributor-0.svc.cluster.local', + 'distributor-1.svc.cluster.local', + } + upstreams = nginx._upstreams({'distributor': addresses}) + + # THEN the server entries in the upstream block are in sorted order + server_args = [d['args'][0] for d in upstreams[0]['block'] if d['directive'] == 'server'] + assert server_args == sorted(server_args) + + def test_exception_raised_if_nginx_module_missing(caplog): # GIVEN an instance of Nginx class mock_container = MagicMock() From 5c114e19a5f5315d1aef41423100e7b15abd90fa Mon Sep 17 00:00:00 2001 From: James Garner Date: Mon, 22 Jun 2026 22:25:50 +1200 Subject: [PATCH 46/65] docs: update repository links for TLS interfaces (#526) This PR updates the project URLs for the `certificate_transfer` and `tls-certificates` interface libraries to reflect our current practices: - Documentation link to the package reference docs. - Deep repository link to the appropriate directory. - Changelog link to file in repository. --- interfaces/certificate_transfer/pyproject.toml | 4 +++- interfaces/index.json | 2 +- interfaces/tls-certificates/pyproject.toml | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/interfaces/certificate_transfer/pyproject.toml b/interfaces/certificate_transfer/pyproject.toml index 41379e7e6..94ce6c5b6 100644 --- a/interfaces/certificate_transfer/pyproject.toml +++ b/interfaces/certificate_transfer/pyproject.toml @@ -32,8 +32,10 @@ integration = [ # installed for `just integration interfaces/certificate_transf ] [project.urls] -"Repository" = "https://github.com/canonical/charmlibs" +"Documentation" = "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/certificate-transfer" +"Repository" = "https://github.com/canonical/charmlibs/tree/main/interfaces/certificate_transfer" "Issues" = "https://github.com/canonical/charmlibs/issues" +"Changelog" = "https://github.com/canonical/charmlibs/blob/main/interfaces/certificate_transfer/CHANGELOG.md" [build-system] requires = ["hatchling"] diff --git a/interfaces/index.json b/interfaces/index.json index 33a386f13..9c9a7d395 100644 --- a/interfaces/index.json +++ b/interfaces/index.json @@ -817,7 +817,7 @@ "version": "1", "lib": "charmlibs.interfaces.tls_certificates", "lib_url": "https://pypi.org/project/charmlibs-interfaces-tls-certificates", - "lib_docs_url": "", + "lib_docs_url": "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/tls-certificates", "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/tls-certificates/", "summary": "Securely request TLS certificates.", "description": "The `tls-certificates` interface allows charms to securely request and receive TLS certificates. The requirer charm is responsible for its private key and defining its certificate signing requests (CSRs). The provider charm delivers the certificate for each request, including the CA chain and CA certificate.", diff --git a/interfaces/tls-certificates/pyproject.toml b/interfaces/tls-certificates/pyproject.toml index 0000e565e..812a52211 100644 --- a/interfaces/tls-certificates/pyproject.toml +++ b/interfaces/tls-certificates/pyproject.toml @@ -36,8 +36,10 @@ integration = [ # installed for `just integration interfaces/tls_certificates` ] [project.urls] -"Repository" = "https://github.com/canonical/charmlibs" +"Documentation" = "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/tls-certificates" +"Repository" = "https://github.com/canonical/charmlibs/tree/main/interfaces/tls-certificates" "Issues" = "https://github.com/canonical/charmlibs/issues" +"Changelog" = "https://github.com/canonical/charmlibs/blob/main/interfaces/tls-certificates/CHANGELOG.md" [build-system] requires = ["hatchling"] From 485159e7b866dcdd7815d7baa8a44c879750635a Mon Sep 17 00:00:00 2001 From: James Garner Date: Mon, 22 Jun 2026 22:28:26 +1200 Subject: [PATCH 47/65] docs: migrate TLS certificates docs from Discourse (#516) This PR migrates the Charmhub-hosted TLS Certificates interface docs to this repo. Some of the docs have been combined into single docs, and some docs diataxis categories were changed (details below). I've also excluded docs covering information about specific Charmhub-hosted library versions (like the v3 -> v4 migration), and content targeted at charm users rather than charm authors, which doesn't really fit for library docs. --- The tutorial was adapted as-is ([preview build](https://canonical-ubuntu-documentation-library--516.com.readthedocs.build/charmlibs/tutorials/charmlibs/interfaces/tls-certificates/tutorial/)). - Tutorials: [Getting Started (v4)](https://charmhub.io/tls-certificates-interface/docs/h-getting-started) The "Configure certificate requests" how-to adapts content from two docs ([preview build](https://canonical-ubuntu-documentation-library--516.com.readthedocs.build/charmlibs/how-to/charmlibs/interfaces/tls-certificates/configure-certificate-requests/)): - Explanation: [Common Name and SANs Attributes](https://charmhub.io/tls-certificates-interface/docs/h-common-name-and-sans-attributes) - (For Charm Developers) Reference: [Recommended Configuration Options](https://charmhub.io/tls-certificates-interface/docs/h-recommended-config-options) The "Library design" explanation adapts content three docs ([preview build](https://canonical-ubuntu-documentation-library--516.com.readthedocs.build/charmlibs/explanation/charmlibs/interfaces/tls-certificates/design/)): - Explanation: [The TLS Certificates Interface](https://charmhub.io/tls-certificates-interface/docs/h-tls-certificates-interface) - Explanation: [Automatic Certificate Renewals](https://charmhub.io/tls-certificates-interface/docs/h-certificate-renewal) - Explanation: [Security](https://charmhub.io/tls-certificates-interface/docs/h-security) The package docstring has an update to point to the new docs, which renders in the library reference docs ([preview build](https://canonical-ubuntu-documentation-library--516.com.readthedocs.build/charmlibs/reference/charmlibs/interfaces/tls-certificates/)). The package readme was also updated to point to the new docs (and the reference docs). --- Excluded historical library version info -- but maybe a table or something would be worth including in the design page in future, updated for the current versions? Let me know if you'd like to see any of that in this PR. Otherwise this content can just remain on Charmhub for historical reference. - (For Charm Developers) Reference: [Library versions](https://charmhub.io/tls-certificates-interface/docs/h-library-versions) - (For Charm Developers) Reference: [Important Change in TLS Certificates Interface V4.8](https://charmhub.io/tls-certificates-interface/docs/h-private-key-label-change) - Explanation: [Technical Differences between v3 and v4](https://charmhub.io/tls-certificates-interface/docs/h-library-differences-v3-to-v4) Excluded charm user stuff -- this should probably be hosted somewhere else though, like the TLS Certificates charm docs perhaps? Or [solution level docs](https://discourse.charmhub.io/t/creating-solutions-on-charmhub-io/20503) for TLS? - (For Charm Users) Reference: [Secure Internal Communication of a Charm](https://charmhub.io/tls-certificates-interface/docs/h-securing-internal-communication) - (For Charm Users) Reference: [Secure Client <-> App Communication](https://charmhub.io/tls-certificates-interface/docs/h-securing-api-communication) - (For Charm Users) Reference: [Trust CA certificates of TLS Providers](https://charmhub.io/tls-certificates-interface/docs/h-ca-trust-best-practices) --- interfaces/tls-certificates/README.md | 8 +- .../docs/explanation/design.md | 48 +++ .../how-to/configure-certificate-requests.md | 98 +++++ interfaces/tls-certificates/docs/tutorial.md | 347 ++++++++++++++++++ .../interfaces/tls_certificates/__init__.py | 11 +- 5 files changed, 506 insertions(+), 6 deletions(-) create mode 100644 interfaces/tls-certificates/docs/explanation/design.md create mode 100644 interfaces/tls-certificates/docs/how-to/configure-certificate-requests.md create mode 100644 interfaces/tls-certificates/docs/tutorial.md diff --git a/interfaces/tls-certificates/README.md b/interfaces/tls-certificates/README.md index b5701fcea..238f5b7a2 100644 --- a/interfaces/tls-certificates/README.md +++ b/interfaces/tls-certificates/README.md @@ -8,6 +8,8 @@ To install, add `charmlibs-interfaces-tls-certificates` to your Python dependenc from charmlibs.interfaces import tls_certificates ``` -See the [reference documentation](https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/tls-certificates). - -Also see the [usage documentation](https://charmhub.io/tls-certificates-interface) on Charmhub. This documentation was written when the library was hosted on Charmhub, so some parts might not be directly applicable. +Read more: +- [Tutorial](https://documentation.ubuntu.com/charmlibs/tutorials/charmlibs/interfaces/tls-certificates/tutorial/) +- [Library reference](https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/tls-certificates/) +- [How to configure certificate requests](https://documentation.ubuntu.com/charmlibs/how-to/charmlibs/interfaces/tls-certificates/configure-certificate-requests/) +- [Explanation of the library design](https://documentation.ubuntu.com/charmlibs/explanation/charmlibs/interfaces/tls-certificates/design/) diff --git a/interfaces/tls-certificates/docs/explanation/design.md b/interfaces/tls-certificates/docs/explanation/design.md new file mode 100644 index 000000000..a9d53427c --- /dev/null +++ b/interfaces/tls-certificates/docs/explanation/design.md @@ -0,0 +1,48 @@ +# Library design + +The TLS Certificates interface allows charms to request TLS certificates without sharing their private key. + +This library began life as a port of ``tls_certificates_interface.tls_certificates`` v4.22. + +## What the library does for you + +When a charm uses `TLSCertificatesRequiresV4`, the library: + +- Generates an RSA private key on first use and stores it in a Juju secret owned by the requiring charm. +- Builds a CSR from the `CertificateRequestAttributes` you supply and writes it to relation data. +- Receives the signed certificate from the provider and emits a `certificate_available` event. +- Tracks certificate expiry and triggers renewal automatically (see below). + +Because the library generates the private key, upgrading a charm that previously managed its own key will cause one certificate rotation: the library generates a new key, the previous CSR no longer matches, and a new certificate is issued. If you need to retain a specific key — for example, to avoid that rotation — you can pass it to `TLSCertificatesRequiresV4` at instantiation time. + +## How automatic renewals work + +The library renews certificates without any code in the requirer charm: + +1. When the requirer receives a certificate from the provider, it stores the certificate in a Juju secret with an expiry set ahead of the certificate's own expiry. +2. A `certificate_available` event is emitted, prompting the requiring charm to write the certificate where its workload expects it. +3. When the Juju secret expires, the library removes the old CSR from the relation data, deletes the secret, generates a new CSR, and writes the new CSR to the relation data. +4. The provider reads the new CSR, issues a new certificate, and writes it to relation data. +5. The requirer reads the new certificate, stores it in a fresh Juju secret, and re-emits `certificate_available`. + +The requiring charm only ever has to handle `certificate_available`; the rest is managed by the library. + +## Private key handling + +The library never transmits the requirer's private key over the relation. Only CSRs and the resulting certificates cross the relation; the private key stays inside the requiring charm's data. + +### Storage at rest + +The library stores the private key in a Juju secret owned by the requiring charm. Only the charm itself and a Juju administrator can read the secret. For details of the secret backend Juju uses, see the [Juju documentation on secret backends](https://documentation.ubuntu.com/juju/3.6/howto/manage-secret-backends/#manage-secret-backends). + +### Key generation + +Private keys are generated using the [RSA algorithm](https://en.wikipedia.org/wiki/RSA_(cryptosystem)). + +### Key labelling and multiple integrations + +When a requirer charm participates in more than one `tls-certificates` integration, the library stores one private key per relation. The Juju secret labels include both the unit number and the relation name, so each integration gets its own key and certificate without collisions. + +### Manual key rotation + +Charm authors can rotate the private key by calling the `regenerate_private_key` method, which generates a new private key, removes the old certificate requests, and sends new ones to the TLS provider. diff --git a/interfaces/tls-certificates/docs/how-to/configure-certificate-requests.md b/interfaces/tls-certificates/docs/how-to/configure-certificate-requests.md new file mode 100644 index 000000000..4b6cab0e9 --- /dev/null +++ b/interfaces/tls-certificates/docs/how-to/configure-certificate-requests.md @@ -0,0 +1,98 @@ +# Configure certificate requests + +Each certificate your charm needs is described by a `CertificateRequestAttributes` object. This page covers two decisions you need to make when you build that object: + +1. What values to put in the common name and Subject Alternative Name (SAN) fields. +2. How to let operators set those values at deploy time, rather than hard-coding them. + +## Choose values for the common name and SANs + +There is no single correct answer; the right values depend on what the certificate is used for. The guidance below covers the most common cases. + +The appropriate SANs depend on the type of communication: internal unit-to-unit communication, or external client-to-server API communication. + +### Internal communication + +Different applications can use different mechanisms for one unit to access another. If units communicate with each other using IP addresses, then the IP address provided by Juju for connecting to that unit (accessible through the `ingress_address` attribute of the `Network` class in Ops) should be included in the certificate's IP SANs. For Kubernetes charms, it is often sufficient to include the Kubernetes Service DNS name (for example, `myapp-endpoints.myapp.svc.cluster.local`). If internal communication relies on externally resolvable names, the charm should allow those names to be configured and must provide a mechanism to resolve them to the corresponding IP addresses. + +### External communication + +When a unit is accessed directly by a client (either a user or another application), the certificate should cover all possible ways the API can be reached. If clients are expected to connect using an IP address, that IP should be included in the IP SANs. If they connect using a domain name, that name should be included in the DNS SANs. The domain name should be configurable in the requirer charm. + +A better approach is to offload TLS termination to a load balancer or proxy, and have the API accessed from behind it. In that case, TLS termination occurs at the load-balancer level, and backend units do not need to manage individual certificates for external clients. + +## Expose certificate attributes as charm config + +For charms that serve **public HTTPS endpoints**, certificate attributes should be decided at deployment time by the charm's user, not hard-coded in the charm. Expose them as Juju configuration options and read them when building the `CertificateRequestAttributes`. + +The recommended set of configuration options is: + +- `common-name` +- `sans-dns` +- `organization` +- `organizational-unit` +- `email-address` +- `country-name` +- `state-or-province-name` +- `locality-name` + +Read each option from the charm config and pass it to `CertificateRequestAttributes`: + +```python +from typing import Optional + +import ops +from charmlibs.interfaces.tls_certificates import ( + CertificateRequestAttributes, + Mode, + TLSCertificatesRequiresV4, +) + + +class TLSRequirerExample(ops.CharmBase): + + def __init__(self, framework: ops.Framework): + super().__init__(framework) + self.certificates = TLSCertificatesRequiresV4( + charm=self, + relationship_name="certificates", + certificate_requests=[self._get_certificate_request()], + mode=Mode.UNIT, + ) + + def _get_certificate_request(self) -> CertificateRequestAttributes: + return CertificateRequestAttributes( + common_name=self._get_config_common_name(), + sans_dns=self._get_sans_dns(), + organization=self._get_config_organization(), + organizational_unit=self._get_config_organizational_unit(), + email_address=self._get_config_email_address(), + country_name=self._get_config_country_name(), + state_or_province_name=self._get_config_state_or_province_name(), + locality_name=self._get_config_locality_name(), + ) + + def _get_config_common_name(self) -> str: + return self.model.config["common-name"] + + def _get_sans_dns(self) -> Optional[list[str]]: + return self.model.config.get("sans-dns") + + def _get_config_organization(self) -> str: + return self.model.config["organization"] + + def _get_config_organizational_unit(self) -> str: + return self.model.config["organizational-unit"] + + def _get_config_email_address(self) -> str: + return self.model.config["email-address"] + + def _get_config_country_name(self) -> str: + return self.model.config["country-name"] + + def _get_config_state_or_province_name(self) -> str: + return self.model.config["state-or-province-name"] + + def _get_config_locality_name(self) -> str: + return self.model.config["locality-name"] +``` diff --git a/interfaces/tls-certificates/docs/tutorial.md b/interfaces/tls-certificates/docs/tutorial.md new file mode 100644 index 000000000..921e2db14 --- /dev/null +++ b/interfaces/tls-certificates/docs/tutorial.md @@ -0,0 +1,347 @@ +# Getting started + +In this tutorial, we will take a [working Nginx charm](https://github.com/canonical/tls-certificates-interface-demo) and add the TLS Certificates integration using the `charmlibs.interfaces.tls_certificates` library. + +## Pre-requisites + +### Knowledge +- You have some experience writing charms and are comfortable with the charm development tooling + +### Software +- Ubuntu 22.04 +- A Juju controller with at least version 3.0 running on MicroK8s. +- Charmcraft + +## 1. Pack and deploy the nginx demo charm +In this section, we will deploy a demo Nginx charm and access its HTTP address via our browser. + +Clone the TLS Certificates Interface Demo project: +```shell +git clone git@github.com:canonical/tls-certificates-interface-demo.git +``` + +Move to the repo directory: + +```shell +cd tls-certificates-interface-demo +``` + +Pack the charm + +```shell +charmcraft pack +``` + +Create a Juju model: + +```shell +juju add-model demo +``` + +Deploy the charm + +```shell +juju deploy ./tls-certificates-interface-demo_amd64.charm nginx-http --resource nginx-image=nginx:1.27.1 +``` + +Wait for the charm to go to the Active/Idle status: + +```shell +> juju status +Model Controller Cloud/Region Version SLA Timestamp +demo microk8s-localhost microk8s/localhost 3.5.3 unsupported 15:12:25-04:00 + +App Version Status Scale Charm Channel Rev Address Exposed Message +nginx-http active 1 tls-certificates-interface-demo 0 10.152.183.199 no + +Unit Workload Agent Address Ports Message +nginx-http/0* active idle 10.1.19.158 +``` + +Using your browser, navigate to the application address on port 8080 using the HTTP scheme. Here this would be `http://10.152.183.199:8080`. + +You should see the default Nginx landing page ("Welcome to nginx!") with a short paragraph confirming the server is working and a link to nginx.org. + +We now know that have a working Nginx charm. + +## 2. Import and use the TLS Certificates Library + +This section outlines the changes that have to be made to the Nginx charm to support the TLS certificates integration. You can use [this pull request](https://github.com/canonical/tls-certificates-interface-demo/pull/5) as reference as well. + +Add `charmlibs-interfaces-tls-certificates` to your Python dependencies, then import the classes you need at the top of `src/charm.py`: + +```python +from charmlibs.interfaces.tls_certificates import ( + Certificate, + CertificateRequestAttributes, + Mode, + PrivateKey, + TLSCertificatesRequiresV4, +) +``` + +All of the following changes are made in `src/charm.py`. + +Add the following global variables: + +```python +CERTS_DIR_PATH = "/etc/nginx" +PRIVATE_KEY_NAME = "nginx.key" +CERTIFICATE_NAME = "nginx.pem" +``` + +In the charm's class constructor, instantiate a `TLSCertificatesRequiresV4` object and have the `certificate_available` event handled by our central `_configure` event handler: + +```python + +class TlsCertificatesInterfaceDemoCharm(ops.CharmBase): + """Charm the service.""" + + def __init__(self, framework: ops.Framework): + super().__init__(framework) + ... + self.certificates = TLSCertificatesRequiresV4( + charm=self, + relationship_name="certificates", + certificate_requests=[self._get_certificate_request_attributes()], + mode=Mode.UNIT, + ) + ... + framework.observe( + self.certificates.on.certificate_available, self._configure + ) + ... +``` + +Add a `_get_certificate_request_attributes` method that describes the attributes of the certificate we'd like to request. + +```python + +class TlsCertificatesInterfaceDemoCharm(ops.CharmBase): + + def _get_certificate_request_attributes(self) -> CertificateRequestAttributes: + return CertificateRequestAttributes(common_name="example.com") + +``` + +In the collect unit status event handler, have the charm go to `Blocked` status until it is integrated with a TLS Certificates Provider: + +```python + +class TlsCertificatesInterfaceDemoCharm(ops.CharmBase): + + def __init__(self, framework: ops.Framework): + ... + framework.observe(self.on.collect_unit_status, self._on_collect_status) + + def _on_collect_status(self, event: ops.CollectStatusEvent): + ... + if not self._relation_created("certificates"): + event.add_status( + ops.BlockedStatus("certificates integration not created") + ) + return + ... +``` + +Update the `_configure` event handler to manage TLS Certificates and restart the Pebble container when certificates have changed: + +```python +class TlsCertificatesInterfaceDemoCharm(ops.CharmBase): + """Charm the service.""" + + def __init__(self, framework: ops.Framework): + super().__init__(framework) + ... + framework.observe(self.on["nginx"].pebble_ready, self._configure) + framework.observe(self.on.config_changed, self._configure) + framework.observe(self.certificates.on.certificate_available, self._configure) + + def _configure(self, _: ops.EventBase): + if not self.container.can_connect(): + return + if not self._relation_created("certificates"): + return + if not self._certificate_is_available(): + return + certificate_update_required = self._check_and_update_certificate() + desired_config_file = self._generate_config_file() + if config_update_required := self._is_config_update_required(desired_config_file): + self._push_config_file(content=desired_config_file) + should_restart = config_update_required or certificate_update_required + self._configure_pebble(restart=should_restart) +``` + +We will need the following methods to handle pulling, pushing and comparing certificates: + +```python + +class TlsCertificatesInterfaceDemoCharm(ops.CharmBase): + + def _relation_created(self, relation_name: str) -> bool: + return bool(self.model.relations.get(relation_name)) + + def _certificate_is_available(self) -> bool: + cert, key = self.certificates.get_assigned_certificate( + certificate_request=self._get_certificate_request_attributes() + ) + return bool(cert and key) + + def _check_and_update_certificate(self) -> bool: + """Check if the certificate or private key needs an update and perform the update. + + This method retrieves the currently assigned certificate and private key associated with + the charm's TLS relation. It checks whether the certificate or private key has changed + or needs to be updated. If an update is necessary, the new certificate or private key is + stored. + + Returns: + bool: True if either the certificate or the private key was updated, False otherwise. + """ + provider_certificate, private_key = self.certificates.get_assigned_certificate( + certificate_request=self._get_certificate_request_attributes() + ) + if not provider_certificate or not private_key: + logger.debug("Certificate or private key is not available") + return False + if certificate_update_required := self._is_certificate_update_required( + provider_certificate.certificate + ): + self._store_certificate(certificate=provider_certificate.certificate) + if private_key_update_required := self._is_private_key_update_required(private_key): + self._store_private_key(private_key=private_key) + return certificate_update_required or private_key_update_required + + def _is_certificate_update_required(self, certificate: Certificate) -> bool: + return self._get_existing_certificate() != certificate + + def _is_private_key_update_required(self, private_key: PrivateKey) -> bool: + return self._get_existing_private_key() != private_key + + def _get_existing_certificate(self) -> Optional[Certificate]: + return self._get_stored_certificate() if self._certificate_is_stored() else None + + def _get_existing_private_key(self) -> Optional[PrivateKey]: + return self._get_stored_private_key() if self._private_key_is_stored() else None + + def _certificate_is_stored(self) -> bool: + return self.container.exists(path=f"{CERTS_DIR_PATH}/{CERTIFICATE_NAME}") + + def _private_key_is_stored(self) -> bool: + return self.container.exists(path=f"{CERTS_DIR_PATH}/{PRIVATE_KEY_NAME}") + + def _get_stored_certificate(self) -> Certificate: + cert_string = str(self.container.pull(path=f"{CERTS_DIR_PATH}/{CERTIFICATE_NAME}").read()) + return Certificate.from_string(cert_string) + + def _get_stored_private_key(self) -> PrivateKey: + key_string = str(self.container.pull(path=f"{CERTS_DIR_PATH}/{PRIVATE_KEY_NAME}").read()) + return PrivateKey.from_string(key_string) + + def _store_certificate(self, certificate: Certificate) -> None: + """Store certificate in workload.""" + self.container.push(path=f"{CERTS_DIR_PATH}/{CERTIFICATE_NAME}", source=str(certificate)) + logger.info("Pushed certificate pushed to workload") + + def _store_private_key(self, private_key: PrivateKey) -> None: + """Store private key in workload.""" + self.container.push( + path=f"{CERTS_DIR_PATH}/{PRIVATE_KEY_NAME}", + source=str(private_key), + ) + logger.info("Pushed private key to workload") + +``` + +## 3. Handle attribute changes + +The `CertificateRequestAttributes` can change. For the library to pick up the changes and generate new CSRs, you can register `refresh_events` when instantiating `TLSCertificatesRequiresV4`: + +```python + +class TlsCertificatesInterfaceDemoCharm(ops.CharmBase): + """Charm the service.""" + + def __init__(self, framework: ops.Framework): + super().__init__(framework) + ... + self.certificates = TLSCertificatesRequiresV4( + charm=self, + relationship_name="certificates", + certificate_requests=[self._get_certificate_request_attributes()], + mode=Mode.UNIT, + refresh_events=[self.on.config_changed] + ) + ... + framework.observe( + self.certificates.on.certificate_available, self._configure + ) + ... +``` +Or you can use the public `sync` function of the library to trigger the same refresh process: + +```python +self.certificates.sync() +``` + + +## 4. Deploy the nginx charm and integrate it with a TLS provider + +Deploy the new nginx charm: + +``` +juju deploy ./tls-certificates-interface-demo_amd64.charm nginx-https --resource nginx-image=nginx:1.27.1 +``` + +Wait for the charm to go to the Blocked/idle status: + +```shell +> juju status +Model Controller Cloud/Region Version SLA Timestamp +demo microk8s-localhost microk8s/localhost 3.5.3 unsupported 15:15:22-04:00 + +App Version Status Scale Charm Channel Rev Address Exposed Message +nginx-http active 1 tls-certificates-interface-demo 0 10.152.183.199 no +nginx-https blocked 1 tls-certificates-interface-demo 1 10.152.183.188 no certificates integration not created + +Unit Workload Agent Address Ports Message +nginx-http/0* active idle 10.1.19.158 +nginx-https/0* blocked idle 10.1.19.145 certificates integration not created +``` + +Deploy [Self Signed Certificates](https://charmhub.io/self-signed-certificates) (a TLS Certificates provider), and integrate it with the nginx charm + +```shell +juju deploy self-signed-certificates --channel=1/stable +juju integrate self-signed-certificates:certificates nginx-https:certificates +``` + +Wait for the `nginx-https` charm to go to the Active/Idle status: + +```shell +> juju status +Model Controller Cloud/Region Version SLA Timestamp +demo microk8s-localhost microk8s/localhost 3.5.3 unsupported 15:17:13-04:00 + +App Version Status Scale Charm Channel Rev Address Exposed Message +nginx-http active 1 tls-certificates-interface-demo 0 10.152.183.199 no +nginx-https waiting 1 tls-certificates-interface-demo 1 10.152.183.188 no installing agent +self-signed-certificates active 1 self-signed-certificates latest/stable 155 10.152.183.242 no + +Unit Workload Agent Address Ports Message +nginx-http/0* active idle 10.1.19.158 +nginx-https/0* active idle 10.1.19.145 +self-signed-certificates/0* active idle 10.1.19.146 +``` + +Using your browser, navigate to the application address on port 8080 using the HTTPS scheme. Here this would be `https://10.152.183.188:8080`. + +You should see the browser's standard "Your connection is not private" warning page (in Chrome the error code is `NET::ERR_CERT_AUTHORITY_INVALID`), telling you that the HTTPS certificate is not valid. + +Don't worry, this message is expected since the certificate we received is self-signed. + +Click on Advanced -> Proceed and you should now see the same Nginx page as in step 1 -- the default Nginx landing page, this time served over HTTPS. + +You can inspect the certificate from your browser's address bar. You should see a leaf certificate issued for `example.com` by `Self Signed Certificates`, with a chain that ends at the self-signed CA certificate generated by the `self-signed-certificates` charm. The Subject Alternative Name (SAN) section will list `DNS Name=example.com`. + +Congratulations, you added the TLS integration to your charm. diff --git a/interfaces/tls-certificates/src/charmlibs/interfaces/tls_certificates/__init__.py b/interfaces/tls-certificates/src/charmlibs/interfaces/tls_certificates/__init__.py index 97b3872c9..47668f288 100644 --- a/interfaces/tls-certificates/src/charmlibs/interfaces/tls_certificates/__init__.py +++ b/interfaces/tls-certificates/src/charmlibs/interfaces/tls_certificates/__init__.py @@ -14,10 +14,15 @@ """Manage TLS certificates using the ``tls-certificates`` interface (V1). -This is a port of ``tls_certificates_interface.tls_certificates`` v4.22. +This library implements the Requirer and Provider roles for the ``k8s_backup_target`` relation, +in the :class:`TLSCertificatesRequiresV4` and :class:`TLSCertificatesProvidesV4` classes. -Learn more about how to use the TLS Certificates interface library by reading the -`usage documentation on Charmhub `__. +Read more: + +- `Tutorial `_ +- `Library reference `_ +- `How to configure certificate requests `_ +- `Explanation of the library design `_ """ from ._tls_certificates import ( From 7e8edcc4bab54403ad1b1ffede3ec3b88f1cf13b Mon Sep 17 00:00:00 2001 From: Patricia Reinoso Date: Mon, 22 Jun 2026 12:58:54 +0200 Subject: [PATCH 48/65] chore(rollingops): update README (#536) ## Description This PR updates the README.md We no longer need to specify the python version when using just. --- rollingops/README.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/rollingops/README.md b/rollingops/README.md index 25ae16919..0ed194b8b 100644 --- a/rollingops/README.md +++ b/rollingops/README.md @@ -27,10 +27,3 @@ See the [reference documentation](https://documentation.ubuntu.com/charmlibs/ref # Developing Refer to [CONTRIBUTING.md](https://github.com/canonical/charmlibs/blob/main/CONTRIBUTING.md) for development instructions. - -**Note:** Until this [issue](https://github.com/canonical/charmlibs/issues/449) is resolved, -you must explicitly set the Python version when running `just` commands: - -```bash -just python=3.12 rollingops -``` From 9c08cc7fad12162ca4b9ca1b1516904b19534edc Mon Sep 17 00:00:00 2001 From: Sina P <55766091+sinapah@users.noreply.github.com> Date: Mon, 22 Jun 2026 10:13:06 -0400 Subject: [PATCH 49/65] feat!: NginxPrometheusExporter configures TLS (#539) Related to: https://github.com/canonical/opentelemetry-collector-k8s-operator/issues/278. This PR is based on work by @MichaelThamm in https://github.com/canonical/cos-coordinated-workers/pull/124. The referenced PR above was features in https://github.com/canonical/cos-coordinated-workers **after** `Nginx_k8s` was featured here in Charmlibs. As a result, this critical fix was never applied in the Charmlibs distribution of Nginx_k8s. This PR brings those changes to Charmlibs and makes a moderate change in logic compared to what was in https://github.com/canonical/cos-coordinated-workers/pull/124. Once this PR goes in, we will need to merge this tandem PR: https://github.com/canonical/cos-coordinated-workers/pull/163. Afterwards, Mimir, Tempo, and Loki coordinators will need to bump their CW deps. --- nginx_k8s/CHANGELOG.md | 5 + nginx_k8s/src/charmlibs/nginx_k8s/__init__.py | 2 +- .../nginx_k8s/_nginx_prometheus_exporter.py | 140 +++++++++++++--- nginx_k8s/tests/integration/test_nginx.py | 2 +- nginx_k8s/tests/unit/test_config.py | 20 ++- .../tests/unit/test_container_managers.py | 6 +- .../unit/test_nginx_prometheus_exporter.py | 152 ++++++++++++++++++ 7 files changed, 299 insertions(+), 28 deletions(-) create mode 100644 nginx_k8s/tests/unit/test_nginx_prometheus_exporter.py diff --git a/nginx_k8s/CHANGELOG.md b/nginx_k8s/CHANGELOG.md index c59ff65c9..ae5abe00a 100644 --- a/nginx_k8s/CHANGELOG.md +++ b/nginx_k8s/CHANGELOG.md @@ -1,3 +1,8 @@ +# 1.0.0 - 22 June 2026 + +This change introduces necessary fixes to logic related to the Nginx Prometheus Exporter. As part of this fix, a breaking change is introduced, warranting a bump of the major version. The changes include: +- ensure that the NginxPrometheusExporter correctly configures based on the availability of TLS certificates + # 0.1.1 - 19 June 2026 This includes a small bugfix only. diff --git a/nginx_k8s/src/charmlibs/nginx_k8s/__init__.py b/nginx_k8s/src/charmlibs/nginx_k8s/__init__.py index 52ce02d02..3609fc8d3 100644 --- a/nginx_k8s/src/charmlibs/nginx_k8s/__init__.py +++ b/nginx_k8s/src/charmlibs/nginx_k8s/__init__.py @@ -49,4 +49,4 @@ 'TLSConfigManager', ) -__version__ = '0.1.1' +__version__ = '1.0.0' diff --git a/nginx_k8s/src/charmlibs/nginx_k8s/_nginx_prometheus_exporter.py b/nginx_k8s/src/charmlibs/nginx_k8s/_nginx_prometheus_exporter.py index cd0e19ebf..d5fabab86 100644 --- a/nginx_k8s/src/charmlibs/nginx_k8s/_nginx_prometheus_exporter.py +++ b/nginx_k8s/src/charmlibs/nginx_k8s/_nginx_prometheus_exporter.py @@ -3,7 +3,21 @@ """Nginx-prometheus-exporter module.""" +import hashlib + import ops +import yaml + +from ._tls_config import TLSConfig + +PROM_EXPORTER_DIR = '/etc/exporter' +PROM_EXPORTER_KEY_PATH = f'{PROM_EXPORTER_DIR}/certs/server.key' +PROM_EXPORTER_CERT_PATH = f'{PROM_EXPORTER_DIR}/certs/server.crt' +PROM_EXPORTER_WEB_CONFIG = f'{PROM_EXPORTER_DIR}/web-config.yaml' + + +def sha256(value: str) -> str: + return hashlib.sha256(value.encode()).hexdigest() class NginxPrometheusExporter: @@ -18,39 +32,129 @@ def __init__( container: ops.Container, nginx_port: int = 8080, nginx_insecure: bool = False, + nginx_tls_port: int = 443, nginx_prometheus_exporter_port: int = 9113, + nginx_serves_tls: bool = False, ) -> None: self.port = nginx_prometheus_exporter_port self._container = container self._nginx_insecure = nginx_insecure self._nginx_port = nginx_port + self._nginx_tls_port = nginx_tls_port + self._nginx_serves_tls = nginx_serves_tls - def reconcile(self): + def reconcile( + self, + tls_config: TLSConfig | None = None, + # The Coordinator sets nginx_serves_tls to Nginx's are_certificates_on_disk property. + nginx_serves_tls: bool = False, + ) -> None: """Configure pebble layer and restart if necessary.""" - if self._container.can_connect(): - self._container.add_layer(self._layer_name, self.layer, combine=True) - self._container.autostart() + if not self._container.can_connect(): + return + + cert_hash = self._configure_tls(tls_config) + web_config_hash = sha256(self.web_config) + + self._container.push( + PROM_EXPORTER_WEB_CONFIG, + self.web_config, + make_dirs=True, + ) + + self._container.add_layer( + self._layer_name, + self._layer( + reload_sentinel=f'{cert_hash},{web_config_hash}', + nginx_serves_tls=nginx_serves_tls, + ), + combine=True, + ) + + self._container.replan() + + def _configure_tls(self, tls_config: TLSConfig | None) -> str: + """Write certificates to disk.""" + if not tls_config: + if self._container.exists(PROM_EXPORTER_KEY_PATH): + self._container.remove_path(PROM_EXPORTER_KEY_PATH) + if self._container.exists(PROM_EXPORTER_CERT_PATH): + self._container.remove_path(PROM_EXPORTER_CERT_PATH) + + return sha256('') + + self._container.push( + PROM_EXPORTER_KEY_PATH, + tls_config.private_key, + make_dirs=True, + permissions=0o600, + ) + + self._container.push( + PROM_EXPORTER_CERT_PATH, + tls_config.server_cert, + make_dirs=True, + permissions=0o600, + ) + + return sha256( + tls_config.private_key + tls_config.server_cert, + ) + + @property + def are_certificates_on_disk(self) -> bool: + """Return True if the certificates files are on disk. + + This is used to determine whether the exporter should serve + metrics over HTTP or HTTPS + by checking whether the certificates are present on THIS container's FS. + It has no effect on whether or not the exporter will attempt to + scrape nginx over HTTP or HTTPS. + That is determined by the `nginx_serves_tls` parameter passed to the reconciler. + """ + return ( + self._container.can_connect() + and self._container.exists(PROM_EXPORTER_KEY_PATH) + and self._container.exists(PROM_EXPORTER_CERT_PATH) + ) @property - def layer(self) -> ops.pebble.Layer: - """Return the Pebble layer for Nginx Prometheus exporter.""" - scheme = 'http' if self._nginx_insecure else 'https' + def web_config(self) -> str: + cfg: dict[str, object] = {} + + if self.are_certificates_on_disk: + cfg['tls_server_config'] = { + 'cert_file': PROM_EXPORTER_CERT_PATH, + 'key_file': PROM_EXPORTER_KEY_PATH, + } + + return yaml.safe_dump(cfg) + + def _layer(self, reload_sentinel: str, nginx_serves_tls: bool = False) -> ops.pebble.Layer: return ops.pebble.Layer({ - 'summary': 'Nginx prometheus exporter layer.', - 'description': 'Pebble config layer for the Nginx Prometheus exporter service.', + 'summary': 'Nginx prometheus exporter layer', + 'description': 'Pebble config layer for nginx-prometheus-exporter', 'services': { self._service_name: { 'override': 'replace', - 'summary': 'Nginx prometheus exporter service.', - 'command': ( - self._executable_name + ' ' - # needed because nginx might have a cert, but it may be invalid - # for 127.0.0.1 - f'--no-nginx.ssl-verify ' - f'--web.listen-address=:{self.port} ' - f'--nginx.scrape-uri={scheme}://127.0.0.1:{self._nginx_port}/status' - ), + 'summary': 'Nginx prometheus exporter', + 'command': self.command(nginx_serves_tls=nginx_serves_tls), 'startup': 'enabled', + 'environment': { + '_reload': reload_sentinel, + }, } }, }) + + def command(self, nginx_serves_tls: bool = False) -> str: + nginx_scheme = 'https' if nginx_serves_tls else 'http' + nginx_port = self._nginx_tls_port if nginx_serves_tls else self._nginx_port + + return ( + f'{self._executable_name} ' + f'--web.listen-address=:{self.port} ' + f'--nginx.scrape-uri={nginx_scheme}://127.0.0.1:{nginx_port}/status ' + f'--no-nginx.ssl-verify ' + f'--web.config.file={PROM_EXPORTER_WEB_CONFIG}' + ) diff --git a/nginx_k8s/tests/integration/test_nginx.py b/nginx_k8s/tests/integration/test_nginx.py index 8c9909536..2f6082069 100644 --- a/nginx_k8s/tests/integration/test_nginx.py +++ b/nginx_k8s/tests/integration/test_nginx.py @@ -56,4 +56,4 @@ def test_configs(juju: jubilant.Juju, charm: str): cmd = yaml.safe_load(res['nginx-prom-exporter-plan'])['services']['nginx-prometheus-exporter'][ 'command' ] - assert '--nginx.scrape-uri=https://127.0.0.1:8080/status' in cmd + assert '--nginx.scrape-uri=http://127.0.0.1:8080/status' in cmd diff --git a/nginx_k8s/tests/unit/test_config.py b/nginx_k8s/tests/unit/test_config.py index 1dd2c7337..c8ac01d5c 100644 --- a/nginx_k8s/tests/unit/test_config.py +++ b/nginx_k8s/tests/unit/test_config.py @@ -24,6 +24,12 @@ sample_dns_ip = '198.18.0.0' +# sha256('') + ',' + sha256(yaml.safe_dump({})) for the no-TLS case +_PEXP_RELOAD_NO_TLS = ( + 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855,' + 'ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356' +) + logger = logging.getLogger(__name__) @@ -142,17 +148,21 @@ def test_has_config_changed(ctx: testing.Context, nginx_container): NginxPrometheusExporter, (), { - 'summary': 'Nginx prometheus exporter layer.', - 'description': 'Pebble config layer for the Nginx Prometheus exporter service.', + 'summary': 'Nginx prometheus exporter layer', + 'description': 'Pebble config layer for nginx-prometheus-exporter', 'services': { 'nginx-prometheus-exporter': { - 'summary': 'Nginx prometheus exporter service.', + 'summary': 'Nginx prometheus exporter', 'startup': 'enabled', 'override': 'replace', 'command': 'nginx-prometheus-exporter ' - '--no-nginx.ssl-verify ' '--web.listen-address=:9113 ' - '--nginx.scrape-uri=https://127.0.0.1:8080/status', + '--nginx.scrape-uri=http://127.0.0.1:8080/status ' + '--no-nginx.ssl-verify ' + '--web.config.file=/etc/exporter/web-config.yaml', + 'environment': { + '_reload': _PEXP_RELOAD_NO_TLS, + }, } }, }, diff --git a/nginx_k8s/tests/unit/test_container_managers.py b/nginx_k8s/tests/unit/test_container_managers.py index 595eb2ce9..7c48be655 100644 --- a/nginx_k8s/tests/unit/test_container_managers.py +++ b/nginx_k8s/tests/unit/test_container_managers.py @@ -104,10 +104,10 @@ def test_layer_commands( pexp_command = ( state_out.get_container('nginx-pexp').plan.services['nginx-prometheus-exporter'].command ) - scheme = 'http' if nginx_insecure else 'https' assert ( pexp_command == f'nginx-prometheus-exporter ' - f'--no-nginx.ssl-verify ' f'--web.listen-address=:{nginx_pexp_port} ' - f'--nginx.scrape-uri={scheme}://127.0.0.1:{nginx_port}/status' + f'--nginx.scrape-uri=http://127.0.0.1:{nginx_port}/status ' + f'--no-nginx.ssl-verify ' + f'--web.config.file=/etc/exporter/web-config.yaml' ) diff --git a/nginx_k8s/tests/unit/test_nginx_prometheus_exporter.py b/nginx_k8s/tests/unit/test_nginx_prometheus_exporter.py new file mode 100644 index 000000000..9f84772d2 --- /dev/null +++ b/nginx_k8s/tests/unit/test_nginx_prometheus_exporter.py @@ -0,0 +1,152 @@ +# Copyright 2025 Canonical Ltd. +# See LICENSE file for licensing details. + +from dataclasses import replace + +import ops +import ops.testing +import pytest + +from charmlibs.nginx_k8s import NginxPrometheusExporter, TLSConfig +from charmlibs.nginx_k8s._nginx_prometheus_exporter import ( + PROM_EXPORTER_CERT_PATH, + PROM_EXPORTER_KEY_PATH, +) + +MOCK_TLS_CONFIG = TLSConfig( + server_cert='mock-server-cert', + ca_cert='mock-ca-cert', + private_key='mock-private-key', +) + + +@pytest.fixture +def exporter_context(): + return ops.testing.Context( + ops.CharmBase, + meta={'name': 'foo', 'containers': {'nginx-pexp': {}}}, + ) + + +@pytest.fixture +def exporter_container(): + return ops.testing.Container('nginx-pexp', can_connect=True) + + +@pytest.fixture +def exporter_certificate_mounts(tmp_path): + mounts = {} + for path, name in ( + (PROM_EXPORTER_KEY_PATH, 'server.key'), + (PROM_EXPORTER_CERT_PATH, 'server.crt'), + ): + temp_file = tmp_path / name + temp_file.write_text('mock-cert-data') + mounts[path] = ops.testing.Mount(location=path, source=str(temp_file)) + return mounts + + +def test_exporter_certs_mgmt( + exporter_context: ops.testing.Context, + exporter_container: ops.testing.Container, + exporter_certificate_mounts: dict, +): + # GIVEN a charm with a container that has certificate mounts + ctx = exporter_context + + # WHEN we process any event + with ctx( + ctx.on.update_status(), + state=ops.testing.State( + containers={replace(exporter_container, mounts=exporter_certificate_mounts)}, + ), + ) as mgr: + exporter = NginxPrometheusExporter(mgr.charm.unit.get_container('nginx-pexp')) + + # THEN the certificates exist on disk + assert exporter.are_certificates_on_disk + + # AND when we clear the TLS configuration + exporter._configure_tls(tls_config=None) + + # THEN the certificates are removed from disk + assert not exporter.are_certificates_on_disk + + +def test_exporter_web_config_file_switch( + exporter_context: ops.testing.Context, + exporter_container: ops.testing.Container, +): + # GIVEN a charm with an exporter container + ctx = exporter_context + + # WHEN we reconcile without TLS + with ctx( + ctx.on.update_status(), + state=ops.testing.State(containers={exporter_container}), + ) as mgr: + NginxPrometheusExporter(mgr.charm.unit.get_container('nginx-pexp')).reconcile() + state_out = mgr.run() + + # THEN the scrape URI uses HTTP on the standard nginx port + command = ( + state_out.get_container('nginx-pexp').plan.services['nginx-prometheus-exporter'].command + ) + assert '--nginx.scrape-uri=http://127.0.0.1:8080/status' in command + + # AND WHEN we reconcile with nginx_serves_tls=True + with ctx( + ctx.on.update_status(), + state=ops.testing.State(containers={exporter_container}), + ) as mgr: + NginxPrometheusExporter(mgr.charm.unit.get_container('nginx-pexp')).reconcile( + nginx_serves_tls=True + ) + state_out = mgr.run() + + # THEN the scrape URI switches to HTTPS on the TLS port + command = ( + state_out.get_container('nginx-pexp').plan.services['nginx-prometheus-exporter'].command + ) + assert '--nginx.scrape-uri=https://127.0.0.1:443/status' in command + + +def test_nginx_exporter_pebble_layer_sentinel( + exporter_context: ops.testing.Context, + exporter_container: ops.testing.Container, +): + # GIVEN a charm with an exporter container + ctx = exporter_context + + # WHEN we reconcile without TLS + with ctx( + ctx.on.update_status(), + state=ops.testing.State(containers={exporter_container}), + ) as mgr: + NginxPrometheusExporter(mgr.charm.unit.get_container('nginx-pexp')).reconcile() + state_out = mgr.run() + + sentinel_no_tls = ( + state_out.get_container('nginx-pexp') + .plan.services['nginx-prometheus-exporter'] + .environment.get('_reload') + ) + + # AND WHEN we reconcile with a TLS config + with ctx( + ctx.on.update_status(), + state=ops.testing.State(containers={exporter_container}), + ) as mgr: + NginxPrometheusExporter(mgr.charm.unit.get_container('nginx-pexp')).reconcile( + MOCK_TLS_CONFIG + ) + state_out = mgr.run() + + sentinel_tls = ( + state_out.get_container('nginx-pexp') + .plan.services['nginx-prometheus-exporter'] + .environment.get('_reload') + ) + + # THEN the reload sentinel differs between TLS and non-TLS configurations + assert sentinel_no_tls != sentinel_tls From 10449eb3a39438cb52b3aa49a13cf89ec5ae4901 Mon Sep 17 00:00:00 2001 From: Sina P <55766091+sinapah@users.noreply.github.com> Date: Mon, 22 Jun 2026 11:13:05 -0400 Subject: [PATCH 50/65] fix: _tls_config CA cert path (#543) Fixes #544 by correcting the CA cert path that TLSConfig checks. You can test this behaviour by first deploying the same bundle as you see in the issue. Then: 1. Shelling into Mimir's charm container: `juju ssh mimir/0 bash` 2. Navigate to the corresponding code in `/var/lib/juju/agents/unit-mimir-0/charm/venv/lib/python3.14/site-packages/charmlibs/nginx_k8s/_tls_config.py`. 3. Make the change this PR makes on line 34 of the file. 4. Trigger the `nginx` config to be written again: `jhack fire mimir/0 update-status` 5. Now, the Pebble log error in Otelcol (see issue) should go away. 6. Manual testing from Otelcol's workload container should succeed in hitting Mimir's remote write endpoint: `curl https://mimir-0.mimir-endpoints.test.svc.cluster.local:443/api/v1/push` --- nginx_k8s/CHANGELOG.md | 4 ++++ nginx_k8s/src/charmlibs/nginx_k8s/__init__.py | 2 +- nginx_k8s/src/charmlibs/nginx_k8s/_tls_config.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/nginx_k8s/CHANGELOG.md b/nginx_k8s/CHANGELOG.md index ae5abe00a..a32d544e8 100644 --- a/nginx_k8s/CHANGELOG.md +++ b/nginx_k8s/CHANGELOG.md @@ -1,3 +1,7 @@ +# 1.0.1 - 22 June 2026 + +This change ensures that _tls_config checks the correct path for a CA cert. If the module checks the wrong path, it cannot correctly determine whether TLS is enabled or not. + # 1.0.0 - 22 June 2026 This change introduces necessary fixes to logic related to the Nginx Prometheus Exporter. As part of this fix, a breaking change is introduced, warranting a bump of the major version. The changes include: diff --git a/nginx_k8s/src/charmlibs/nginx_k8s/__init__.py b/nginx_k8s/src/charmlibs/nginx_k8s/__init__.py index 3609fc8d3..93c49ba7f 100644 --- a/nginx_k8s/src/charmlibs/nginx_k8s/__init__.py +++ b/nginx_k8s/src/charmlibs/nginx_k8s/__init__.py @@ -49,4 +49,4 @@ 'TLSConfigManager', ) -__version__ = '1.0.0' +__version__ = '1.0.1' diff --git a/nginx_k8s/src/charmlibs/nginx_k8s/_tls_config.py b/nginx_k8s/src/charmlibs/nginx_k8s/_tls_config.py index 5e53b6546..b54e34eff 100644 --- a/nginx_k8s/src/charmlibs/nginx_k8s/_tls_config.py +++ b/nginx_k8s/src/charmlibs/nginx_k8s/_tls_config.py @@ -31,7 +31,7 @@ class TLSConfigManager: KEY_PATH = '/etc/nginx/certs/server.key' CERT_PATH = '/etc/nginx/certs/server.cert' - CA_CERT_PATH = '/usr/local/share/ca-certificates/ca.cert' + CA_CERT_PATH = '/usr/local/share/ca-certificates/ca.crt' def __init__( self, From 865da59347633765c05ab82d4063e535faa4059c Mon Sep 17 00:00:00 2001 From: James Garner Date: Tue, 23 Jun 2026 12:55:18 +1200 Subject: [PATCH 51/65] ci: exclude certain 'global' files from triggering all package tests (#545) This PR adds the ability to exclude certain files from triggering all package tests. For instance, currently anything changed under `.github/` triggers all tests to run. This makes perfect sense when workflows are changed, but doesn't really provide any value if PR templates are what's changing. The exclusions are a static list of full file paths (from the repository root) to avoid unintended matches. This doesn't change `CODEOWNERS`, so the maintainers are still requested for review as appropriate. --- .github/get-changed.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/get-changed.py b/.github/get-changed.py index 88df1635e..5e3865a38 100755 --- a/.github/get-changed.py +++ b/.github/get-changed.py @@ -32,6 +32,14 @@ 'uv.lock', 'test-requirements.txt', } +_EXCLUSIONS = { + '.github/PULL_REQUEST_TEMPLATE.md', + '.github/PULL_REQUEST_TEMPLATE/adding-a-new-library.md', + '.github/PULL_REQUEST_TEMPLATE/blank.md', + '.github/PULL_REQUEST_TEMPLATE/migrating-a-library.md', + '.github/dependabot.yaml', + '.github/zizmor.yaml', +} _REPO_ROOT = pathlib.Path(__file__).parent.parent logging.basicConfig(level=logging.DEBUG) @@ -63,7 +71,7 @@ def _main() -> None: def _get_global_changes(git_base_ref: str) -> list[str]: cmd = ['git', 'diff', '--name-only', git_base_ref] diff = subprocess.check_output(cmd, text=True).strip().splitlines() - changes = {c.split('/')[0] for c in diff} + changes = {c.split('/')[0] for c in diff if c not in _EXCLUSIONS} return sorted(_GLOBAL_FILES.intersection(changes)) From a78a99ac4c6e046ac2b6d9654dc72ff80e663d49 Mon Sep 17 00:00:00 2001 From: James Garner Date: Tue, 23 Jun 2026 13:01:30 +1200 Subject: [PATCH 52/65] docs: add PR templates (#542) This PR adds PR templates to Charmlibs, using a thin default template which presents options for more specific types of PRs: migrating an existing library, adding a new library, and a blank template for everything else. This can be extended in future if needed, for example specific teams or libraries might need their own selectable templates. Resolves #404. --------- Co-authored-by: Tony Meyer --- .docs/how-to/migrate.md | 2 +- .github/PULL_REQUEST_TEMPLATE.md | 7 ++ .../adding-a-new-library.md | 54 +++++++++++++++ .github/PULL_REQUEST_TEMPLATE/blank.md | 0 .../migrating-a-library.md | 68 +++++++++++++++++++ 5 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/PULL_REQUEST_TEMPLATE/adding-a-new-library.md create mode 100644 .github/PULL_REQUEST_TEMPLATE/blank.md create mode 100644 .github/PULL_REQUEST_TEMPLATE/migrating-a-library.md diff --git a/.docs/how-to/migrate.md b/.docs/how-to/migrate.md index b8bb16727..4b8d32ea5 100644 --- a/.docs/how-to/migrate.md +++ b/.docs/how-to/migrate.md @@ -123,7 +123,7 @@ Now follow these steps to migrate your library's source code: 1. Copy the copyright header from `__init__.py` to `_.py` to satisfy the linter. 2. Move the docstring from `_.py` to `__init__.py` so that it's included in your library's automatically built reference docs. 3. Document in the `_.py` docstring the API and patch version of the source code that you're migrating. This will be helpful for future maintainers and users if they need to debug issues. -4. Delete `LIB_ID`, `LIB_API`, and `LIB_PATCH` from `_.py` -- unless they're used internally by the library, then you'll need to keep them for now. +4. Delete `LIBID`, `LIBAPI`, and `LIBPATCH` from `_.py` -- unless they're used internally by the library, then you'll need to keep them for now. 5. Move the contents of `PYDEPS` to the `dependencies` entry in your `pyproject.toml` (using `just add`), and delete the `PYDEPS` variable. You'll also need to add any additional dependencies that were assumed to be provided by the charm, like `ops` or `pydantic`. Consider adding version constraints to your dependencies too. 6. Import the public API of your library to `__init__.py` and add the imported names to `__all__`, like this: ```python diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..43dff000c --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,7 @@ +Thanks for contributing to charmlibs! Click the `Preview` tab and select the template that best matches your PR: + +- [Migrating an existing library](?expand=1&template=migrating-a-library.md) +- [Adding a new library or interface](?expand=1&template=adding-a-new-library.md) +- [Something else](?expand=1&template=blank.md) + +To return to this page after selecting a template, just go back one page in your browser. diff --git a/.github/PULL_REQUEST_TEMPLATE/adding-a-new-library.md b/.github/PULL_REQUEST_TEMPLATE/adding-a-new-library.md new file mode 100644 index 000000000..0278e78d2 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/adding-a-new-library.md @@ -0,0 +1,54 @@ + + +This PR adds a new charm library. + +## Library being added + +- **New `charmlibs` package:** +- **Specification:** + + +This is a general library (`charmlibs.`, `just init`). +This is an interface library (`charmlibs.interfaces.`, `just init --interface`). + + +## Checklist + +Package: +- [ ] Package initialised with `just init` or `just init --interface`. +- [ ] Public API exported from `__init__.py` with `__all__`. +- [ ] Everything else is private, unless explicitly documented as public. +- [ ] Module docstring in `__init__.py` describes the package (rendered in the reference docs). +- [ ] Library version set for release (typically `1.0.0`). + +Repository metadata: +- [ ] `.docs/reference/libs.yaml` updated with a new entry. +- [ ] `CODEOWNERS` updated with a `//` entry for the owning team. + +Tests and docs: +- [ ] Unit tests added, plus functional and integration tests as appropriate. +- [ ] Unnecessary files created by `just init` have been removed (including `test_version.py` and `tests/functional` and `tests/integration` if unused). +- [ ] Diataxis docs added under `/docs/` (only if needed, prefer module docstring for lightweight docs). + + +### Interface library specific items + +- [ ] Directory name exactly matches the interface name as written in `charmcraft.yaml`. +- [ ] Interface metadata added under `interfaces//interface/` (readme, metadata, databag schema). +- [ ] Testing package added under `interfaces//testing/` exporting `relation_for_provider` and `relation_for_requirer` if needed (see [how-to guide](https://documentation.ubuntu.com/charmlibs/how-to/provide-relation-data-for-charm-tests/)). + +## Commit strategy + + + +The first commit in this PR is purely the output of `just init`. This commit can be excluded from the changes view (once verified), making it easy to se what changes were made on top of the template. diff --git a/.github/PULL_REQUEST_TEMPLATE/blank.md b/.github/PULL_REQUEST_TEMPLATE/blank.md new file mode 100644 index 000000000..e69de29bb diff --git a/.github/PULL_REQUEST_TEMPLATE/migrating-a-library.md b/.github/PULL_REQUEST_TEMPLATE/migrating-a-library.md new file mode 100644 index 000000000..63a245f58 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/migrating-a-library.md @@ -0,0 +1,68 @@ + + +This PR migrates an existing library to the charmlibs monorepo. + +## Library being migrated + +- **Charmhub-hosted library:** + - **LIBAPI:** + - **LIBPATCH:** + - **LIBID:** +- **New `charmlibs` package:** + + +This is a general library (`charmlibs.`, `just init`). +This is an interface library (`charmlibs.interfaces.`, `just init --interface`). + +## Migration status + + +This is a bug compatible migration of the Charmhub-hosted library, releasing as version 1.0.0. + +Package: +- [ ] Package initialised with `just init` or `just init --interface`. +- [ ] Code migrated to `src/charmlibs//_.py`. +- [ ] Public API exported from `__init__.py` with `__all__`. +- [ ] Charmhub lib docstring moved to `__init__.py` (this is rendered in the docs). +- [ ] Charmhub-hosted `LIBAPI` and `LIBPATCH` version documented in the migrated module's docstring. +- [ ] `LIBID`, `LIBAPI`, `LIBPATCH` removed (or retained with a note on why). +- [ ] `PYDEPS` moved to `pyproject.toml` `dependencies` with appropriate constraints. +- [ ] Library version set for release (typically `1.0.0`). + +Repository metadata: +- [ ] `.docs/reference/libs.yaml` updated with entries for new and old libs. +- [ ] `CODEOWNERS` updated with a `//` entry for the owning team. + +Tests and docs: +- [ ] Unit tests migrated, plus functional and integration tests as appropriate. +- [ ] Unnecessary files created by `just init` have been removed (including `test_version.py` and `tests/functional` and `tests/integration` if unused). +- [ ] Diataxis docs (if any) migrated to `/docs/`. + + +### Interface library specific items + +- [ ] Directory name exactly matches the interface name as written in `charmcraft.yaml`. +- [ ] Interface metadata added (or updated if necessary), or an issue created and tracked to do this as a follow-up task. +- [ ] Testing package added under `interfaces//testing/` exporting `relation_for_provider` and `relation_for_requirer` if needed (see [how-to guide](https://documentation.ubuntu.com/charmlibs/how-to/provide-relation-data-for-charm-tests/)), or an issue created and tracked to do this as a follow-up task. + + +## Commit strategy + +To make review easier, this PR begins with a series of mechanical commits that can be easily excluded from review: + + + +1. **Scaffolding** — output of `just init` or `just init --interface`. +2. **Lift and shift** — the existing Charmhub library source, tests, and docs copied to their new locations verbatim, providing a baseline for comparison with the original. +4. **Fix imports** — update import paths from `charms..v` to `charmlibs.` (or `charmlibs.interfaces.`) in code, tests and docs. +3. **Lint and format** — result of `just format` and `just lint`, with any necessary `pyproject.toml` config or ignores to avoid unwanted changes. From ac536558cf04fb074b2495e00defd5a7bcdd0265 Mon Sep 17 00:00:00 2001 From: James Garner Date: Tue, 23 Jun 2026 16:03:19 +1200 Subject: [PATCH 53/65] ci: normalize extension for zizmor config (#550) Per the zizmor docs, the `.yaml` file extensions is perfectly acceptable. Per discussion elsewhere, it's nice to keep these normalized across the repo and our wider ecosystem, and the `.yaml` file extension is far more common (`charmcraft.yaml`, `interface.yaml`, `.github/workflows/*.yaml`, etc). --- .github/{zizmor.yml => zizmor.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{zizmor.yml => zizmor.yaml} (100%) diff --git a/.github/zizmor.yml b/.github/zizmor.yaml similarity index 100% rename from .github/zizmor.yml rename to .github/zizmor.yaml From 253837417ee412b15add132de986336720ca2592 Mon Sep 17 00:00:00 2001 From: Dave Wilding Date: Tue, 23 Jun 2026 13:52:42 +0800 Subject: [PATCH 54/65] docs: move docs to canonical.com/juju/docs/charmlibs (#515) This PR updates our doc config so that all URLs use `canonical.com/juju/docs/charmlibs` instead of the backend URL from Read the Docs. Our docs are still hosted on Read the Docs, but we'll be proxying them under canonical.com/juju/docs. **Main changes** - Updated Charmlibs doc URLs in `.docs/conf.py`. - Updated hardcoded URLs for Charmlibs, excluding libs that aren't maintained by Charm Tech. - Added a script `.docs/.sphinx/_static/overwrite_links.js`, which is not strictly needed at the moment. This script makes sure that the version switcher from Read the Docs has correct URLs - but we don't use the version switcher. I'm including the script because it's a standard part of the new docs setup at Canonical. **Extra changes** - Updated the intersphinx URL and hardcoded URLs for Ops. To account for [operator#2545](https://github.com/canonical/charmlibs/pull/515) - Updated the hardcoded URLs for Jubilant. To account for [jubilant#337](https://github.com/canonical/jubilant/pull/337). - Updated the hardcoded URLs for Pebble. To account for [pebble#882](https://github.com/canonical/pebble/pull/882). --- .docs/_static/overwrite_links.js | 28 ++++ .docs/conf.py | 10 +- .docs/index.md | 2 +- .docs/reference/libs.yaml | 32 ++--- .docs/tutorial.md | 2 +- .example/README.md | 2 +- .example/pyproject.toml | 2 +- .../adding-a-new-library.md | 6 +- .../migrating-a-library.md | 6 +- .package/README.md | 4 +- .package/pyproject.toml | 2 +- .scripts/import_discourse_docs.py | 2 +- .scripts/ls.py | 2 +- .../{{ cookiecutter.project_slug }}/README.md | 2 +- .../pyproject.toml | 2 +- .../testing/README.md | 2 +- .tutorial/README.md | 2 +- AGENTS.md | 22 +-- CONTRIBUTING.md | 8 +- README.md | 34 ++--- apt/pyproject.toml | 2 +- interfaces/.example/README.md | 2 +- interfaces/.example/pyproject.toml | 2 +- interfaces/.example/testing/README.md | 2 +- interfaces/.package/README.md | 6 +- interfaces/.package/pyproject.toml | 2 +- interfaces/README.md | 2 +- interfaces/index.json | 136 +++++++++--------- passwd/README.md | 2 +- passwd/pyproject.toml | 2 +- pathops/pyproject.toml | 2 +- snap/README.md | 2 +- snap/pyproject.toml | 2 +- sysctl/README.md | 2 +- sysctl/pyproject.toml | 2 +- systemd/README.md | 2 +- systemd/pyproject.toml | 2 +- 37 files changed, 187 insertions(+), 157 deletions(-) create mode 100644 .docs/_static/overwrite_links.js diff --git a/.docs/_static/overwrite_links.js b/.docs/_static/overwrite_links.js new file mode 100644 index 000000000..7d5ef2dde --- /dev/null +++ b/.docs/_static/overwrite_links.js @@ -0,0 +1,28 @@ +// Our docs are proxied through to canonical.com/juju/docs/charmlibs. +// If multiple doc versions are configured, Read the Docs renders a version switcher. +// The version switcher has the readthedocs-hosted.com URL in links, which we don't want, +// so this script overwrites those links to use the correct public-facing URL. + +// Replace oldDomain with newDomain +const oldDomain = 'canonical-juju-charmlibs.readthedocs-hosted.com'; +const newDomain = 'canonical.com/juju/docs/charmlibs'; + +// Use a MutationObserver to wait for the RTD flyout element to appear in the DOM +const observer = new MutationObserver(function(mutations, obs) { + const rtdFlyout = document.querySelector('readthedocs-flyout'); + if (!rtdFlyout) return; + + obs.disconnect(); + + rtdFlyout.addEventListener('click', function() { + const shadowRoot = rtdFlyout.shadowRoot; + if (!shadowRoot) return; + + const anchors = shadowRoot.querySelectorAll('a'); + anchors.forEach(anchor => { + anchor.href = anchor.href.replace(new RegExp(oldDomain, 'g'), newDomain); + }); + }); +}); + +observer.observe(document.body, { childList: true, subtree: true }); diff --git a/.docs/conf.py b/.docs/conf.py index 6a9b59ae9..e2cddf821 100644 --- a/.docs/conf.py +++ b/.docs/conf.py @@ -38,12 +38,12 @@ # Project name project = "Charmlibs" author = "Canonical Ltd." -slug = 'charmlibs' # https://meta.discourse.org/t/what-is-category-slug/87897 +slug = 'juju/docs/charmlibs' # Set to the path after https://canonical.com/ html_title = f"{project} documentation" # sidebar documentation title copyright = f"{datetime.date.today().year} CC-BY-SA, {author}" # shown at the bottom of the page # Documentation website URL -ogp_site_url = "https://documentation.ubuntu.com/charmlibs/" +ogp_site_url = "https://canonical.com/juju/docs/charmlibs/" # Preview name of the documentation website ogp_site_name = project # Preview image URL @@ -75,8 +75,9 @@ templates_path = ["_templates"] # Sitemap configuration: https://sphinx-sitemap.readthedocs.io/ -html_baseurl = 'https://documentation.ubuntu.com/charmlibs/' +html_baseurl = 'https://canonical.com/juju/docs/charmlibs/' sitemap_url_scheme = '{link}' # URL scheme. Add language and version scheme elements. +sitemap_filename = 'doc-sitemap.xml' sitemap_show_lastmod = True # Include `lastmod` dates in the sitemap: sitemap_excludes = [ # Exclude generated pages from the sitemap: '404/', @@ -179,7 +180,7 @@ # myst_enable_extensions = set() intersphinx_mapping = { - 'ops': ('https://documentation.ubuntu.com/ops/latest', None), + 'ops': ('https://canonical.com/juju/docs/ops/latest', None), 'python': ('https://docs.python.org/3', None), 'juju': ('https://documentation.ubuntu.com/juju/3.6', None), 'charmcraft': ('https://documentation.ubuntu.com/charmcraft/latest', None), @@ -205,6 +206,7 @@ html_js_files = [ "https://assets.ubuntu.com/v1/287a5e8f-bundle.js", "tag_click_search.js", + "overwrite_links.js", ] # Specifies a reST snippet to be prepended to each .rst file diff --git a/.docs/index.md b/.docs/index.md index 31663a2db..f8a1b6ad5 100644 --- a/.docs/index.md +++ b/.docs/index.md @@ -1,5 +1,5 @@ --- -relatedlinks: "[Charmcraft](https://documentation.ubuntu.com/charmcraft/stable/), [Concierge](https://github.com/canonical/concierge), [Jubilant](https://documentation.ubuntu.com/jubilant/), [Juju](https://documentation.ubuntu.com/juju/3.6/), [Ops](https://documentation.ubuntu.com/ops/), [Pebble](https://documentation.ubuntu.com/pebble/)" +relatedlinks: "[Charmcraft](https://documentation.ubuntu.com/charmcraft/stable/), [Concierge](https://github.com/canonical/concierge), [Jubilant](https://canonical.com/juju/docs/jubilant/), [Juju](https://documentation.ubuntu.com/juju/3.6/), [Ops](https://canonical.com/juju/docs/ops/), [Pebble](https://ubuntu.com/docs/pebble/)" --- # Charmlibs diff --git a/.docs/reference/libs.yaml b/.docs/reference/libs.yaml index 61f7ea6f9..2fc24c88c 100644 --- a/.docs/reference/libs.yaml +++ b/.docs/reference/libs.yaml @@ -208,7 +208,7 @@ general: - name: charmlibs.apt status: recommended url: https://pypi.org/project/charmlibs-apt - docs: https://documentation.ubuntu.com/charmlibs/reference/charmlibs/apt + docs: https://canonical.com/juju/docs/charmlibs/reference/charmlibs/apt src: https://github.com/canonical/charmlibs/tree/main/apt kind: PyPI machine: true @@ -219,7 +219,7 @@ general: - name: charmlibs.passwd status: recommended url: https://pypi.org/project/charmlibs-passwd - docs: https://documentation.ubuntu.com/charmlibs/reference/charmlibs/passwd + docs: https://canonical.com/juju/docs/charmlibs/reference/charmlibs/passwd src: https://github.com/canonical/charmlibs/tree/main/passwd kind: PyPI machine: true @@ -230,7 +230,7 @@ general: - name: charmlibs.pathops status: recommended url: https://pypi.org/project/charmlibs-pathops - docs: https://documentation.ubuntu.com/charmlibs/reference/charmlibs/pathops + docs: https://canonical.com/juju/docs/charmlibs/reference/charmlibs/pathops src: https://github.com/canonical/charmlibs/tree/main/pathops kind: PyPI machine: true @@ -241,7 +241,7 @@ general: - name: charmlibs.rollingops status: recommended url: https://pypi.org/project/charmlibs-rollingops - docs: https://documentation.ubuntu.com/charmlibs/reference/charmlibs/rollingops + docs: https://canonical.com/juju/docs/charmlibs/reference/charmlibs/rollingops src: https://github.com/canonical/charmlibs/tree/main/rollingops kind: PyPI machine: true @@ -253,7 +253,7 @@ general: - name: charmlibs.snap status: recommended url: https://pypi.org/project/charmlibs-snap - docs: https://documentation.ubuntu.com/charmlibs/reference/charmlibs/snap + docs: https://canonical.com/juju/docs/charmlibs/reference/charmlibs/snap src: https://github.com/canonical/charmlibs/tree/main/snap kind: PyPI machine: true @@ -264,7 +264,7 @@ general: - name: charmlibs.sysctl status: recommended url: https://pypi.org/project/charmlibs-sysctl - docs: https://documentation.ubuntu.com/charmlibs/reference/charmlibs/sysctl + docs: https://canonical.com/juju/docs/charmlibs/reference/charmlibs/sysctl src: https://github.com/canonical/charmlibs/tree/main/sysctl kind: PyPI machine: true @@ -275,7 +275,7 @@ general: - name: charmlibs.systemd status: recommended url: https://pypi.org/project/charmlibs-systemd - docs: https://documentation.ubuntu.com/charmlibs/reference/charmlibs/systemd + docs: https://canonical.com/juju/docs/charmlibs/reference/charmlibs/systemd src: https://github.com/canonical/charmlibs/tree/main/systemd kind: PyPI machine: true @@ -784,7 +784,7 @@ interfaces: kind: Charmhub rel_name: opencti-connector rel_url_charmhub: https://charmhub.io/integrations/opencti_connector - rel_url_schema: https://documentation.ubuntu.com/charmlibs/reference/interfaces/opencti_connector/v0/ + rel_url_schema: https://canonical.com/juju/docs/charmlibs/reference/interfaces/opencti_connector/v0/ description: '' tags: - security @@ -1635,7 +1635,7 @@ interfaces: - name: charmlibs.interfaces.service_mesh status: recommended url: https://pypi.org/project/charmlibs-interfaces-service-mesh - docs: https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/service-mesh + docs: https://canonical.com/juju/docs/charmlibs/reference/charmlibs/interfaces/service-mesh src: https://github.com/canonical/charmlibs/tree/main/interfaces/service_mesh kind: PyPI rel_name: service_mesh @@ -1683,7 +1683,7 @@ interfaces: - name: charmlibs.interfaces.tls_certificates status: recommended url: https://pypi.org/project/charmlibs-interfaces-tls-certificates - docs: https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/tls_certificates + docs: https://canonical.com/juju/docs/charmlibs/reference/charmlibs/interfaces/tls_certificates src: https://github.com/canonical/charmlibs/tree/main/interfaces/tls_certificates kind: PyPI rel_name: tls_certificates @@ -1775,7 +1775,7 @@ interfaces: kind: PyPI rel_name: litmus_auth rel_url_charmhub: https://charmhub.io/integrations/litmus_auth - rel_url_schema: https://documentation.ubuntu.com/charmlibs/reference/interfaces/litmus_auth/v0/ + rel_url_schema: https://canonical.com/juju/docs/charmlibs/reference/interfaces/litmus_auth/v0/ description: '' tags: - internal @@ -1783,7 +1783,7 @@ interfaces: - name: charmlibs.interfaces.istio_request_auth status: recommended url: https://pypi.org/project/charmlibs-interfaces-istio-request-auth - docs: https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio-request-auth + docs: https://canonical.com/juju/docs/charmlibs/reference/charmlibs/interfaces/istio-request-auth src: https://github.com/canonical/charmlibs/tree/main/interfaces/istio_request_auth kind: PyPI rel_name: istio-request-auth @@ -1809,7 +1809,7 @@ interfaces: - name: charmlibs.interfaces.gateway_metadata status: recommended url: https://pypi.org/project/charmlibs-interfaces-gateway-metadata - docs: https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/gateway-metadata + docs: https://canonical.com/juju/docs/charmlibs/reference/charmlibs/interfaces/gateway-metadata src: https://github.com/canonical/charmlibs/tree/main/interfaces/gateway_metadata kind: PyPI rel_name: gateway-metadata @@ -1833,12 +1833,12 @@ interfaces: - name: charmlibs.interfaces.istio_metadata status: recommended url: https://pypi.org/project/charmlibs-interfaces-istio-metadata - docs: https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio_metadata + docs: https://canonical.com/juju/docs/charmlibs/reference/charmlibs/interfaces/istio_metadata src: https://github.com/canonical/charmlibs/tree/main/interfaces/istio_metadata kind: PyPI rel_name: istio_metadata rel_url_charmhub: '' - rel_url_schema: https://documentation.ubuntu.com/charmlibs/reference/interfaces/istio_metadata/v0/ + rel_url_schema: https://canonical.com/juju/docs/charmlibs/reference/interfaces/istio_metadata/v0/ description: Share Istio installation metadata (e.g. root namespace) between charms. tags: - networking @@ -1870,7 +1870,7 @@ interfaces: - name: charmlibs.interfaces.istio_ingress_route status: recommended url: https://pypi.org/project/charmlibs-interfaces-istio-ingress-route - docs: https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio-ingress-route + docs: https://canonical.com/juju/docs/charmlibs/reference/charmlibs/interfaces/istio-ingress-route src: https://github.com/canonical/charmlibs/tree/main/interfaces/istio_ingress_route kind: PyPI rel_name: istio-ingress-route diff --git a/.docs/tutorial.md b/.docs/tutorial.md index 6b6000f48..3d7514643 100644 --- a/.docs/tutorial.md +++ b/.docs/tutorial.md @@ -243,7 +243,7 @@ You should also feel free to split your tests across as many files as needed to For more on `ops.testing`, see: - [ops.testing reference docs for custom events](ops.testing.CharmEvents.custom) -- [ops.testing how-to for testing that a custom event is emitted](https://documentation.ubuntu.com/ops/latest/howto/manage-libraries/#test-that-the-custom-event-is-emitted) +- [ops.testing how-to for testing that a custom event is emitted](https://canonical.com/juju/docs/ops/latest/howto/manage-libraries/#test-that-the-custom-event-is-emitted) (tutorial-add-functional-tests)= ### Add functional tests diff --git a/.example/README.md b/.example/README.md index 552bb56a5..4a04468cd 100644 --- a/.example/README.md +++ b/.example/README.md @@ -8,4 +8,4 @@ To install, add `charmlibs-example` to your Python dependencies. Then in your Py from charmlibs import example ``` -See the [reference documentation](https://documentation.ubuntu.com/charmlibs/reference/charmlibs/example) for more. +See the [reference documentation](https://canonical.com/juju/docs/charmlibs/reference/charmlibs/example) for more. diff --git a/.example/pyproject.toml b/.example/pyproject.toml index fa6389b66..d41c93b6f 100644 --- a/.example/pyproject.toml +++ b/.example/pyproject.toml @@ -39,7 +39,7 @@ integration = [ # installed for `just integration example` ] [project.urls] -"Documentation" = "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/example" +"Documentation" = "https://canonical.com/juju/docs/charmlibs/reference/charmlibs/example" "Repository" = "https://github.com/canonical/charmlibs/tree/main/example" "Issues" = "https://github.com/canonical/charmlibs/issues" "Changelog" = "https://github.com/canonical/charmlibs/blob/main/example/CHANGELOG.md" diff --git a/.github/PULL_REQUEST_TEMPLATE/adding-a-new-library.md b/.github/PULL_REQUEST_TEMPLATE/adding-a-new-library.md index 0278e78d2..de0dec2d2 100644 --- a/.github/PULL_REQUEST_TEMPLATE/adding-a-new-library.md +++ b/.github/PULL_REQUEST_TEMPLATE/adding-a-new-library.md @@ -4,10 +4,10 @@ Template: Adding a new library to the charmlibs monorepo. If you selected the wrong template, go back one page in your browser. Read first: -https://documentation.ubuntu.com/charmlibs/how-to/python-package/#what-can-i-publish-under-the-charmlibs-namespace +https://canonical.com/juju/docs/charmlibs/how-to/python-package/#what-can-i-publish-under-the-charmlibs-namespace To learn how to add a new library: -https://documentation.ubuntu.com/charmlibs/tutorial/ +https://canonical.com/juju/docs/charmlibs/tutorial/ --> This PR adds a new charm library. @@ -45,7 +45,7 @@ Tests and docs: - [ ] Directory name exactly matches the interface name as written in `charmcraft.yaml`. - [ ] Interface metadata added under `interfaces//interface/` (readme, metadata, databag schema). -- [ ] Testing package added under `interfaces//testing/` exporting `relation_for_provider` and `relation_for_requirer` if needed (see [how-to guide](https://documentation.ubuntu.com/charmlibs/how-to/provide-relation-data-for-charm-tests/)). +- [ ] Testing package added under `interfaces//testing/` exporting `relation_for_provider` and `relation_for_requirer` if needed (see [how-to guide](https://canonical.com/juju/docs/charmlibs/how-to/provide-relation-data-for-charm-tests/)). ## Commit strategy diff --git a/.github/PULL_REQUEST_TEMPLATE/migrating-a-library.md b/.github/PULL_REQUEST_TEMPLATE/migrating-a-library.md index 63a245f58..3de5ac1a6 100644 --- a/.github/PULL_REQUEST_TEMPLATE/migrating-a-library.md +++ b/.github/PULL_REQUEST_TEMPLATE/migrating-a-library.md @@ -4,10 +4,10 @@ Template: Migrating an existing Charmhub-hosted library into the charmlibs monor If you selected the wrong template, go back one page in your browser. Read first: -https://documentation.ubuntu.com/charmlibs/how-to/python-package/#what-can-i-publish-under-the-charmlibs-namespace +https://canonical.com/juju/docs/charmlibs/how-to/python-package/#what-can-i-publish-under-the-charmlibs-namespace To learn how to migrate a library: -https://documentation.ubuntu.com/charmlibs/how-to/migrate/ +https://canonical.com/juju/docs/charmlibs/how-to/migrate/ --> This PR migrates an existing library to the charmlibs monorepo. @@ -53,7 +53,7 @@ Tests and docs: - [ ] Directory name exactly matches the interface name as written in `charmcraft.yaml`. - [ ] Interface metadata added (or updated if necessary), or an issue created and tracked to do this as a follow-up task. -- [ ] Testing package added under `interfaces//testing/` exporting `relation_for_provider` and `relation_for_requirer` if needed (see [how-to guide](https://documentation.ubuntu.com/charmlibs/how-to/provide-relation-data-for-charm-tests/)), or an issue created and tracked to do this as a follow-up task. +- [ ] Testing package added under `interfaces//testing/` exporting `relation_for_provider` and `relation_for_requirer` if needed (see [how-to guide](https://canonical.com/juju/docs/charmlibs/how-to/provide-relation-data-for-charm-tests/)), or an issue created and tracked to do this as a follow-up task. ## Commit strategy diff --git a/.package/README.md b/.package/README.md index 0ae25af06..79d96bae8 100644 --- a/.package/README.md +++ b/.package/README.md @@ -6,6 +6,6 @@ This namespace is for packages designed to be used by [Juju charms](https://cano Read more: -- Get started with charm development, using [Charmcraft tutorials](https://documentation.ubuntu.com/charmcraft/stable/tutorial/) and [Ops tutorials](https://documentation.ubuntu.com/ops/latest/tutorial/) -- Learn how to [use and write charm libraries](https://documentation.ubuntu.com/ops/latest/howto/manage-libraries/) +- Get started with charm development, using [Charmcraft tutorials](https://documentation.ubuntu.com/charmcraft/stable/tutorial/) and [Ops tutorials](https://canonical.com/juju/docs/ops/latest/tutorial/) +- Learn how to [use and write charm libraries](https://canonical.com/juju/docs/ops/latest/howto/manage-libraries/) - Learn how to [distribute your own charm library as a Python package](https://canonical-charmlibs.readthedocs-hosted.com/how-to/python-package/) diff --git a/.package/pyproject.toml b/.package/pyproject.toml index 4541df0d2..9f60de498 100644 --- a/.package/pyproject.toml +++ b/.package/pyproject.toml @@ -22,7 +22,7 @@ functional = [] integration = [] [project.urls] -"Documentation" = "https://documentation.ubuntu.com/charmlibs" +"Documentation" = "https://canonical.com/juju/docs/charmlibs" "Repository" = "https://github.com/canonical/charmlibs" "Issues" = "https://github.com/canonical/charmlibs/issues" diff --git a/.scripts/import_discourse_docs.py b/.scripts/import_discourse_docs.py index c638deaa5..04930dad2 100755 --- a/.scripts/import_discourse_docs.py +++ b/.scripts/import_discourse_docs.py @@ -28,7 +28,7 @@ It's a helper for migrating a library's docs into the charmlibs monorepo -- see the "Migrate your library's docs" section of the migration how-to guide: -https://documentation.ubuntu.com/charmlibs/how-to/migrate/ +https://canonical.com/juju/docs/charmlibs/how-to/migrate/ Usage: .scripts/import_discourse_docs.py diff --git a/.scripts/ls.py b/.scripts/ls.py index f1f3d40f9..b1ec742f7 100755 --- a/.scripts/ls.py +++ b/.scripts/ls.py @@ -369,7 +369,7 @@ def _get_docs_url(category: str, root: pathlib.Path, path: pathlib.Path) -> str: url = _pyproject_toml(path, root=root)['project']['urls'].get('Documentation', '') return url.strip() assert category == 'interfaces' - return f'https://documentation.ubuntu.com/charmlibs/reference/interfaces/{path.name}/' + return f'https://canonical.com/juju/docs/charmlibs/reference/interfaces/{path.name}/' def _get_lib_docs_url(category: str, root: pathlib.Path, path: pathlib.Path) -> str: diff --git a/.template/{{ cookiecutter.project_slug }}/README.md b/.template/{{ cookiecutter.project_slug }}/README.md index 1ec61fb12..52d997160 100644 --- a/.template/{{ cookiecutter.project_slug }}/README.md +++ b/.template/{{ cookiecutter.project_slug }}/README.md @@ -8,4 +8,4 @@ To install, add `{{ cookiecutter.__dist_pkg }}` to your Python dependencies. The from {{ cookiecutter.__ns }} import {{ cookiecutter.__pkg }} ``` -See the [reference documentation](https://documentation.ubuntu.com/charmlibs/reference/charmlibs/{{ cookiecutter.__path_prefix }}{{cookiecutter.__canonical_name}}) for more. +See the [reference documentation](https://canonical.com/juju/docs/charmlibs/reference/charmlibs/{{ cookiecutter.__path_prefix }}{{cookiecutter.__canonical_name}}) for more. diff --git a/.template/{{ cookiecutter.project_slug }}/pyproject.toml b/.template/{{ cookiecutter.project_slug }}/pyproject.toml index dbfc159a9..618ac5431 100644 --- a/.template/{{ cookiecutter.project_slug }}/pyproject.toml +++ b/.template/{{ cookiecutter.project_slug }}/pyproject.toml @@ -45,7 +45,7 @@ integration = [ # installed for `just integration {{ cookiecutter.__path_prefix ] [project.urls] -"Documentation" = "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/{{ cookiecutter.__path_prefix }}{{ cookiecutter.__canonical_name }}" +"Documentation" = "https://canonical.com/juju/docs/charmlibs/reference/charmlibs/{{ cookiecutter.__path_prefix }}{{ cookiecutter.__canonical_name }}" "Repository" = "https://github.com/canonical/charmlibs/tree/main/{{ cookiecutter.__path_prefix }}{{ cookiecutter.project_slug }}" "Issues" = "https://github.com/canonical/charmlibs/issues" "Changelog" = "https://github.com/canonical/charmlibs/blob/main/{{ cookiecutter.__path_prefix }}{{ cookiecutter.project_slug }}/CHANGELOG.md" diff --git a/.template/{{ cookiecutter.project_slug }}/testing/README.md b/.template/{{ cookiecutter.project_slug }}/testing/README.md index b6f77ffe6..a08d07cf7 100644 --- a/.template/{{ cookiecutter.project_slug }}/testing/README.md +++ b/.template/{{ cookiecutter.project_slug }}/testing/README.md @@ -8,4 +8,4 @@ To install, add `{{ cookiecutter.__dist_pkg }}-testing` to your Python dependenc from {{ cookiecutter.__ns }} import {{ cookiecutter.__pkg }}_testing ``` -See the [library reference documentation](https://documentation.ubuntu.com/charmlibs/reference/charmlibs/{{ cookiecutter.__path_prefix }}{{cookiecutter.__pkg}}) for more. +See the [library reference documentation](https://canonical.com/juju/docs/charmlibs/reference/charmlibs/{{ cookiecutter.__path_prefix }}{{cookiecutter.__pkg}}) for more. diff --git a/.tutorial/README.md b/.tutorial/README.md index 1d11ffae2..4bab8bfa8 100644 --- a/.tutorial/README.md +++ b/.tutorial/README.md @@ -8,4 +8,4 @@ To install, add `charmlibs-uptime` to your Python dependencies. Then in your Pyt from charmlibs import uptime ``` -See the [reference documentation](https://documentation.ubuntu.com/charmlibs/reference/charmlibs/uptime) for more. +See the [reference documentation](https://canonical.com/juju/docs/charmlibs/reference/charmlibs/uptime) for more. diff --git a/AGENTS.md b/AGENTS.md index 62775ea20..c22ea9333 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ **Key concepts:** - **Juju** — Canonical's open-source orchestration engine. Deploys and manages applications on Kubernetes and bare-metal/VM clouds. -- **Charm** — a Python program that uses the [ops](https://documentation.ubuntu.com/ops/) framework to respond to Juju lifecycle events and manage a workload. Charms can be Kubernetes-based (using [Pebble](https://documentation.ubuntu.com/pebble/) to manage container processes) or machine-based. +- **Charm** — a Python program that uses the [ops](https://canonical.com/juju/docs/ops/) framework to respond to Juju lifecycle events and manage a workload. Charms can be Kubernetes-based (using [Pebble](https://ubuntu.com/docs/pebble/) to manage container processes) or machine-based. - **Juju relation** — a named connection between two charms, backed by shared key-value stores called *databags*. Relations are how charms communicate and exchange structured configuration. - **Charm library** — a reusable Python package that charm authors import. All libraries in this repo are distributed as Python packages on PyPI (not Charmhub-hosted single-file modules, which are a legacy distribution method). @@ -40,7 +40,7 @@ charmlibs/ │ ├── testing/ # Optional testing subpackage for charm unit tests │ ├── pyproject.toml │ └── ... -├── .docs/ # Sphinx source for documentation.ubuntu.com/charmlibs +├── .docs/ # Sphinx source for canonical.com/juju/docs/charmlibs ├── .template/ # Cookiecutter template used by `just init` ├── .github/workflows/ # CI: ci.yaml, test-package.yaml, test-interface.yaml, publish.yaml ├── justfile # Task runner recipes @@ -127,7 +127,7 @@ just add interfaces/tls-certificates --requirements my-requirements.txt A test type is only executed if the corresponding `tests/` subdirectory exists. Remove a directory to skip that test type entirely. -Read more: [types of tests in the charmlibs monorepo](https://documentation.ubuntu.com/charmlibs/explanation/charmlibs-tests/). +Read more: [types of tests in the charmlibs monorepo](https://canonical.com/juju/docs/charmlibs/explanation/charmlibs-tests/). ### Running functional tests with Workshop @@ -208,7 +208,7 @@ Interface libraries manage the structured data that charms exchange over a Juju - Typically have unit and integration tests but no functional tests (all meaningful interaction is through Juju). - Use `just init --interface` to scaffold. -Read more: [how to design relation interfaces](https://documentation.ubuntu.com/charmlibs/how-to/design-relation-interfaces/), [how to provide relation data for charm tests](https://documentation.ubuntu.com/charmlibs/how-to/provide-relation-data-for-charm-tests/). +Read more: [how to design relation interfaces](https://canonical.com/juju/docs/charmlibs/how-to/design-relation-interfaces/), [how to provide relation data for charm tests](https://canonical.com/juju/docs/charmlibs/how-to/provide-relation-data-for-charm-tests/). ## Documentation @@ -220,7 +220,7 @@ just docs html pathops This is also run as part of `just check`. The full docs site (all packages) is built with `just docs`. -When writing or editing docstrings in `__init__.py` or other public modules, remember they appear verbatim in the published reference at [documentation.ubuntu.com/charmlibs](https://documentation.ubuntu.com/charmlibs). Keep them informative for library users, not implementation notes. +When writing or editing docstrings in `__init__.py` or other public modules, remember they appear verbatim in the published reference at [canonical.com/juju/docs/charmlibs](https://canonical.com/juju/docs/charmlibs). Keep them informative for library users, not implementation notes. Don't use block quotes (`>`) for "Read more", "See also", or similar cross-reference sections. Instead, use bare text like `Read more: {ref}\`some-page\`` or, for two links, a comma-separated list or, for three or more links a bulleted list. For example: @@ -232,10 +232,10 @@ Read more: {ref}`charm-libs-charmhub-hosted`, {ref}`Charmcraft | Manage librarie ### Migrating a library's docs -To bring an existing library's tutorials, how-to guides, and explanations into the monorepo, follow the **Migrate your library's docs** section of [How to migrate to the charmlibs monorepo](https://documentation.ubuntu.com/charmlibs/how-to/migrate/). In short: +To bring an existing library's tutorials, how-to guides, and explanations into the monorepo, follow the **Migrate your library's docs** section of [How to migrate to the charmlibs monorepo](https://canonical.com/juju/docs/charmlibs/how-to/migrate/). In short: - For Charmhub-hosted docs, download each Discourse topic with `uv run .scripts/import-discourse-docs.py `, choosing a diataxis category (and so an output path) for each. -- Then edit the imported pages to fit charmlibs conventions, following [How to add docs to a library](https://documentation.ubuntu.com/charmlibs/how-to/add-library-docs/). +- Then edit the imported pages to fit charmlibs conventions, following [How to add docs to a library](https://canonical.com/juju/docs/charmlibs/how-to/add-library-docs/). There is no `reference` docs category — reference docs are generated from docstrings. @@ -251,10 +251,10 @@ There is no `reference` docs category — reference docs are generated from docs | Resource | URL | |----------|-----| | charmlibs docs | .docs/index.md | -| ops (charm framework) | https://documentation.ubuntu.com/ops/ | -| ops.testing reference | https://documentation.ubuntu.com/ops/latest/reference/ops-testing/ | +| ops (charm framework) | https://canonical.com/juju/docs/ops/ | +| ops.testing reference | https://canonical.com/juju/docs/ops/latest/reference/ops-testing/ | | Juju docs | https://documentation.ubuntu.com/juju/latest/llms.txt | | Charmcraft docs | https://documentation.ubuntu.com/charmcraft/stable/ | -| Jubilant (integration test client) | https://documentation.ubuntu.com/jubilant/ | -| Pebble | https://documentation.ubuntu.com/pebble/ | +| Jubilant (integration test client) | https://canonical.com/juju/docs/jubilant/ | +| Pebble | https://ubuntu.com/docs/pebble/ | | Concierge (CI Juju setup) | https://raw.githubusercontent.com/canonical/concierge/refs/heads/main/README.md | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c82743e4f..a3071ffb2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,8 +18,8 @@ Then run `just` or `just help` from anywhere in the repository for usage. # Adding a new library Run `just init` to create a new general library, or `just init --interface` for a new interface library. -We recommend following the [tutorial](https://documentation.ubuntu.com/charmlibs/tutorial) to learn how to add your library to the `charmlibs` monorepo. -If you're migrating a library that was published elsewhere, read the [how-to guide for migrating an existing library to this repository](https://documentation.ubuntu.com/charmlibs/how-to/migrate/). +We recommend following the [tutorial](https://canonical.com/juju/docs/charmlibs/tutorial) to learn how to add your library to the `charmlibs` monorepo. +If you're migrating a library that was published elsewhere, read the [how-to guide for migrating an existing library to this repository](https://canonical.com/juju/docs/charmlibs/how-to/migrate/). # Working on an existing library @@ -57,7 +57,7 @@ just add pathops 'pydantic>=2' - Integration tests involve packing real test charms and deploying them on a Juju cloud. Pack first with `just pack-k8s ` or `just pack-machine `, then run `just integration-k8s ` or `just integration-machine `. -Read more: [the different types of tests](https://documentation.ubuntu.com/charmlibs/explanation/charmlibs-tests/). +Read more: [the different types of tests](https://canonical.com/juju/docs/charmlibs/explanation/charmlibs-tests/). # Pull requests @@ -76,4 +76,4 @@ Libraries are automatically published to PyPI when a merged PR bumps the version Any PR that would trigger a release must also update the library's `CHANGELOG.md` — CI will block the merge otherwise. -Read more: [publishing packages from the monorepo](https://documentation.ubuntu.com/charmlibs/explanation/charmlibs-publishing/). +Read more: [publishing packages from the monorepo](https://canonical.com/juju/docs/charmlibs/explanation/charmlibs-publishing/). diff --git a/README.md b/README.md index 9391d0d26..aac2be461 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ `charmlibs` is the home of Canonical's charm libraries -- Python packages used by [Juju](https://canonical.com/juju) charms. -Charms are Python programs that use the [Ops](https://documentation.ubuntu.com/ops/) framework to manage workloads on Kubernetes or machine clouds. Charm libraries package up common functionality so that teams don't have to reinvent the wheel. +Charms are Python programs that use the [Ops](https://canonical.com/juju/docs/ops/) framework to manage workloads on Kubernetes or machine clouds. Charm libraries package up common functionality so that teams don't have to reinvent the wheel. > [!IMPORTANT] > Each library in this monorepo is distributed as a separate Python package on PyPI, so charms only include what they actually need. @@ -21,44 +21,44 @@ There are two kinds of charm libraries: All public interfaces intended for use by other charms should have a corresponding `charmlibs.interfaces` library. -If your library is more of a team-internal utility, [Distribute charm libraries](https://documentation.ubuntu.com/charmlibs/how-to/python-package/) covers the alternatives. +If your library is more of a team-internal utility, [Distribute charm libraries](https://canonical.com/juju/docs/charmlibs/how-to/python-package/) covers the alternatives. Got something in mind? You can talk to us on [Matrix](https://matrix.to/#/#charmhub-charmdev:ubuntu.com) before opening a PR — we'd love to hear about it. -**Ready to dive in?** Follow the **[tutorial](https://documentation.ubuntu.com/charmlibs/tutorial)** to add a new library, or the **[migration guide](https://documentation.ubuntu.com/charmlibs/how-to/migrate/)** to port a Charmhub-hosted library. See [CONTRIBUTING.md](./CONTRIBUTING.md) for the developer quick-reference. +**Ready to dive in?** Follow the **[tutorial](https://canonical.com/juju/docs/charmlibs/tutorial)** to add a new library, or the **[migration guide](https://canonical.com/juju/docs/charmlibs/how-to/migrate/)** to port a Charmhub-hosted library. See [CONTRIBUTING.md](./CONTRIBUTING.md) for the developer quick-reference. For the details of how the monorepo works: -- [Types of tests](https://documentation.ubuntu.com/charmlibs/explanation/charmlibs-tests/): unit, functional, and Juju integration tests -- [Publishing from the monorepo](https://documentation.ubuntu.com/charmlibs/explanation/charmlibs-publishing/): semantic versioning, dev versions, trusted publishing -- Customizing your [functional](https://documentation.ubuntu.com/charmlibs/how-to/customize-functional-tests/) and [integration](https://documentation.ubuntu.com/charmlibs/how-to/customize-integration-tests/) tests. +- [Types of tests](https://canonical.com/juju/docs/charmlibs/explanation/charmlibs-tests/): unit, functional, and Juju integration tests +- [Publishing from the monorepo](https://canonical.com/juju/docs/charmlibs/explanation/charmlibs-publishing/): semantic versioning, dev versions, trusted publishing +- Customizing your [functional](https://canonical.com/juju/docs/charmlibs/how-to/customize-functional-tests/) and [integration](https://canonical.com/juju/docs/charmlibs/how-to/customize-integration-tests/) tests. ## Writing charm libraries -[Distribute charm libraries](https://documentation.ubuntu.com/charmlibs/how-to/python-package/) walks through the distribution options for a new library — git dependencies, PyPI, and this monorepo — and how to choose between them. +[Distribute charm libraries](https://canonical.com/juju/docs/charmlibs/how-to/python-package/) walks through the distribution options for a new library — git dependencies, PyPI, and this monorepo — and how to choose between them. For interface libraries specifically: -- [Design relation interfaces](https://documentation.ubuntu.com/charmlibs/how-to/design-relation-interfaces/) — rules and patterns for backwards-compatible relation data formats -- [Provide relation data for charm tests](https://documentation.ubuntu.com/charmlibs/how-to/provide-relation-data-for-charm-tests/) — how to write the testing subpackage for your interface library +- [Design relation interfaces](https://canonical.com/juju/docs/charmlibs/how-to/design-relation-interfaces/) — rules and patterns for backwards-compatible relation data formats +- [Provide relation data for charm tests](https://canonical.com/juju/docs/charmlibs/how-to/provide-relation-data-for-charm-tests/) — how to write the testing subpackage for your interface library ## Using libraries in a charm Browse the library listings to find what you need: -- [General library listing](https://documentation.ubuntu.com/charmlibs/reference/general-libs/) -- [Interface library listing](https://documentation.ubuntu.com/charmlibs/reference/interface-libs/) +- [General library listing](https://canonical.com/juju/docs/charmlibs/reference/general-libs/) +- [Interface library listing](https://canonical.com/juju/docs/charmlibs/reference/interface-libs/) -The [Manage charm libraries](https://documentation.ubuntu.com/charmlibs/how-to/manage-libraries/) guide covers adding a library to your charm, version constraints, git dependencies, and using legacy Charmhub-hosted libraries. +The [Manage charm libraries](https://canonical.com/juju/docs/charmlibs/how-to/manage-libraries/) guide covers adding a library to your charm, version constraints, git dependencies, and using legacy Charmhub-hosted libraries. We also host the library reference docs: -- [charmlibs package reference](https://documentation.ubuntu.com/charmlibs/reference/charmlibs/) — `apt`, `pathops`, `snap`, and more -- [charmlibs.interfaces package reference](https://documentation.ubuntu.com/charmlibs/reference/charmlibs-interfaces/) — `tls_certificates`, `tracing`, and more -- [Interface specifications](https://documentation.ubuntu.com/charmlibs/reference/interfaces/) — relation data schemas and docs for each interface +- [charmlibs package reference](https://canonical.com/juju/docs/charmlibs/reference/charmlibs/) — `apt`, `pathops`, `snap`, and more +- [charmlibs.interfaces package reference](https://canonical.com/juju/docs/charmlibs/reference/charmlibs-interfaces/) — `tls_certificates`, `tracing`, and more +- [Interface specifications](https://canonical.com/juju/docs/charmlibs/reference/interfaces/) — relation data schemas and docs for each interface -Read more in the docs: [Charm libraries explained](https://documentation.ubuntu.com/charmlibs/explanation/charm-libs/) +Read more in the docs: [Charm libraries explained](https://canonical.com/juju/docs/charmlibs/explanation/charm-libs/) --- -New to charming? The [ops documentation](https://documentation.ubuntu.com/ops/) is the best place to start. +New to charming? The [ops documentation](https://canonical.com/juju/docs/ops/) is the best place to start. diff --git a/apt/pyproject.toml b/apt/pyproject.toml index 290a90547..8bedbcd05 100644 --- a/apt/pyproject.toml +++ b/apt/pyproject.toml @@ -25,7 +25,7 @@ functional = [] integration = [] [project.urls] -"Documentation" = "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/apt/" +"Documentation" = "https://canonical.com/juju/docs/charmlibs/reference/charmlibs/apt/" "Repository" = "https://github.com/canonical/charmlibs/tree/main/apt" "Issues" = "https://github.com/canonical/charmlibs/issues" "Changelog" = "https://github.com/canonical/charmlibs/blob/main/apt/CHANGELOG.md" diff --git a/interfaces/.example/README.md b/interfaces/.example/README.md index 83d999cf8..a27620857 100644 --- a/interfaces/.example/README.md +++ b/interfaces/.example/README.md @@ -8,4 +8,4 @@ To install, add `charmlibs-interfaces-example-interface` to your Python dependen from charmlibs.interfaces import example_interface ``` -See the [reference documentation](https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/example-interface) for more. +See the [reference documentation](https://canonical.com/juju/docs/charmlibs/reference/charmlibs/interfaces/example-interface) for more. diff --git a/interfaces/.example/pyproject.toml b/interfaces/.example/pyproject.toml index 7edbd1224..f933f8b3d 100644 --- a/interfaces/.example/pyproject.toml +++ b/interfaces/.example/pyproject.toml @@ -43,7 +43,7 @@ integration = [ # installed for `just integration interfaces/example-interface` ] [project.urls] -"Documentation" = "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/example-interface" +"Documentation" = "https://canonical.com/juju/docs/charmlibs/reference/charmlibs/interfaces/example-interface" "Repository" = "https://github.com/canonical/charmlibs/tree/main/interfaces/example-interface" "Issues" = "https://github.com/canonical/charmlibs/issues" "Changelog" = "https://github.com/canonical/charmlibs/blob/main/interfaces/example-interface/CHANGELOG.md" diff --git a/interfaces/.example/testing/README.md b/interfaces/.example/testing/README.md index 5e171b8a5..378b3a492 100644 --- a/interfaces/.example/testing/README.md +++ b/interfaces/.example/testing/README.md @@ -8,4 +8,4 @@ To install, add `charmlibs-interfaces-example-interface-testing` to your Python from charmlibs.interfaces import example_interface_testing ``` -See the [library reference documentation](https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/example_interface) for more. +See the [library reference documentation](https://canonical.com/juju/docs/charmlibs/reference/charmlibs/interfaces/example_interface) for more. diff --git a/interfaces/.package/README.md b/interfaces/.package/README.md index a47bd79ee..5fa6ea29e 100644 --- a/interfaces/.package/README.md +++ b/interfaces/.package/README.md @@ -2,9 +2,9 @@ This package should not be installed - it exists solely to reserve the `charmlibs.interfaces` namespace. -This namespace is for packages designed to be used by [Juju charms](https://canonical.com/juju/charms-architecture) to implement [relations](https://documentation.ubuntu.com/ops/latest/howto/manage-relations/), known as charm interface libraries. Read the [charmlibs documentation](https://documentation.ubuntu.com/charmlibs) for more information about charm libraries. +This namespace is for packages designed to be used by [Juju charms](https://canonical.com/juju/charms-architecture) to implement [relations](https://canonical.com/juju/docs/ops/latest/howto/manage-relations/), known as charm interface libraries. Read the [charmlibs documentation](https://canonical.com/juju/docs/charmlibs) for more information about charm libraries. Read more: -- Get started with charm development, using [Charmcraft tutorials](https://documentation.ubuntu.com/charmcraft/stable/tutorial/) and [Ops tutorials](https://documentation.ubuntu.com/ops/latest/tutorial/) -- Learn how to [use and write charm libraries](https://documentation.ubuntu.com/ops/latest/howto/manage-libraries/) +- Get started with charm development, using [Charmcraft tutorials](https://documentation.ubuntu.com/charmcraft/stable/tutorial/) and [Ops tutorials](https://canonical.com/juju/docs/ops/latest/tutorial/) +- Learn how to [use and write charm libraries](https://canonical.com/juju/docs/ops/latest/howto/manage-libraries/) diff --git a/interfaces/.package/pyproject.toml b/interfaces/.package/pyproject.toml index 34ddf024a..65244d328 100644 --- a/interfaces/.package/pyproject.toml +++ b/interfaces/.package/pyproject.toml @@ -22,7 +22,7 @@ functional = [] integration = [] [project.urls] -"Documentation" = "https://documentation.ubuntu.com/charmlibs" +"Documentation" = "https://canonical.com/juju/docs/charmlibs" "Repository" = "https://github.com/canonical/charmlibs" "Issues" = "https://github.com/canonical/charmlibs/issues" diff --git a/interfaces/README.md b/interfaces/README.md index f9a0ec4d4..48eb1a445 100644 --- a/interfaces/README.md +++ b/interfaces/README.md @@ -1,3 +1,3 @@ # charmlibs-interfaces -This subdirectory hosts the source code for Canonical's charm interface libraries. Charm interface libraries are hosted on PyPI at `charmlibs-interfaces-` and are importable as `charmlibs.interfaces.`. For more information, read the [docs](https://documentation.ubuntu.com/charmlibs). +This subdirectory hosts the source code for Canonical's charm interface libraries. Charm interface libraries are hosted on PyPI at `charmlibs-interfaces-` and are importable as `charmlibs.interfaces.`. For more information, read the [docs](https://canonical.com/juju/docs/charmlibs). diff --git a/interfaces/index.json b/interfaces/index.json index 9c9a7d395..488000e96 100644 --- a/interfaces/index.json +++ b/interfaces/index.json @@ -5,7 +5,7 @@ "lib": "charms.oathkeeper.auth_proxy", "lib_url": "https://charmhub.io/oathkeeper/libraries/auth_proxy", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/auth_proxy/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/auth_proxy/", "summary": "Configure an Identity and Access Proxy.", "description": "The `auth_proxy` interface allows charms to configure an Identity and Access Proxy.\nThe requirer supplies the configuration, such as protected URLs, allowed endpoints, and headers.\nThe provider is responsible for transforming the configuration into access rules and forwarding relevant configuration to the proxy.", "tags": [ @@ -19,7 +19,7 @@ "lib": "charms.data_platform_libs.data_interfaces", "lib_url": "https://charmhub.io/data-platform-libs/libraries/data_interfaces", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/azure_service_principal/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/azure_service_principal/", "summary": "Interact with Microsoft Entra ID using service principal credentials.", "description": "The `azure_service_principal` interface allows charms to receive the service principal credential needed to access Entra ID objects such as Azure storage.\nRequirer charms can specify which fields should be transferred as Juju secrets, and the provider charm will deliver the credentials as requested.", "tags": [ @@ -34,7 +34,7 @@ "lib": "", "lib_url": "", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/azure_storage/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/azure_storage/", "summary": "Access Azure Blob Storage and Data Lake Storage (Gen 2).", "description": "The `azure_storage` interface allows charms to access Azure Blob Storage and Data Lake Storage (Gen 2) with appropriate credentials and connection details.\nThe provider charm supplies storage account credentials and configuration, while the requirer charm specifies the container to access.", "tags": [], @@ -46,7 +46,7 @@ "lib": "charms.certificate_transfer_interface.certificate_transfer", "lib_url": "https://charmhub.io/certificate-transfer-interface/libraries/certificate_transfer", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/certificate_transfer/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/certificate_transfer/", "summary": "Receive public or CA certificates.", "description": "The `certificate_transfer` interface allows charms to receive public and/or CA certificates.\nThe provider charm shares its certificates with the requirer.", "tags": [ @@ -60,7 +60,7 @@ "lib": "charms.cloudflare_configurator.cloudflared_route", "lib_url": "https://charmhub.io/cloudflare-configurator/libraries/cloudflared_route", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/cloudflared_route/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/cloudflared_route/", "summary": "Configure a cloudflared tunnel.", "description": "The `cloudflared_route` interface allows information to be exchanged between the cloudflared tunnel configurator charm and the cloudflared tunnel charm.", "tags": [ @@ -75,7 +75,7 @@ "lib": "charms.data_platform_libs.data_interfaces", "lib_url": "https://charmhub.io/data-platform-libs/libraries/data_interfaces", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/connect_client/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/connect_client/", "summary": "Interact with Kafka Connect clusters.", "description": "The `connect_client` interface allows charms to interact with Kafka Connect clusters for connector plugin and task management.\nThe provider charm manages the Kafka Connect cluster and deploys connector plugins from URLs supplied by the requirer, while the requirer charm manages its connectors and tasks using the provider's REST endpoints.", "tags": [ @@ -90,7 +90,7 @@ "lib": "charms.grafana_agent.cos_agent", "lib_url": "https://charmhub.io/grafana-agent/libraries/cos_agent", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/cos_agent/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/cos_agent/", "summary": "Machine-charm-specific interface for managing multiple observability-related data streams.", "description": "The `cos_agent` interface allows machine charms to send telemetry \u2014 such as metrics, logs, dashboards, and alert rules \u2014 to Opentelemetry Colector or Grafana Agent charms.\n\nThis interface is designed specifically for machine charms, where the requirer is typically the [grafana-agent](https://charmhub.io/grafana-agent) and the [opentelemetry-collector](https://github.com/canonical/opentelemetry-collector-operator/) subordinate charms.", "tags": [ @@ -104,7 +104,7 @@ "lib": "", "lib_url": "", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/database_backup/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/database_backup/", "summary": "Manage database backup and restore operations.", "description": "The `database_backup` interface allows a database backup manager charm to coordinate backup and restore operations with database charms.\nThe provider charm (database) executes backup and restore jobs based on requirer requests and reports their status, while the requirer charm (backup manager) requests jobs, manages storage configuration, and stores backup artifacts.", "tags": [], @@ -116,7 +116,7 @@ "lib": "", "lib_url": "", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/dns_record/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/dns_record/", "summary": "Create and manage DNS records.", "description": "", "tags": [], @@ -128,7 +128,7 @@ "lib": "charms.data_platform_libs.data_interfaces", "lib_url": "https://charmhub.io/data-platform-libs/libraries/data_interfaces", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/etcd_client/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/etcd_client/", "summary": "Access etcd clusters with credentials and TLS configuration.", "description": "The `etcd_client` interface allows charms to access etcd clusters with user-specific credentials and TLS configuration.\nThe provider charm creates etcd users and roles based on requirer requests, while the requirer charm shares certificate information and key access prefixes.", "tags": [ @@ -143,7 +143,7 @@ "lib": "charms.filesystem_client.filesystem_info", "lib_url": "https://charmhub.io/filesystem-client/libraries/filesystem_info", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/filesystem_info/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/filesystem_info/", "summary": "Export mount information about shared filesystems.", "description": "The `filesystem_info` interface allows charms that export shared filesystems to expose the required mount information.\nThe provider charm exports mount information for the requirer charm.", "tags": [ @@ -157,7 +157,7 @@ "lib": "charms.sdcore_nms_k8s.fiveg_core_gnb", "lib_url": "https://charmhub.io/sdcore-nms-k8s/libraries/fiveg_core_gnb", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/fiveg_core_gnb/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/fiveg_core_gnb/", "summary": "", "description": "", "tags": [ @@ -171,7 +171,7 @@ "lib": "", "lib_url": "", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/fiveg_f1/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/fiveg_f1/", "summary": "", "description": "", "tags": [], @@ -183,7 +183,7 @@ "lib": "", "lib_url": "", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/fiveg_gnb_identity/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/fiveg_gnb_identity/", "summary": "", "description": "", "tags": [], @@ -195,7 +195,7 @@ "lib": "charms.sdcore_amf_k8s.fiveg_n2", "lib_url": "https://charmhub.io/sdcore-amf-k8s/libraries/fiveg_n2", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/fiveg_n2/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/fiveg_n2/", "summary": "", "description": "", "tags": [ @@ -209,7 +209,7 @@ "lib": "", "lib_url": "", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/fiveg_n3/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/fiveg_n3/", "summary": "", "description": "", "tags": [], @@ -221,7 +221,7 @@ "lib": "charms.sdcore_upf_k8s.fiveg_n4", "lib_url": "https://charmhub.io/sdcore-upf-k8s/libraries/fiveg_n4", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/fiveg_n4/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/fiveg_n4/", "summary": "", "description": "", "tags": [ @@ -235,7 +235,7 @@ "lib": "charms.sdcore_nrf_k8s.fiveg_nrf", "lib_url": "https://charmhub.io/sdcore-nrf-k8s/libraries/fiveg_nrf", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/fiveg_nrf/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/fiveg_nrf/", "summary": "", "description": "", "tags": [ @@ -249,7 +249,7 @@ "lib": "", "lib_url": "", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/fiveg_rfsim/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/fiveg_rfsim/", "summary": "", "description": "", "tags": [], @@ -261,7 +261,7 @@ "lib": "charms.oathkeeper.forward_auth", "lib_url": "https://charmhub.io/oathkeeper/libraries/forward_auth", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/forward_auth/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/forward_auth/", "summary": "", "description": "", "tags": [ @@ -275,7 +275,7 @@ "lib": "charms.grafana_k8s.grafana_auth", "lib_url": "https://charmhub.io/grafana-k8s/libraries/grafana_auth", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/grafana_auth/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/grafana_auth/", "summary": "Provide authentication configuration for Grafana.", "description": "The `grafana_auth` interface supports configuring Grafana authentication.\nThe provider sends the authentication mode and its configuration.\nThe requirer configures authentication with Grafana and replies with its publicly reachable URL.", "tags": [ @@ -290,7 +290,7 @@ "lib": "charms.grafana_k8s.grafana_source", "lib_url": "https://charmhub.io/grafana-k8s/libraries/grafana_source", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/grafana_datasource/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/grafana_datasource/", "summary": "Provide Grafana data source servers.", "description": "The `grafana_source` interface supports exposing servers implementing the Grafana source HTTP API.\nThe provider publishes the endpoint URL for the servers, one per unit.\nThe requirer replies with unique IDs for the Grafana application and a mapping of unique IDs for the units.", "tags": [ @@ -304,7 +304,7 @@ "lib": "cosl", "lib_url": "https://pypi.org/project/cosl/", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/grafana_datasource_exchange/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/grafana_datasource_exchange/", "summary": "Share UIDs for Grafana datasources between charms.", "description": "The `grafana_datasource_exchange` interface allows charms that generate telemetry and have a reference to the datasources where said telemetry is queriable, to share those references to other charms for correlation and cross-referencing purposes.\n\nThis is a symmetrical relation, where implementers broadcast the same information regardless of whether they are the provider or requirer.\nTo avoid constraints on integration topology, charms should implement both a requirer and provider endpoint for this interface.", "tags": [ @@ -319,7 +319,7 @@ "lib": "charms.hydra.hydra_endpoints", "lib_url": "https://charmhub.io/hydra/libraries/hydra_endpoints", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/hydra_endpoints/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/hydra_endpoints/", "summary": "", "description": "", "tags": [ @@ -333,7 +333,7 @@ "lib": "charms.traefik_k8s.ingress", "lib_url": "https://charmhub.io/traefik-k8s/libraries/ingress", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/ingress/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/ingress/", "summary": "", "description": "", "tags": [ @@ -348,7 +348,7 @@ "lib": "charms.traefik_k8s.ingress_per_unit", "lib_url": "https://charmhub.io/traefik-k8s/libraries/ingress_per_unit", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/ingress_per_unit/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/ingress_per_unit/", "summary": "", "description": "", "tags": [ @@ -363,7 +363,7 @@ "lib": "", "lib_url": "", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/ip_router/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/ip_router/", "summary": "", "description": "", "tags": [], @@ -375,7 +375,7 @@ "lib": "charmlibs.interfaces.istio_metadata", "lib_url": "https://pypi.org/project/charmlibs-interfaces-istio-metadata", "lib_docs_url": "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio-metadata", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/istio_metadata/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/istio_metadata/", "summary": "Share Istio installation metadata between charms.", "description": "The `istio_metadata` interface allows an Istio control plane charm to share information about its installation (such as the root namespace) with other charms that need to interoperate with Istio.\nThe provider charm (the Istio control plane) publishes the metadata, and requirer charms read it to configure their own integration with Istio.", "tags": [ @@ -389,7 +389,7 @@ "lib": "", "lib_url": "", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/jwt/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/jwt/", "summary": "Provide and consume JSON Web Token configuration.", "description": "The `jwt` interface allows charms to provide or require JSON Web Token configuration for secure token validation.\nThe provider charm supplies signing keys and optional validation parameters, while the requirer charm applies these settings to validate incoming JWTs in its workload.", "tags": [], @@ -401,7 +401,7 @@ "lib": "", "lib_url": "", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/k8s-service/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/k8s-service/", "summary": "", "description": "", "tags": [], @@ -413,7 +413,7 @@ "lib": "charmlibs.interfaces.k8s_backup_target", "lib_url": "", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/k8s_backup_target/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/k8s_backup_target/", "summary": "Configure Kubernetes backup target specifications.", "description": "The `k8s_backup_target` interface allows client charms to specify Kubernetes backup requirements to a backup integrator charm.\nThe provider charm (client) provides backup specifications including namespaces, resource types, and retention policies, while the requirer charm (backup integrator) receives these specifications and forwards them to a backup operator.", "tags": [], @@ -425,7 +425,7 @@ "lib": "charms.data_platform_libs.data_interfaces", "lib_url": "https://charmhub.io/data-platform-libs/libraries/data_interfaces", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/kafka_client/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/kafka_client/", "summary": "Interact with Kafka clusters as a client.", "description": "The `kafka_client` interface allows charms to interact with Kafka clusters as producers, consumers, or admin clients.\nThe provider charm creates Kafka users with appropriate ACLs and permissions based on requested roles, while the requirer charm specifies its desired topic and role.", "tags": [ @@ -440,7 +440,7 @@ "lib": "", "lib_url": "", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/karapace_client/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/karapace_client/", "summary": "Access Karapace schema registry services.", "description": "The `karapace_client` interface allows charms to access Karapace schema registry services with user-specific credentials.\nThe provider charm manages Karapace users and subject access controls, while the requirer charm requests access to specific subjects with desired role permissions.", "tags": [], @@ -452,7 +452,7 @@ "lib": "", "lib_url": "", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/kratos_external_idp/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/kratos_external_idp/", "summary": "", "description": "", "tags": [], @@ -464,7 +464,7 @@ "lib": "charms.kratos.kratos_info", "lib_url": "https://charmhub.io/kratos/libraries/kratos_info", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/kratos_info/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/kratos_info/", "summary": "", "description": "", "tags": [ @@ -478,7 +478,7 @@ "lib": "", "lib_url": "", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/kubeflow_dashboard_links/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/kubeflow_dashboard_links/", "summary": "", "description": "", "tags": [], @@ -490,7 +490,7 @@ "lib": "charms.glauth_k8s.ldap", "lib_url": "https://charmhub.io/glauth-k8s/libraries/ldap", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/ldap/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/ldap/", "summary": "Configure and connect to a Lightweight Directory Access Protocol (LDAP) server.", "description": "The `ldap` interface supports sharing configuration and connection information for a Lightweight Directory Access Protocol (LDAP) server.\nThe provider is responsible for sharing the LDAP URL and other information needed for the requirer to connect and perform LDAP operations.", "tags": [ @@ -504,7 +504,7 @@ "lib": "litmus-libs", "lib_url": "https://pypi.org/project/litmus-libs/", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/litmus_auth/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/litmus_auth/", "summary": "Exchange gRPC server endpoints for communication between Litmus auth and backend charms.", "description": "The `litmus_auth` interface enables communication between Litmus auth and Litmus backend charms.\nBoth the provider and requirer publish their respective gPRC server's endpoint.", "tags": [ @@ -519,7 +519,7 @@ "lib": "charms.identity_platform_login_ui_operator.login_ui_endpoints", "lib_url": "https://charmhub.io/identity-platform-login-ui-operator/libraries/login_ui_endpoints", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/login_ui_endpoints/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/login_ui_endpoints/", "summary": "", "description": "", "tags": [ @@ -533,7 +533,7 @@ "lib": "", "lib_url": "", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/milter/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/milter/", "summary": "Provide or consume a mail filter.", "description": "The `milter` interface allows charms to connect to a charm serving as a mail filter (milter).\nThe provider charm shares the information needed to connect.", "tags": [], @@ -545,7 +545,7 @@ "lib": "", "lib_url": "", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/mimir_cluster/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/mimir_cluster/", "summary": "Mimir coordinator and worker charm communication.", "description": "The `mimir_cluster` interface supports communication between the [mimir-coordinator-k8s](https://charmhub.io/mimir-coordinator-k8s) and [mimir-worker-k8s](https://charmhub.io/mimir-worker-k8s) charms.", "tags": [], @@ -557,7 +557,7 @@ "lib": "charms.data_platform_libs.data_interfaces", "lib_url": "https://charmhub.io/data-platform-libs/libraries/data_interfaces", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/mongodb_client/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/mongodb_client/", "summary": "Access MongoDB databases with unique credentials.", "description": "The `mongodb_client` interface allows charms to access MongoDB databases with unique, relation-specific credentials transmitted via Juju Secrets.\nThe provider charm creates database users and provides connection details, while the requirer charm requests a database name and optional custom entities with specific role permissions.", "tags": [ @@ -572,7 +572,7 @@ "lib": "charms.data_platform_libs.data_interfaces", "lib_url": "https://charmhub.io/data-platform-libs/libraries/data_interfaces", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/mysql_client/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/mysql_client/", "summary": "Access MySQL databases with unique credentials.", "description": "The `mysql_client` interface allows charms to access MySQL databases with unique, relation-specific credentials transmitted via Juju Secrets.\nThe provider charm creates database users and provides connection details, while the requirer charm requests a database name and optional custom entities with specific role permissions.", "tags": [ @@ -587,7 +587,7 @@ "lib": "charms.nginx_ingress_integrator.nginx_route", "lib_url": "https://charmhub.io/nginx-ingress-integrator/libraries/nginx_route", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/nginx-route/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/nginx-route/", "summary": "Create an Nginx ingress resource in a Kubernetes cluster.", "description": "The `nginx_route` interface allows charms to request the creation of an Nginx ingress resource in the Kubernetes cluster.\nThe requirer charm supplies the configuration for the ingress resources.\nThe provider charm creates the resource as requested.", "tags": [ @@ -602,7 +602,7 @@ "lib": "charms.hydra.oauth", "lib_url": "https://charmhub.io/hydra/libraries/oauth", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/oauth/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/oauth/", "summary": "Connect to an OAuth2/OIDC Provider.", "description": "The `oauth` interface allows charms to interface with an OAuth2/OIDC Provider.\nThe requirer acts as an OAuth2 client.\nThe provider exposes an OAuth2/OIDC Provider.", "tags": [ @@ -616,7 +616,7 @@ "lib": "charms.opencti.opencti_connector", "lib_url": "https://charmhub.io/opencti/libraries/opencti_connector", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/opencti_connector/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/opencti_connector/", "summary": "Connect a single OpenCTI provider to one or more OpenCTI connectors.", "description": "The `opencti_connector` interface supports connecting an OpenCTI provider charm to one or more OpenCTI connectors.\nThe provider charm specifies the expected connector type for each connector relation.\nEach requirer charm shares its OpenCTI URL (and token) for the provider to connect to.", "tags": [ @@ -630,7 +630,7 @@ "lib": "charms.openfga_k8s.openfga", "lib_url": "https://charmhub.io/openfga-k8s/libraries/openfga", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/openfga/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/openfga/", "summary": "Set up and connect to an OpenFGA store.", "description": "The `openfga` interface supports setting up an OpenFGA store and connecting to it.\nThe requirer specifies the store name, while the provider creates the store and shares the data needed to connect.", "tags": [ @@ -644,7 +644,7 @@ "lib": "charms.data_platform_libs.data_interfaces", "lib_url": "https://charmhub.io/data-platform-libs/libraries/data_interfaces", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/opensearch_client/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/opensearch_client/", "summary": "Access OpenSearch and ElasticSearch clusters.", "description": "The `opensearch_client` interface allows charms to access OpenSearch and ElasticSearch clusters with user-specific credentials and index permissions transmitted via Juju Secrets.\nThe provider charm creates users and manages index access controls, while the requirer charm requests access to specific indices with optional role permissions.", "tags": [ @@ -659,7 +659,7 @@ "lib": "charms.data_platform_libs.data_interfaces", "lib_url": "https://charmhub.io/data-platform-libs/libraries/data_interfaces", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/postgresql_client/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/postgresql_client/", "summary": "Access PostgreSQL databases with unique credentials.", "description": "The `postgresql_client` interface allows charms to access PostgreSQL databases with unique, relation-specific credentials.\nThe provider charm creates database users and provides connection details, while the requirer charm requests a database name and optional custom entities with specific role permissions. Supports subordinate charms that provide external connectivity.", "tags": [ @@ -674,7 +674,7 @@ "lib": "", "lib_url": "", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/profiling/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/profiling/", "summary": "", "description": "", "tags": [], @@ -686,7 +686,7 @@ "lib": "charms.prometheus_k8s.prometheus_remote_write", "lib_url": "https://charmhub.io/prometheus-k8s/libraries/prometheus_remote_write", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/prometheus_remote_write/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/prometheus_remote_write/", "summary": "Write Prometheus metrics to remote endpoint.", "description": "The `prometheus_remote_write` endpoint supports writing Prometheus metrics to a remote endpoint.\nThe provider publishes one or more endpoints, and emits alerts per rule files.\nThe requirer pushes metrics to the provided endpoints, and sends rule files to the provider.", "tags": [ @@ -700,7 +700,7 @@ "lib": "charms.prometheus_k8s.prometheus_scrape", "lib_url": "https://charmhub.io/prometheus-k8s/libraries/prometheus_scrape", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/prometheus_scrape/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/prometheus_scrape/", "summary": "Configure Prometheus scraping jobs.", "description": "The `prometheus_scrape` interface supports configuring Prometheus scrape jobs and recording and alerting rules.\nThe provider specifies one or more scrape compatible metrics endpoints, and rule files.\nThe requirer then scrapes the provider's metrics and emits alerts per rule files.", "tags": [ @@ -714,7 +714,7 @@ "lib": "", "lib_url": "", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/pyroscope_cluster/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/pyroscope_cluster/", "summary": "Pyroscope coordinator and worker charm communication.", "description": "The `pyroscope_cluster` interface supports communication between the [pyroscope-coordinator-k8s](https://charmhub.io/pyroscope-coordinator-k8s) and [pyroscope-worker-k8s](https://charmhub.io/pyroscope-worker-k8s) charms.", "tags": [], @@ -726,7 +726,7 @@ "lib": "", "lib_url": "", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/s3/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/s3/", "summary": "", "description": "", "tags": [], @@ -738,7 +738,7 @@ "lib": "charms.saml_integrator.saml", "lib_url": "https://charmhub.io/saml-integrator/libraries/saml", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/saml/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/saml/", "summary": "Authenticate users with SAML.", "description": "The `saml` interface allows charms to connect to a SAML Identity Provider for authenticating users.\nThe provider charm is the SAML Identity Provider, and shares the necessary data with requirer charms (SAML Service Providers).", "tags": [ @@ -752,7 +752,7 @@ "lib": "charms.sdcore_nms_k8s.sdcore_config", "lib_url": "https://charmhub.io/sdcore-nms-k8s/libraries/sdcore_config", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/sdcore_config/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/sdcore_config/", "summary": "", "description": "", "tags": [ @@ -766,7 +766,7 @@ "lib": "charms.sdcore_webui_k8s.sdcore_management", "lib_url": "https://charmhub.io/sdcore-webui-k8s/libraries/sdcore_management", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/sdcore_management/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/sdcore_management/", "summary": "", "description": "", "tags": [ @@ -780,7 +780,7 @@ "lib": "charms.smtp_integrator.smtp", "lib_url": "https://charmhub.io/smtp-integrator/libraries/smtp", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/smtp/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/smtp/", "summary": "Connect to a SMTP server.", "description": "The `smtp` interface allows charms to connect to a SMTP server.\nThe provider charm provides the SMTP details so that the requirer can connect.", "tags": [ @@ -794,7 +794,7 @@ "lib": "", "lib_url": "", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/spark_service_account/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/spark_service_account/", "summary": "", "description": "", "tags": [], @@ -806,7 +806,7 @@ "lib": "", "lib_url": "", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/tempo_cluster/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/tempo_cluster/", "summary": "Tempo coordinator and worker charm communication.", "description": "The `tempo_cluster` interface supports communication between the [tempo-coordinator-k8s](https://charmhub.io/tempo-coordinator-k8s) and [tempo-worker-k8s](https://charmhub.io/tempo-worker-k8s) charms.", "tags": [], @@ -818,7 +818,7 @@ "lib": "charmlibs.interfaces.tls_certificates", "lib_url": "https://pypi.org/project/charmlibs-interfaces-tls-certificates", "lib_docs_url": "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/tls-certificates", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/tls-certificates/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/tls-certificates/", "summary": "Securely request TLS certificates.", "description": "The `tls-certificates` interface allows charms to securely request and receive TLS certificates. The requirer charm is responsible for its private key and defining its certificate signing requests (CSRs). The provider charm delivers the certificate for each request, including the CA chain and CA certificate.", "tags": [ @@ -832,7 +832,7 @@ "lib": "charms.tempo_coordinator_k8s.tracing", "lib_url": "https://charmhub.io/tempo-coordinator-k8s/libraries/tracing", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/tracing/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/tracing/", "summary": "Request endpoints for pushing trace data.", "description": "The `tracing` interface supports push-based tracing.\nThe requirer publishes the list of protocols it wants to use to send traces.\nThe provider replies with an API URL and an endpoint for each protocol, omitting those it does not support.", "tags": [ @@ -846,7 +846,7 @@ "lib": "dpcharmlibs.interfaces", "lib_url": "https://pypi.org/project/dpcharmlibs-interfaces", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/valkey_client/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/valkey_client/", "summary": "Access Valkey databases with credentials and TLS configuration.", "description": "The `valkey_client` interface allows charms to access Valkey databases with user-specific credentials and TLS configuration.\nThe provider charm creates Valkey users and permissions based on requirer requests, while the requirer charm shares the desired key access prefix.", "tags": [ @@ -860,7 +860,7 @@ "lib": "", "lib_url": "", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/vault-autounseal/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/vault-autounseal/", "summary": "", "description": "", "tags": [], @@ -872,7 +872,7 @@ "lib": "charms.vault_k8s.vault_kv", "lib_url": "https://charmhub.io/vault-k8s/libraries/vault_kv", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/vault-kv/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/vault-kv/", "summary": "Securely share Vault credentials to interact with the Vault Key-Value (KV) backend.", "description": "The `vault-kv` interface allows the charms to securely request and receive the credentials needed to interact with the Key-Value (KV) backend of Vault.\nThe requirer charm requests credentials, and the provider charm supplies them.", "tags": [ @@ -886,7 +886,7 @@ "lib": "", "lib_url": "", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/velero_backup_config/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/velero_backup_config/", "summary": "Configure Kubernetes backup requirements for Velero.", "description": "The `velero_backup_config` interface allows client charms to specify Kubernetes backup requirements to a Velero Operator charm.\nThe requirer charm (client) provides backup specifications including namespaces, resource types, and retention policies, while the provider charm (Velero Operator) receives these specifications and uses them to execute backup operations.", "tags": [], @@ -898,7 +898,7 @@ "lib": "", "lib_url": "", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/wazuh_api_client/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/wazuh_api_client/", "summary": "The `wazuh_api_client` interface allows charms to connect to a Wazuh server.\nThe provider charm manages access to the server, securely providing credentials and endpoints to the requirer charms.", "description": "Connect to a Wazuh server.", "tags": [], @@ -910,7 +910,7 @@ "lib": "", "lib_url": "", "lib_docs_url": "", - "docs_url": "https://documentation.ubuntu.com/charmlibs/reference/interfaces/zookeeper/", + "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/zookeeper/", "summary": "", "description": "", "tags": [], diff --git a/passwd/README.md b/passwd/README.md index b659013a3..d10cdfcac 100644 --- a/passwd/README.md +++ b/passwd/README.md @@ -8,4 +8,4 @@ To install, add `charmlibs-passwd` to your Python dependencies. Then in your Pyt from charmlibs import passwd ``` -See the [reference documentation](https://documentation.ubuntu.com/charmlibs/reference/charmlibs/passwd) for more. +See the [reference documentation](https://canonical.com/juju/docs/charmlibs/reference/charmlibs/passwd) for more. diff --git a/passwd/pyproject.toml b/passwd/pyproject.toml index b2f7fac81..b2946f8a6 100644 --- a/passwd/pyproject.toml +++ b/passwd/pyproject.toml @@ -32,7 +32,7 @@ integration = [ # installed for `just integration passwd` ] [project.urls] -"Documentation" = "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/passwd/" +"Documentation" = "https://canonical.com/juju/docs/charmlibs/reference/charmlibs/passwd/" "Repository" = "https://github.com/canonical/charmlibs/tree/main/passwd" "Issues" = "https://github.com/canonical/charmlibs/issues" "Changelog" = "https://github.com/canonical/charmlibs/blob/main/passwd/CHANGELOG.md" diff --git a/pathops/pyproject.toml b/pathops/pyproject.toml index d0fea5a04..7e952737a 100644 --- a/pathops/pyproject.toml +++ b/pathops/pyproject.toml @@ -29,7 +29,7 @@ integration = [ ] [project.urls] -"Documentation" = "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/pathops/" +"Documentation" = "https://canonical.com/juju/docs/charmlibs/reference/charmlibs/pathops/" "Repository" = "https://github.com/canonical/charmlibs/tree/main/pathops" "Issues" = "https://github.com/canonical/charmlibs/issues" "Changelog" = "https://github.com/canonical/charmlibs/blob/main/pathops/CHANGELOG.md" diff --git a/snap/README.md b/snap/README.md index 110e94eba..afeba781a 100644 --- a/snap/README.md +++ b/snap/README.md @@ -8,4 +8,4 @@ To install, add `charmlibs-snap` to your Python dependencies. Then in your Pytho from charmlibs import snap ``` -See the [reference documentation](https://documentation.ubuntu.com/charmlibs/reference/charmlibs/snap) for more. +See the [reference documentation](https://canonical.com/juju/docs/charmlibs/reference/charmlibs/snap) for more. diff --git a/snap/pyproject.toml b/snap/pyproject.toml index 03b709db7..db0a0af6c 100644 --- a/snap/pyproject.toml +++ b/snap/pyproject.toml @@ -27,7 +27,7 @@ functional = [] integration = [] [project.urls] -"Documentation" = "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/snap/" +"Documentation" = "https://canonical.com/juju/docs/charmlibs/reference/charmlibs/snap/" "Repository" = "https://github.com/canonical/charmlibs/tree/main/snap" "Issues" = "https://github.com/canonical/charmlibs/issues" "Changelog" = "https://github.com/canonical/charmlibs/blob/main/snap/CHANGELOG.md" diff --git a/sysctl/README.md b/sysctl/README.md index 8273df542..3e363dbe2 100644 --- a/sysctl/README.md +++ b/sysctl/README.md @@ -8,4 +8,4 @@ To install, add `charmlibs-sysctl` to your Python dependencies. Then in your Pyt from charmlibs import sysctl ``` -See the [reference documentation](https://documentation.ubuntu.com/charmlibs/reference/charmlibs/sysctl) for more. +See the [reference documentation](https://canonical.com/juju/docs/charmlibs/reference/charmlibs/sysctl) for more. diff --git a/sysctl/pyproject.toml b/sysctl/pyproject.toml index 2bbba5341..d386a95bb 100644 --- a/sysctl/pyproject.toml +++ b/sysctl/pyproject.toml @@ -32,7 +32,7 @@ integration = [ # installed for `just integration sysctl` ] [project.urls] -"Documentation" = "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/sysctl/" +"Documentation" = "https://canonical.com/juju/docs/charmlibs/reference/charmlibs/sysctl/" "Repository" = "https://github.com/canonical/charmlibs/tree/main/sysctl" "Issues" = "https://github.com/canonical/charmlibs/issues" "Changelog" = "https://github.com/canonical/charmlibs/blob/main/sysctl/CHANGELOG.md" diff --git a/systemd/README.md b/systemd/README.md index bff4e92bf..5c167ed9d 100644 --- a/systemd/README.md +++ b/systemd/README.md @@ -8,4 +8,4 @@ To install, add `charmlibs-systemd` to your Python dependencies. Then in your Py from charmlibs import systemd ``` -See the [reference documentation](https://documentation.ubuntu.com/charmlibs/reference/charmlibs/systemd) for more. +See the [reference documentation](https://canonical.com/juju/docs/charmlibs/reference/charmlibs/systemd) for more. diff --git a/systemd/pyproject.toml b/systemd/pyproject.toml index 6a1c02caa..c0e3423a2 100644 --- a/systemd/pyproject.toml +++ b/systemd/pyproject.toml @@ -25,7 +25,7 @@ functional = [] integration = [] [project.urls] -"Documentation" = "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/systemd/" +"Documentation" = "https://canonical.com/juju/docs/charmlibs/reference/charmlibs/systemd/" "Repository" = "https://github.com/canonical/charmlibs/tree/main/systemd" "Issues" = "https://github.com/canonical/charmlibs/issues" "Changelog" = "https://github.com/canonical/charmlibs/blob/main/systemd/CHANGELOG.md" From 1a850a9d36ad88c9ab8cb01bed8eb7dbc86e96da Mon Sep 17 00:00:00 2001 From: Sina P <55766091+sinapah@users.noreply.github.com> Date: Tue, 23 Jun 2026 09:22:28 -0400 Subject: [PATCH 55/65] chore: update codeowners nginx k8s (#554) Just updating the owners of `nginx_k8s` to the broad O11y team. --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 8ec238416..01e74cc9a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -29,7 +29,7 @@ # charmlibs packages (alphabetical) /apt/ @canonical/charmlibs-maintainers -/nginx_k8s/ @canonical/tracing-and-profiling +/nginx_k8s/ @canonical/observability /passwd/ @canonical/charmlibs-maintainers /pathops/ @canonical/charmlibs-maintainers /rollingops/ @canonical/data From 5caebcda03cecd00300ccb112db5216cb6e3a948 Mon Sep 17 00:00:00 2001 From: Dave Wilding Date: Wed, 24 Jun 2026 14:38:54 +0800 Subject: [PATCH 56/65] docs: update Charmlibs URLs for Service Mesh team packages (#555) We recently moved Charmlibs docs to https://canonical.com/juju/docs/charmlibs/. The previous URLs redirect through, but it's best if we use the new destination URLs. I haven't bumped the package versions - I'll leave that up to the Service Mesh team. --- interfaces/gateway_metadata/README.md | 2 +- interfaces/gateway_metadata/pyproject.toml | 2 +- interfaces/index.json | 2 +- interfaces/istio_ingress_route/README.md | 2 +- interfaces/istio_ingress_route/pyproject.toml | 2 +- interfaces/istio_metadata/README.md | 2 +- interfaces/istio_metadata/pyproject.toml | 2 +- interfaces/istio_request_auth/README.md | 2 +- interfaces/istio_request_auth/pyproject.toml | 2 +- interfaces/service_mesh/README.md | 2 +- interfaces/service_mesh/pyproject.toml | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/interfaces/gateway_metadata/README.md b/interfaces/gateway_metadata/README.md index e15ac3e97..a1e01f12b 100644 --- a/interfaces/gateway_metadata/README.md +++ b/interfaces/gateway_metadata/README.md @@ -8,4 +8,4 @@ To install, add `charmlibs-interfaces-gateway-metadata` to your Python dependenc from charmlibs.interfaces import gateway_metadata ``` -See the [reference documentation](https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/gateway-metadata) for more. +See the [reference documentation](https://canonical.com/juju/docs/charmlibs/reference/charmlibs/interfaces/gateway-metadata) for more. diff --git a/interfaces/gateway_metadata/pyproject.toml b/interfaces/gateway_metadata/pyproject.toml index a48391a43..986f8abb1 100644 --- a/interfaces/gateway_metadata/pyproject.toml +++ b/interfaces/gateway_metadata/pyproject.toml @@ -33,7 +33,7 @@ integration = [ # installed for `just integration interfaces/gateway_metadata` ] [project.urls] -"Documentation" = "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/gateway-metadata" +"Documentation" = "https://canonical.com/juju/docs/charmlibs/reference/charmlibs/interfaces/gateway-metadata" "Repository" = "https://github.com/canonical/charmlibs/tree/main/interfaces/gateway_metadata" "Issues" = "https://github.com/canonical/charmlibs/issues" "Changelog" = "https://github.com/canonical/charmlibs/blob/main/interfaces/gateway_metadata/CHANGELOG.md" diff --git a/interfaces/index.json b/interfaces/index.json index 488000e96..62e2b6571 100644 --- a/interfaces/index.json +++ b/interfaces/index.json @@ -374,7 +374,7 @@ "version": "0", "lib": "charmlibs.interfaces.istio_metadata", "lib_url": "https://pypi.org/project/charmlibs-interfaces-istio-metadata", - "lib_docs_url": "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio-metadata", + "lib_docs_url": "https://canonical.com/juju/docs/charmlibs/reference/charmlibs/interfaces/istio-metadata", "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/istio_metadata/", "summary": "Share Istio installation metadata between charms.", "description": "The `istio_metadata` interface allows an Istio control plane charm to share information about its installation (such as the root namespace) with other charms that need to interoperate with Istio.\nThe provider charm (the Istio control plane) publishes the metadata, and requirer charms read it to configure their own integration with Istio.", diff --git a/interfaces/istio_ingress_route/README.md b/interfaces/istio_ingress_route/README.md index 1d0354850..87a6d2b2c 100644 --- a/interfaces/istio_ingress_route/README.md +++ b/interfaces/istio_ingress_route/README.md @@ -8,4 +8,4 @@ To install, add `charmlibs-interfaces-istio-ingress-route` to your Python depend from charmlibs.interfaces import istio_ingress_route ``` -See the [reference documentation](https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio-ingress-route) for more. +See the [reference documentation](https://canonical.com/juju/docs/charmlibs/reference/charmlibs/interfaces/istio-ingress-route) for more. diff --git a/interfaces/istio_ingress_route/pyproject.toml b/interfaces/istio_ingress_route/pyproject.toml index ffd9ebcc1..2babc3885 100644 --- a/interfaces/istio_ingress_route/pyproject.toml +++ b/interfaces/istio_ingress_route/pyproject.toml @@ -33,7 +33,7 @@ integration = [ # installed for `just integration interfaces/istio_ingress_rout ] [project.urls] -"Documentation" = "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio-ingress-route" +"Documentation" = "https://canonical.com/juju/docs/charmlibs/reference/charmlibs/interfaces/istio-ingress-route" "Repository" = "https://github.com/canonical/charmlibs/tree/main/interfaces/istio_ingress_route" "Issues" = "https://github.com/canonical/charmlibs/issues" "Changelog" = "https://github.com/canonical/charmlibs/blob/main/interfaces/istio_ingress_route/CHANGELOG.md" diff --git a/interfaces/istio_metadata/README.md b/interfaces/istio_metadata/README.md index cc300b48a..be6c48d13 100644 --- a/interfaces/istio_metadata/README.md +++ b/interfaces/istio_metadata/README.md @@ -8,4 +8,4 @@ To install, add `charmlibs-interfaces-istio-metadata` to your Python dependencie from charmlibs.interfaces import istio_metadata ``` -See the [reference documentation](https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio_metadata) for more. +See the [reference documentation](https://canonical.com/juju/docs/charmlibs/reference/charmlibs/interfaces/istio_metadata) for more. diff --git a/interfaces/istio_metadata/pyproject.toml b/interfaces/istio_metadata/pyproject.toml index 1b11c860d..7cf6f9ca6 100644 --- a/interfaces/istio_metadata/pyproject.toml +++ b/interfaces/istio_metadata/pyproject.toml @@ -33,7 +33,7 @@ integration = [ # installed for `just integration interfaces/istio_metadata` ] [project.urls] -"Documentation" = "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio-metadata" +"Documentation" = "https://canonical.com/juju/docs/charmlibs/reference/charmlibs/interfaces/istio-metadata" "Repository" = "https://github.com/canonical/charmlibs/tree/main/interfaces/istio_metadata" "Issues" = "https://github.com/canonical/charmlibs/issues" "Changelog" = "https://github.com/canonical/charmlibs/blob/main/interfaces/istio_metadata/CHANGELOG.md" diff --git a/interfaces/istio_request_auth/README.md b/interfaces/istio_request_auth/README.md index 2b44704c4..daff981bf 100644 --- a/interfaces/istio_request_auth/README.md +++ b/interfaces/istio_request_auth/README.md @@ -8,4 +8,4 @@ To install, add `charmlibs-interfaces-istio-request-auth` to your Python depende from charmlibs.interfaces import istio_request_auth ``` -See the [reference documentation](https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio-request-auth) for more. +See the [reference documentation](https://canonical.com/juju/docs/charmlibs/reference/charmlibs/interfaces/istio-request-auth) for more. diff --git a/interfaces/istio_request_auth/pyproject.toml b/interfaces/istio_request_auth/pyproject.toml index 4d8287125..11d2804b2 100644 --- a/interfaces/istio_request_auth/pyproject.toml +++ b/interfaces/istio_request_auth/pyproject.toml @@ -33,7 +33,7 @@ integration = [ # installed for `just integration interfaces/istio_request_auth ] [project.urls] -"Documentation" = "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/istio-request-auth" +"Documentation" = "https://canonical.com/juju/docs/charmlibs/reference/charmlibs/interfaces/istio-request-auth" "Repository" = "https://github.com/canonical/charmlibs/tree/main/interfaces/istio_request_auth" "Issues" = "https://github.com/canonical/charmlibs/issues" "Changelog" = "https://github.com/canonical/charmlibs/blob/main/interfaces/istio_request_auth/CHANGELOG.md" diff --git a/interfaces/service_mesh/README.md b/interfaces/service_mesh/README.md index 293306799..e19da7389 100644 --- a/interfaces/service_mesh/README.md +++ b/interfaces/service_mesh/README.md @@ -8,4 +8,4 @@ To install, add `charmlibs-interfaces-service-mesh` to your Python dependencies. from charmlibs.interfaces import service_mesh ``` -See the [reference documentation](https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/service-mesh) for more. +See the [reference documentation](https://canonical.com/juju/docs/charmlibs/reference/charmlibs/interfaces/service-mesh) for more. diff --git a/interfaces/service_mesh/pyproject.toml b/interfaces/service_mesh/pyproject.toml index 7d3c077c6..42f0c8bd1 100644 --- a/interfaces/service_mesh/pyproject.toml +++ b/interfaces/service_mesh/pyproject.toml @@ -34,7 +34,7 @@ integration = [ # installed for `just integration interfaces/service_mesh` ] [project.urls] -"Documentation" = "https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/service-mesh" +"Documentation" = "https://canonical.com/juju/docs/charmlibs/reference/charmlibs/interfaces/service-mesh" "Repository" = "https://github.com/canonical/charmlibs/tree/main/interfaces/service_mesh" "Issues" = "https://github.com/canonical/charmlibs/issues" "Changelog" = "https://github.com/canonical/charmlibs/blob/main/interfaces/service_mesh/CHANGELOG.md" From 842505de3bb5559f45d128719aae222c2b1dde8b Mon Sep 17 00:00:00 2001 From: Dave Wilding Date: Wed, 24 Jun 2026 16:31:57 +0800 Subject: [PATCH 57/65] docs: update Charmlibs URLs for sloth (#558) We recently moved Charmlibs docs to https://canonical.com/juju/docs/charmlibs/. The previous URLs redirect through, but it's best if we use the new destination URLs. --- interfaces/sloth/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interfaces/sloth/README.md b/interfaces/sloth/README.md index c0e9dea74..4d543e3fd 100644 --- a/interfaces/sloth/README.md +++ b/interfaces/sloth/README.md @@ -60,7 +60,7 @@ class SlothCharm(ops.CharmBase): ## Documentation -For complete documentation, see the [charmlibs documentation](https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/sloth). +For complete documentation, see the [charmlibs documentation](https://canonical.com/juju/docs/charmlibs/reference/charmlibs/interfaces/sloth). ## Contributing From 058f57e76424142c55f67ec029daea3f99a6f916 Mon Sep 17 00:00:00 2001 From: James Garner Date: Thu, 25 Jun 2026 16:58:34 +1200 Subject: [PATCH 58/65] ci: update workshop definitions (#560) I initially defined `functional` actions for all the workshops defined in this repository. This seemed like a convenient way to wrap up the need for `sudo` and also the `just python=... functional` justfile variable syntax for use. With the switch to using a regular option for the Python version, like `just functional --python=...` I'm not convinced that the actions provide value. This PR drops the action definitions and updates the docs accordingly. --- .workshop/jammy.yaml | 4 ---- .workshop/noble.yaml | 4 ---- .workshop/resolute.yaml | 4 ---- AGENTS.md | 10 +++++----- CONTRIBUTING.md | 8 ++++---- 5 files changed, 9 insertions(+), 21 deletions(-) diff --git a/.workshop/jammy.yaml b/.workshop/jammy.yaml index 6987b3eb3..7d55f21f5 100644 --- a/.workshop/jammy.yaml +++ b/.workshop/jammy.yaml @@ -2,7 +2,3 @@ name: jammy base: ubuntu@22.04 sdks: - name: project-charmlibs -actions: - functional: | - # Override Python with: workshop run --env PYTHON=python3.12 jammy -- functional ... - sudo just functional --python "${PYTHON:-python3}" "$@" diff --git a/.workshop/noble.yaml b/.workshop/noble.yaml index b627b5938..c01cf6268 100644 --- a/.workshop/noble.yaml +++ b/.workshop/noble.yaml @@ -2,7 +2,3 @@ name: noble base: ubuntu@24.04 sdks: - name: project-charmlibs -actions: - functional: | - # Override Python with: workshop run --env PYTHON=python3.12 noble -- functional ... - sudo just functional --python "${PYTHON:-python3}" "$@" diff --git a/.workshop/resolute.yaml b/.workshop/resolute.yaml index 16e253196..e7e3cc378 100644 --- a/.workshop/resolute.yaml +++ b/.workshop/resolute.yaml @@ -2,7 +2,3 @@ name: resolute base: ubuntu@26.04 sdks: - name: project-charmlibs -actions: - functional: | - # Override Python with: workshop run --env PYTHON=python3.13 resolute -- functional ... - sudo just functional --python "${PYTHON:-python3}" "$@" diff --git a/AGENTS.md b/AGENTS.md index c22ea9333..7b8d9f1e1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -134,15 +134,15 @@ Read more: [types of tests in the charmlibs monorepo](https://canonical.com/juju Functional tests often require `sudo` and may be destructive to the local environment (e.g. installing or removing system packages). **Never run functional tests directly on the host machine.** Always use [Workshop](https://snapcraft.io/workshop) to run them in an isolated container: ```bash -workshop run resolute -- functional # Ubuntu 26.04 -workshop run noble -- functional # Ubuntu 24.04 -workshop run jammy -- functional # Ubuntu 22.04 +workshop exec resolute -- sudo just functional # Ubuntu 26.04 +workshop exec noble -- sudo just functional # Ubuntu 24.04 +workshop exec jammy -- sudo just functional # Ubuntu 22.04 ``` -Workshop configs are defined in `.workshop/`. The `functional` action runs `sudo just functional "$@"` inside the VM. Extra pytest flags are passed through: +Workshop configs are defined in `.workshop/`. Extra pytest flags are passed through to `just functional` (and on to pytest): ```bash -workshop run noble -- functional snap -x -k test_install +workshop exec noble -- sudo just functional snap -x -k test_install ``` ## Commit and PR conventions diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a3071ffb2..49f9db9d9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,12 +48,12 @@ just add pathops 'pydantic>=2' - `just functional ` runs functional tests, which interact with real external processes (but not Juju itself). Some functional test suites may require `sudo` access and may be destructive to the local environment (e.g. installing or removing system packages). Use [Workshop](https://snapcraft.io/workshop) to run them in an isolated container instead of running them directly on your host: ```bash - workshop run resolute -- functional # Ubuntu 26.04 - workshop run noble -- functional # Ubuntu 24.04 - workshop run jammy -- functional # Ubuntu 22.04 + workshop exec resolute -- sudo just functional # Ubuntu 26.04 + workshop exec noble -- sudo just functional # Ubuntu 24.04 + workshop exec jammy -- sudo just functional # Ubuntu 22.04 ``` - Extra pytest flags are passed through, e.g. `workshop run noble -- functional snap -x -k test_install`. Workshop configs are in `.workshop/`. + Extra pytest flags are passed through, e.g. `workshop exec noble -- sudo just functional snap -x -k test_install`. Workshop configs are in `.workshop/`. - Integration tests involve packing real test charms and deploying them on a Juju cloud. Pack first with `just pack-k8s ` or `just pack-machine `, then run `just integration-k8s ` or `just integration-machine `. From dda7f0bf8dc2ad920f9ca182438f98ad23cbdac3 Mon Sep 17 00:00:00 2001 From: Dave Wilding Date: Fri, 26 Jun 2026 08:30:35 +0800 Subject: [PATCH 59/65] docs: update Charmlibs URL for k8s_backup_target (#556) We recently moved Charmlibs docs to https://canonical.com/juju/docs/charmlibs/. The previous URLs redirect through, but it's best if we use the new destination URLs. I haven't bumped the package version - I'll leave that up to the Analytics team. --- interfaces/k8s_backup_target/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interfaces/k8s_backup_target/README.md b/interfaces/k8s_backup_target/README.md index 176cdc329..12492e1f7 100644 --- a/interfaces/k8s_backup_target/README.md +++ b/interfaces/k8s_backup_target/README.md @@ -8,4 +8,4 @@ To install, add `charmlibs-interfaces-k8s-backup-target` to your Python dependen from charmlibs.interfaces import k8s_backup_target ``` -See the [reference documentation](https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/k8s-backup-target). +See the [reference documentation](https://canonical.com/juju/docs/charmlibs/reference/charmlibs/interfaces/k8s-backup-target). From 3887f8c2e6604d12fbee7baa29dbeac7a7b6470f Mon Sep 17 00:00:00 2001 From: James Garner Date: Fri, 26 Jun 2026 12:40:36 +1200 Subject: [PATCH 60/65] docs: populate lib fields for mlops interfaces (#507) This PR adds `lib` entries to the interface metadata for some `mlops` owned interfaces that were missing them. --- interfaces/index.json | 16 ++++++++++------ .../k8s-service/interface/v0/interface.yaml | 1 + .../interface/v0/interface.yaml | 1 + 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/interfaces/index.json b/interfaces/index.json index 62e2b6571..70135e472 100644 --- a/interfaces/index.json +++ b/interfaces/index.json @@ -398,13 +398,15 @@ { "name": "k8s-service", "version": "0", - "lib": "", - "lib_url": "", + "lib": "charms.mlops_libs.k8s_service_info", + "lib_url": "https://charmhub.io/mlops-libs/libraries/k8s_service_info", "lib_docs_url": "", "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/k8s-service/", "summary": "", "description": "", - "tags": [], + "tags": [ + "networking" + ], "status": "published" }, { @@ -475,13 +477,15 @@ { "name": "kubeflow_dashboard_links", "version": "0", - "lib": "", - "lib_url": "", + "lib": "charms.kubeflow_dashboard.kubeflow_dashboard_links", + "lib_url": "https://charmhub.io/kubeflow-dashboard/libraries/kubeflow_dashboard_links", "lib_docs_url": "", "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/kubeflow_dashboard_links/", "summary": "", "description": "", - "tags": [], + "tags": [ + "operations" + ], "status": "draft" }, { diff --git a/interfaces/k8s-service/interface/v0/interface.yaml b/interfaces/k8s-service/interface/v0/interface.yaml index 18411b508..03efa142a 100644 --- a/interfaces/k8s-service/interface/v0/interface.yaml +++ b/interfaces/k8s-service/interface/v0/interface.yaml @@ -3,6 +3,7 @@ internal: true version: 0 status: published +lib: charms.mlops_libs.k8s_service_info # TODO: the kfp charms are in a multi-charm repo. Some setup will be required to configure the tester diff --git a/interfaces/kubeflow_dashboard_links/interface/v0/interface.yaml b/interfaces/kubeflow_dashboard_links/interface/v0/interface.yaml index 3b48a0e73..605d8fc88 100644 --- a/interfaces/kubeflow_dashboard_links/interface/v0/interface.yaml +++ b/interfaces/kubeflow_dashboard_links/interface/v0/interface.yaml @@ -3,6 +3,7 @@ internal: true version: 0 status: draft +lib: charms.kubeflow_dashboard.kubeflow_dashboard_links providers: - name: kubeflow-dashboard From 5f5810fcb006b33cab98f5da19b9fde24efd0356 Mon Sep 17 00:00:00 2001 From: James Garner Date: Fri, 26 Jun 2026 12:40:59 +1200 Subject: [PATCH 61/65] docs: update project URLs for OTLP lib (#529) This PR updates the project URLs for the OTLP library to reflect our current practices: - Documentation link to the package reference docs. - Deep repository link to the appropriate directory. - Changelog link to file in repository. --- interfaces/otlp/pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/interfaces/otlp/pyproject.toml b/interfaces/otlp/pyproject.toml index b422a50e0..4ad605a87 100644 --- a/interfaces/otlp/pyproject.toml +++ b/interfaces/otlp/pyproject.toml @@ -35,8 +35,10 @@ integration = [ # installed for `just integration otlp` ] [project.urls] -"Repository" = "https://github.com/canonical/charmlibs" +"Documentation" = "https://canonical.com/juju/docs/charmlibs/reference/charmlibs/interfaces/otlp" +"Repository" = "https://github.com/canonical/charmlibs/tree/main/interfaces/otlp" "Issues" = "https://github.com/canonical/charmlibs/issues" +"Changelog" = "https://github.com/canonical/charmlibs/blob/main/interfaces/otlp/CHANGELOG.md" [build-system] requires = ["hatchling"] From fee3148b0f32ddcbb54725e392bcd440d7bc7a15 Mon Sep 17 00:00:00 2001 From: James Garner Date: Fri, 26 Jun 2026 12:44:49 +1200 Subject: [PATCH 62/65] docs(interfaces/k8s_backup_target): update project URLs (#527) This PR updates the project URLs for the `k8s_backup_target` library to reflect our current practices: - Documentation link to the package reference docs. - Deep repository link to the appropriate directory. - Changelog link to file in repository. Since a release is required to have these changes show up on PyPI, and the release cadence for this library is low, I've bumped the version and added a changelog entry. The release only affects metadata, so it's a 'post' release. --- interfaces/index.json | 2 +- interfaces/k8s_backup_target/CHANGELOG.md | 4 ++++ interfaces/k8s_backup_target/pyproject.toml | 4 +++- .../src/charmlibs/interfaces/k8s_backup_target/_version.py | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/interfaces/index.json b/interfaces/index.json index 70135e472..114d53975 100644 --- a/interfaces/index.json +++ b/interfaces/index.json @@ -414,7 +414,7 @@ "version": "0", "lib": "charmlibs.interfaces.k8s_backup_target", "lib_url": "", - "lib_docs_url": "", + "lib_docs_url": "https://canonical.com/juju/docs/charmlibs/reference/charmlibs/interfaces/k8s-backup-target", "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/k8s_backup_target/", "summary": "Configure Kubernetes backup target specifications.", "description": "The `k8s_backup_target` interface allows client charms to specify Kubernetes backup requirements to a backup integrator charm.\nThe provider charm (client) provides backup specifications including namespaces, resource types, and retention policies, while the requirer charm (backup integrator) receives these specifications and forwards them to a backup operator.", diff --git a/interfaces/k8s_backup_target/CHANGELOG.md b/interfaces/k8s_backup_target/CHANGELOG.md index 33b841acf..c387bde07 100644 --- a/interfaces/k8s_backup_target/CHANGELOG.md +++ b/interfaces/k8s_backup_target/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.1.0.post1 - 16 June 2026 + +Update project URLs. + # 0.1.0.post0 - 13 March 2026 Fix docs link in README. diff --git a/interfaces/k8s_backup_target/pyproject.toml b/interfaces/k8s_backup_target/pyproject.toml index 75964abeb..2e81d1552 100644 --- a/interfaces/k8s_backup_target/pyproject.toml +++ b/interfaces/k8s_backup_target/pyproject.toml @@ -32,8 +32,10 @@ integration = [ ] [project.urls] -"Repository" = "https://github.com/canonical/charmlibs" +"Documentation" = "https://canonical.com/juju/docs/charmlibs/reference/charmlibs/interfaces/k8s-backup-target" +"Repository" = "https://github.com/canonical/charmlibs/tree/main/interfaces/k8s_backup_target" "Issues" = "https://github.com/canonical/charmlibs/issues" +"Changelog" = "https://github.com/canonical/charmlibs/blob/main/interfaces/k8s_backup_target/CHANGELOG.md" [build-system] requires = ["hatchling"] diff --git a/interfaces/k8s_backup_target/src/charmlibs/interfaces/k8s_backup_target/_version.py b/interfaces/k8s_backup_target/src/charmlibs/interfaces/k8s_backup_target/_version.py index 564632c3d..6125e4a1f 100644 --- a/interfaces/k8s_backup_target/src/charmlibs/interfaces/k8s_backup_target/_version.py +++ b/interfaces/k8s_backup_target/src/charmlibs/interfaces/k8s_backup_target/_version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = '0.1.0.post0' +__version__ = '0.1.0.post1' From aa70acbb91331185862684042eb8702039df8c06 Mon Sep 17 00:00:00 2001 From: James Garner Date: Fri, 26 Jun 2026 15:18:46 +1200 Subject: [PATCH 63/65] docs: populate lib fields for data interfaces (#508) This PR adds `lib` entries to the interface metadata for some `data` owned interfaces that were missing them. --- .docs/reference/libs.yaml | 16 ++++++++++++++-- interfaces/index.json | 10 ++++++---- interfaces/s3/interface/v1/interface.yaml | 5 +++-- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/.docs/reference/libs.yaml b/.docs/reference/libs.yaml index 2fc24c88c..67846cd1a 100644 --- a/.docs/reference/libs.yaml +++ b/.docs/reference/libs.yaml @@ -1585,7 +1585,7 @@ interfaces: tags: - data - name: charms.data_platform_libs.s3 - status: '' + status: legacy url: https://charmhub.io/data-platform-libs/libraries/s3 docs: '' src: https://github.com/canonical/data-platform-libs @@ -1593,7 +1593,7 @@ interfaces: rel_name: s3 rel_url_charmhub: https://charmhub.io/integrations/s3 rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/s3/v0 - description: Manage s3 credentials and metadata. + description: Deprecated in favour of ``object-storage-charmlib``. tags: - data - name: charms.saml_integrator.saml @@ -1894,3 +1894,15 @@ interfaces: tags: - ingress - networking +- name: object-storage-charmlib + status: recommended + url: https://pypi.org/project/object-storage-charmlib + docs: https://github.com/canonical/object-storage-integrator/blob/main/lib/README.md + src: https://github.com/canonical/object-storage-integrator/tree/main/lib + kind: PyPI + rel_name: s3 + rel_url_charmhub: https://charmhub.io/integrations/s3 + rel_url_schema: https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/s3/v1 + description: Manage S3 (and Azure Blob Storage / GCS) credentials and metadata. + tags: + - data diff --git a/interfaces/index.json b/interfaces/index.json index 114d53975..d0cca6b5f 100644 --- a/interfaces/index.json +++ b/interfaces/index.json @@ -727,13 +727,15 @@ { "name": "s3", "version": "1", - "lib": "", - "lib_url": "", - "lib_docs_url": "", + "lib": "object-storage-charmlib", + "lib_url": "https://pypi.org/project/object-storage-charmlib", + "lib_docs_url": "https://github.com/canonical/object-storage-integrator/blob/main/lib/README.md", "docs_url": "https://canonical.com/juju/docs/charmlibs/reference/interfaces/s3/", "summary": "", "description": "", - "tags": [], + "tags": [ + "data" + ], "status": "draft" }, { diff --git a/interfaces/s3/interface/v1/interface.yaml b/interfaces/s3/interface/v1/interface.yaml index 9518ada19..03d07d09d 100644 --- a/interfaces/s3/interface/v1/interface.yaml +++ b/interfaces/s3/interface/v1/interface.yaml @@ -1,15 +1,16 @@ name: s3 version: 1 status: draft +lib: object-storage-charmlib providers: - name: s3-integrator - url: https://github.com/canonical/object-storage-integrators/tree/main/s3 + url: https://github.com/canonical/object-storage-integrator/tree/main/s3 requirers: # No requirers have yet implemented S3 v1 interface, so to supress the linter error, # a dummy charm used in integration tests for s3-integrator is being added here. - name: test-charm-s3 - url: https://github.com/canonical/object-storage-integrators/tree/main/s3/tests/integration/test-charm-s3 + url: https://github.com/canonical/object-storage-integrator/tree/main/s3/tests/integration/test-charm-s3 maintainer: data-platform From f9d998fa3c84ca45e9c3c19bdc6e3d8750459bab Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Fri, 26 Jun 2026 10:48:03 -0400 Subject: [PATCH 64/65] chore --- .../otlp/src/charmlibs/interfaces/otlp/_otlp.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/interfaces/otlp/src/charmlibs/interfaces/otlp/_otlp.py b/interfaces/otlp/src/charmlibs/interfaces/otlp/_otlp.py index 6e8dfc000..7d849f24b 100644 --- a/interfaces/otlp/src/charmlibs/interfaces/otlp/_otlp.py +++ b/interfaces/otlp/src/charmlibs/interfaces/otlp/_otlp.py @@ -105,20 +105,8 @@ def _deserialize_rules(cls, rules: str | dict[str, Any] | _RulesModel) -> Any: @field_serializer('rules') def _serialize_rules(self, rules: _RulesModel) -> str: - """LZMA-compress rules to reduce content size for larger deployments. - - Keys are sorted so the serialized payload is deterministic for a given logical - ruleset. Without this, nested mappings (e.g. a Sigma rule's ``detection`` block, - which is an arbitrary dict) could serialize in a different key order between hooks, - changing the databag bytes and triggering spurious ``relation-changed`` events. - Note: this normalizes *mapping key* order only; list ordering (e.g. Sigma ``tags``) - is made deterministic at the source in ``cosl.rules.SigmaRules``. - - Pydantic performs the actual value encoding (via ``model_dump_json``); we only - re-serialize the resulting JSON with sorted keys to preserve that encoding fidelity. - """ - normalized = json.dumps(json.loads(rules.model_dump_json()), sort_keys=True) - return LZMABase64.compress(normalized) + """LZMA-compress rules to reduce content size for larger deployments.""" + return LZMABase64.compress(rules.model_dump_json()) class OtlpRequirer: From 7dbe60f00f5c7a1a7a1cd001de7b15ac1c4473bf Mon Sep 17 00:00:00 2001 From: Michael Thamm Date: Fri, 26 Jun 2026 11:28:00 -0400 Subject: [PATCH 65/65] chore --- interfaces/otlp/pyproject.toml | 2 +- interfaces/otlp/uv.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/interfaces/otlp/pyproject.toml b/interfaces/otlp/pyproject.toml index 4ad605a87..5b4f6daa7 100644 --- a/interfaces/otlp/pyproject.toml +++ b/interfaces/otlp/pyproject.toml @@ -16,7 +16,7 @@ classifiers = [ dynamic = ["version"] dependencies = [ # "ops", - "cosl @ git+https://github.com/canonical/cos-lib@feat/sigma-rules-3", + "cosl @ git+https://github.com/canonical/cos-lib@feat/sigma-rules-2", "requests", ] diff --git a/interfaces/otlp/uv.lock b/interfaces/otlp/uv.lock index 71f0912d4..bc39b796b 100644 --- a/interfaces/otlp/uv.lock +++ b/interfaces/otlp/uv.lock @@ -39,7 +39,7 @@ unit = [ [package.metadata] requires-dist = [ - { name = "cosl", git = "https://github.com/canonical/cos-lib?rev=feat%2Fsigma-rules-3" }, + { name = "cosl", git = "https://github.com/canonical/cos-lib?rev=feat%2Fsigma-rules-2" }, { name = "requests" }, ] @@ -152,8 +152,8 @@ wheels = [ [[package]] name = "cosl" -version = "1.9.1" -source = { git = "https://github.com/canonical/cos-lib?rev=feat%2Fsigma-rules-3#b9dd4e92e4c36bbc6504ee76a9605f5714343d24" } +version = "1.10.0" +source = { git = "https://github.com/canonical/cos-lib?rev=feat%2Fsigma-rules-2#36a19f89d8843e8c25e096ac20c8413fec36b757" } dependencies = [ { name = "ops" }, { name = "pydantic" },