Upgrade external libraries

Change-Id: I8e06d63b8eb895f88c86f1f9b3896ce87f22932a
Signed-off-by: Guillaume Boutry <guillaume.boutry@canonical.com>
This commit is contained in:
Guillaume Boutry 2024-07-09 10:17:01 +02:00
parent 16a65cf4e4
commit 6eb7f3b72b
No known key found for this signature in database
GPG Key ID: E95E3326872E55DE
9 changed files with 150 additions and 49 deletions

View File

@ -331,7 +331,7 @@ LIBAPI = 0
# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 37
LIBPATCH = 38
PYDEPS = ["ops>=2.0.0"]
@ -2606,6 +2606,14 @@ class DatabaseProviderData(ProviderData):
"""
self.update_relation_data(relation_id, {"version": version})
def set_subordinated(self, relation_id: int) -> None:
"""Raises the subordinated flag in the application relation databag.
Args:
relation_id: the identifier for a particular relation.
"""
self.update_relation_data(relation_id, {"subordinated": "true"})
class DatabaseProviderEventHandlers(EventHandlers):
"""Provider-side of the database relation handlers."""
@ -2842,6 +2850,21 @@ class DatabaseRequirerEventHandlers(RequirerEventHandlers):
def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
"""Event emitted when the database relation has changed."""
is_subordinate = False
remote_unit_data = None
for key in event.relation.data.keys():
if isinstance(key, Unit) and not key.name.startswith(self.charm.app.name):
remote_unit_data = event.relation.data[key]
elif isinstance(key, Application) and key.name != self.charm.app.name:
is_subordinate = event.relation.data[key].get("subordinated") == "true"
if is_subordinate:
if not remote_unit_data:
return
if remote_unit_data.get("state") != "ready":
return
# Check which data has changed to emit customs events.
diff = self._diff(event)

View File

@ -219,7 +219,7 @@ LIBAPI = 0
# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 35
LIBPATCH = 36
logger = logging.getLogger(__name__)
@ -1050,6 +1050,7 @@ class GrafanaDashboardProvider(Object):
self.framework.observe(self._charm.on.leader_elected, self._update_all_dashboards_from_dir)
self.framework.observe(self._charm.on.upgrade_charm, self._update_all_dashboards_from_dir)
self.framework.observe(self._charm.on.config_changed, self._update_all_dashboards_from_dir)
self.framework.observe(
self._charm.on[self._relation_name].relation_created,

View File

@ -6,7 +6,7 @@
This library is designed to enable developers to more simply patch the Kubernetes Service created
by Juju during the deployment of a sidecar charm. When sidecar charms are deployed, Juju creates a
service named after the application in the namespace (named after the Juju model). This service by
default contains a "placeholder" port, which is 65536/TCP.
default contains a "placeholder" port, which is 65535/TCP.
When modifying the default set of resources managed by Juju, one must consider the lifecycle of the
charm. In this case, any modifications to the default service (created during deployment), will be
@ -109,6 +109,26 @@ class SomeCharm(CharmBase):
# ...
```
Creating a new k8s lb service instead of patching the one created by juju
Service name is optional. If not provided, it defaults to {app_name}-lb.
If provided and equal to app_name, it also defaults to {app_name}-lb to prevent conflicts with the Juju default service.
```python
from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
from lightkube.models.core_v1 import ServicePort
class SomeCharm(CharmBase):
def __init__(self, *args):
# ...
port = ServicePort(int(self.config["charm-config-port"]), name=f"{self.app.name}")
self.service_patcher = KubernetesServicePatch(
self,
[port],
service_type="LoadBalancer",
service_name="application-lb"
)
# ...
```
Additionally, you may wish to use mocks in your charm's unit testing to ensure that the library
does not try to make any API calls, or open any files during testing that are unlikely to be
present, and could break your tests. The easiest way to do this is during your test `setUp`:
@ -146,7 +166,7 @@ LIBAPI = 1
# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 9
LIBPATCH = 11
ServiceType = Literal["ClusterIP", "LoadBalancer"]
@ -186,10 +206,15 @@ class KubernetesServicePatch(Object):
"""
super().__init__(charm, "kubernetes-service-patch")
self.charm = charm
self.service_name = service_name if service_name else self._app
self.service_name = service_name or self._app
# To avoid conflicts with the default Juju service, append "-lb" to the service name.
# The Juju application name is retained for the default service created by Juju.
if self.service_name == self._app and service_type == "LoadBalancer":
self.service_name = f"{self._app}-lb"
self.service_type = service_type
self.service = self._service_object(
ports,
service_name,
self.service_name,
service_type,
additional_labels,
additional_selectors,
@ -202,6 +227,7 @@ class KubernetesServicePatch(Object):
self.framework.observe(charm.on.install, self._patch)
self.framework.observe(charm.on.upgrade_charm, self._patch)
self.framework.observe(charm.on.update_status, self._patch)
self.framework.observe(charm.on.stop, self._remove_service)
# apply user defined events
if refresh_event:
@ -277,7 +303,10 @@ class KubernetesServicePatch(Object):
if self._is_patched(client):
return
if self.service_name != self._app:
self._delete_and_create_service(client)
if not self.service_type == "LoadBalancer":
self._delete_and_create_service(client)
else:
self._create_lb_service(client)
client.patch(Service, self.service_name, self.service, patch_type=PatchType.MERGE)
except ApiError as e:
if e.status.code == 403:
@ -294,6 +323,12 @@ class KubernetesServicePatch(Object):
client.delete(Service, self._app, namespace=self._namespace)
client.create(service)
def _create_lb_service(self, client: Client):
try:
client.get(Service, self.service_name, namespace=self._namespace)
except ApiError:
client.create(self.service)
def is_patched(self) -> bool:
"""Reports if the service patch has been applied.
@ -321,6 +356,30 @@ class KubernetesServicePatch(Object):
] # noqa: E501
return expected_ports == fetched_ports
def _remove_service(self, _):
"""Remove a Kubernetes service associated with this charm.
Specifically designed to delete the load balancer service created by the charm, since Juju only deletes the
default ClusterIP service and not custom services.
Returns:
None
Raises:
ApiError: for deletion errors, excluding when the service is not found (404 Not Found).
"""
client = Client() # pyright: ignore
try:
client.delete(Service, self.service_name, namespace=self._namespace)
except ApiError as e:
if e.status.code == 404:
# Service not found, so no action needed
pass
else:
# Re-raise for other statuses
raise
@property
def _app(self) -> str:
"""Name of the current Juju application.

View File

@ -83,7 +83,7 @@ LIBAPI = 2
# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 5
LIBPATCH = 7
# Regex to locate 7-bit C1 ANSI sequences
@ -319,7 +319,10 @@ class Snap(object):
Default is to return a string.
"""
if typed:
config = json.loads(self._snap("get", ["-d", key]))
args = ["-d"]
if key:
args.append(key)
config = json.loads(self._snap("get", args))
if key:
return config.get(key)
return config
@ -584,13 +587,16 @@ class Snap(object):
"Installing snap %s, revision %s, tracking %s", self._name, revision, channel
)
self._install(channel, cohort, revision)
else:
logger.info("The snap installation completed successfully")
elif revision is None or revision != self._revision:
# The snap is installed, but we are changing it (e.g., switching channels).
logger.info(
"Refreshing snap %s, revision %s, tracking %s", self._name, revision, channel
)
self._refresh(channel=channel, cohort=cohort, revision=revision, devmode=devmode)
logger.info("The snap installation completed successfully")
logger.info("The snap refresh completed successfully")
else:
logger.info("Refresh of snap %s was unnecessary", self._name)
self._update_snap_apps()
self._state = state

View File

@ -178,7 +178,7 @@ configure the following scrape-related settings, which behave as described by th
- `scrape_timeout`
- `proxy_url`
- `relabel_configs`
- `metrics_relabel_configs`
- `metric_relabel_configs`
- `sample_limit`
- `label_limit`
- `label_name_length_limit`
@ -362,7 +362,7 @@ LIBAPI = 0
# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 46
LIBPATCH = 47
PYDEPS = ["cosl"]
@ -377,7 +377,7 @@ ALLOWED_KEYS = {
"scrape_timeout",
"proxy_url",
"relabel_configs",
"metrics_relabel_configs",
"metric_relabel_configs",
"sample_limit",
"label_limit",
"label_name_length_limit",

View File

@ -277,13 +277,13 @@ juju relate <tls-certificates provider charm> <tls-certificates requirer charm>
""" # noqa: D405, D410, D411, D214, D416
import copy
import ipaddress
import json
import logging
import uuid
from contextlib import suppress
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from ipaddress import IPv4Address
from typing import List, Literal, Optional, Union
from cryptography import x509
@ -317,7 +317,7 @@ LIBAPI = 3
# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 15
LIBPATCH = 17
PYDEPS = ["cryptography", "jsonschema"]
@ -1077,7 +1077,7 @@ def generate_csr( # noqa: C901
if sans_oid:
_sans.extend([x509.RegisteredID(x509.ObjectIdentifier(san)) for san in sans_oid])
if sans_ip:
_sans.extend([x509.IPAddress(IPv4Address(san)) for san in sans_ip])
_sans.extend([x509.IPAddress(ipaddress.ip_address(san)) for san in sans_ip])
if sans:
_sans.extend([x509.DNSName(san) for san in sans])
if sans_dns:
@ -1109,25 +1109,16 @@ def csr_matches_certificate(csr: str, cert: str) -> bool:
Returns:
bool: True/False depending on whether the CSR matches the certificate.
"""
try:
csr_object = x509.load_pem_x509_csr(csr.encode("utf-8"))
cert_object = x509.load_pem_x509_certificate(cert.encode("utf-8"))
csr_object = x509.load_pem_x509_csr(csr.encode("utf-8"))
cert_object = x509.load_pem_x509_certificate(cert.encode("utf-8"))
if csr_object.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
) != cert_object.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
):
return False
if (
csr_object.public_key().public_numbers().n # type: ignore[union-attr]
!= cert_object.public_key().public_numbers().n # type: ignore[union-attr]
):
return False
except ValueError:
logger.warning("Could not load certificate or CSR.")
if csr_object.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
) != cert_object.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
):
return False
return True

View File

@ -72,7 +72,7 @@ LIBAPI = 2
# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 12
LIBPATCH = 13
PYDEPS = ["pydantic"]
@ -590,7 +590,7 @@ class IngressPerAppProvider(_IngressPerAppBase):
if PYDANTIC_IS_V1:
results[ingress_relation.app.name] = ingress_data.ingress.dict()
else:
results[ingress_relation.app.name] = ingress_data.ingress.model_dump(mode=json) # type: ignore
results[ingress_relation.app.name] = ingress_data.ingress.model_dump(mode="json")
return results

View File

@ -68,7 +68,7 @@ class ExampleRequirerCharm(CharmBase):
unit_credentials = self.interface.get_unit_credentials(relation)
# unit_credentials is a juju secret id
secret = self.model.get_secret(id=unit_credentials)
secret_content = secret.get_content()
secret_content = secret.get_content(refresh=True)
role_id = secret_content["role-id"]
role_secret_id = secret_content["role-secret-id"]
@ -99,7 +99,7 @@ class ExampleRequirerCharm(CharmBase):
def get_nonce(self):
secret = self.model.get_secret(label=NONCE_SECRET_LABEL)
nonce = secret.get_content()["nonce"]
nonce = secret.get_content(refresh=True)["nonce"]
return nonce
@ -132,7 +132,7 @@ LIBAPI = 0
# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 5
LIBPATCH = 7
PYDEPS = ["pydantic", "pytest-interface-tester"]
@ -163,6 +163,9 @@ class VaultKvProviderSchema(BaseModel):
ca_certificate: str = Field(
description="The CA certificate to use when validating the Vault server's certificate."
)
egress_subnet: str = Field(
description="The CIDR allowed by the role."
)
credentials: Json[Mapping[str, str]] = Field(
description=(
"Mapping of unit name and credentials for that unit."
@ -352,7 +355,18 @@ class VaultKvProvides(ops.Object):
relation.data[self.charm.app]["mount"] = mount
def set_unit_credentials(self, relation: ops.Relation, nonce: str, secret: ops.Secret):
def set_egress_subnet(self, relation: ops.Relation, egress_subnet: str):
"""Set the egress_subnet on the relation."""
if not self.charm.unit.is_leader():
return
relation.data[self.charm.app]["egress_subnet"] = egress_subnet
def set_unit_credentials(
self,
relation: ops.Relation,
nonce: str,
secret: ops.Secret,
):
"""Set the unit credentials on the relation."""
if not self.charm.unit.is_leader():
return
@ -526,7 +540,11 @@ class VaultKvRequires(ops.Object):
self.mount_suffix = mount_suffix
self.framework.observe(
self.charm.on[relation_name].relation_joined,
self._on_vault_kv_relation_joined,
self._handle_relation,
)
self.framework.observe(
self.charm.on.config_changed,
self._handle_relation,
)
self.framework.observe(
self.charm.on[relation_name].relation_changed,
@ -545,17 +563,20 @@ class VaultKvRequires(ops.Object):
"""Set the egress_subnet on the relation."""
relation.data[self.charm.unit]["egress_subnet"] = egress_subnet
def _on_vault_kv_relation_joined(self, event: ops.RelationJoinedEvent):
"""Handle relation joined.
def _handle_relation(self, event: ops.EventBase):
"""Run when a new unit joins the relation or when the address of the unit changes.
Set the secret backend in the application databag if we are the leader.
Always update the egress_subnet in the unit databag.
Emit the connected event.
"""
relation = self.model.get_relation(relation_name=self.relation_name)
if not relation:
return
if self.charm.unit.is_leader():
event.relation.data[self.charm.app]["mount_suffix"] = self.mount_suffix
relation.data[self.charm.app]["mount_suffix"] = self.mount_suffix
self.on.connected.emit(
event.relation.id,
event.relation.name,
relation.id,
relation.name,
)
def _on_vault_kv_relation_changed(self, event: ops.RelationChangedEvent):

View File

@ -1,3 +1,3 @@
# This file is used to trigger a build.
# Change uuid to trigger a new build on every charms.
03381028-42a3-4a2d-9231-7a2642ede8c7
32faabc5-4c45-430a-827e-9d917c2a6c3b