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 # Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version # to 0 if you are raising the major API version
LIBPATCH = 37 LIBPATCH = 38
PYDEPS = ["ops>=2.0.0"] PYDEPS = ["ops>=2.0.0"]
@ -2606,6 +2606,14 @@ class DatabaseProviderData(ProviderData):
""" """
self.update_relation_data(relation_id, {"version": version}) 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): class DatabaseProviderEventHandlers(EventHandlers):
"""Provider-side of the database relation handlers.""" """Provider-side of the database relation handlers."""
@ -2842,6 +2850,21 @@ class DatabaseRequirerEventHandlers(RequirerEventHandlers):
def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: def _on_relation_changed_event(self, event: RelationChangedEvent) -> None:
"""Event emitted when the database relation has changed.""" """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. # Check which data has changed to emit customs events.
diff = self._diff(event) diff = self._diff(event)

View File

@ -219,7 +219,7 @@ LIBAPI = 0
# Increment this PATCH version before using `charmcraft publish-lib` or reset # Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version # to 0 if you are raising the major API version
LIBPATCH = 35 LIBPATCH = 36
logger = logging.getLogger(__name__) 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.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.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.framework.observe(
self._charm.on[self._relation_name].relation_created, 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 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 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 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 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 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 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 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`: 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 # Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version # to 0 if you are raising the major API version
LIBPATCH = 9 LIBPATCH = 11
ServiceType = Literal["ClusterIP", "LoadBalancer"] ServiceType = Literal["ClusterIP", "LoadBalancer"]
@ -186,10 +206,15 @@ class KubernetesServicePatch(Object):
""" """
super().__init__(charm, "kubernetes-service-patch") super().__init__(charm, "kubernetes-service-patch")
self.charm = charm 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( self.service = self._service_object(
ports, ports,
service_name, self.service_name,
service_type, service_type,
additional_labels, additional_labels,
additional_selectors, additional_selectors,
@ -202,6 +227,7 @@ class KubernetesServicePatch(Object):
self.framework.observe(charm.on.install, self._patch) self.framework.observe(charm.on.install, self._patch)
self.framework.observe(charm.on.upgrade_charm, 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.update_status, self._patch)
self.framework.observe(charm.on.stop, self._remove_service)
# apply user defined events # apply user defined events
if refresh_event: if refresh_event:
@ -277,7 +303,10 @@ class KubernetesServicePatch(Object):
if self._is_patched(client): if self._is_patched(client):
return return
if self.service_name != self._app: 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) client.patch(Service, self.service_name, self.service, patch_type=PatchType.MERGE)
except ApiError as e: except ApiError as e:
if e.status.code == 403: if e.status.code == 403:
@ -294,6 +323,12 @@ class KubernetesServicePatch(Object):
client.delete(Service, self._app, namespace=self._namespace) client.delete(Service, self._app, namespace=self._namespace)
client.create(service) 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: def is_patched(self) -> bool:
"""Reports if the service patch has been applied. """Reports if the service patch has been applied.
@ -321,6 +356,30 @@ class KubernetesServicePatch(Object):
] # noqa: E501 ] # noqa: E501
return expected_ports == fetched_ports 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 @property
def _app(self) -> str: def _app(self) -> str:
"""Name of the current Juju application. """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 # Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version # to 0 if you are raising the major API version
LIBPATCH = 5 LIBPATCH = 7
# Regex to locate 7-bit C1 ANSI sequences # Regex to locate 7-bit C1 ANSI sequences
@ -319,7 +319,10 @@ class Snap(object):
Default is to return a string. Default is to return a string.
""" """
if typed: 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: if key:
return config.get(key) return config.get(key)
return config return config
@ -584,13 +587,16 @@ class Snap(object):
"Installing snap %s, revision %s, tracking %s", self._name, revision, channel "Installing snap %s, revision %s, tracking %s", self._name, revision, channel
) )
self._install(channel, cohort, revision) 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). # The snap is installed, but we are changing it (e.g., switching channels).
logger.info( logger.info(
"Refreshing snap %s, revision %s, tracking %s", self._name, revision, channel "Refreshing snap %s, revision %s, tracking %s", self._name, revision, channel
) )
self._refresh(channel=channel, cohort=cohort, revision=revision, devmode=devmode) 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._update_snap_apps()
self._state = state self._state = state

View File

@ -178,7 +178,7 @@ configure the following scrape-related settings, which behave as described by th
- `scrape_timeout` - `scrape_timeout`
- `proxy_url` - `proxy_url`
- `relabel_configs` - `relabel_configs`
- `metrics_relabel_configs` - `metric_relabel_configs`
- `sample_limit` - `sample_limit`
- `label_limit` - `label_limit`
- `label_name_length_limit` - `label_name_length_limit`
@ -362,7 +362,7 @@ LIBAPI = 0
# Increment this PATCH version before using `charmcraft publish-lib` or reset # Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version # to 0 if you are raising the major API version
LIBPATCH = 46 LIBPATCH = 47
PYDEPS = ["cosl"] PYDEPS = ["cosl"]
@ -377,7 +377,7 @@ ALLOWED_KEYS = {
"scrape_timeout", "scrape_timeout",
"proxy_url", "proxy_url",
"relabel_configs", "relabel_configs",
"metrics_relabel_configs", "metric_relabel_configs",
"sample_limit", "sample_limit",
"label_limit", "label_limit",
"label_name_length_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 """ # noqa: D405, D410, D411, D214, D416
import copy import copy
import ipaddress
import json import json
import logging import logging
import uuid import uuid
from contextlib import suppress from contextlib import suppress
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from ipaddress import IPv4Address
from typing import List, Literal, Optional, Union from typing import List, Literal, Optional, Union
from cryptography import x509 from cryptography import x509
@ -317,7 +317,7 @@ LIBAPI = 3
# Increment this PATCH version before using `charmcraft publish-lib` or reset # Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version # to 0 if you are raising the major API version
LIBPATCH = 15 LIBPATCH = 17
PYDEPS = ["cryptography", "jsonschema"] PYDEPS = ["cryptography", "jsonschema"]
@ -1077,7 +1077,7 @@ def generate_csr( # noqa: C901
if sans_oid: if sans_oid:
_sans.extend([x509.RegisteredID(x509.ObjectIdentifier(san)) for san in sans_oid]) _sans.extend([x509.RegisteredID(x509.ObjectIdentifier(san)) for san in sans_oid])
if sans_ip: 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: if sans:
_sans.extend([x509.DNSName(san) for san in sans]) _sans.extend([x509.DNSName(san) for san in sans])
if sans_dns: if sans_dns:
@ -1109,25 +1109,16 @@ def csr_matches_certificate(csr: str, cert: str) -> bool:
Returns: Returns:
bool: True/False depending on whether the CSR matches the certificate. bool: True/False depending on whether the CSR matches the certificate.
""" """
try: csr_object = x509.load_pem_x509_csr(csr.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"))
cert_object = x509.load_pem_x509_certificate(cert.encode("utf-8"))
if csr_object.public_key().public_bytes( if csr_object.public_key().public_bytes(
encoding=serialization.Encoding.PEM, encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo, format=serialization.PublicFormat.SubjectPublicKeyInfo,
) != cert_object.public_key().public_bytes( ) != cert_object.public_key().public_bytes(
encoding=serialization.Encoding.PEM, encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo, 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.")
return False return False
return True return True

View File

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

View File

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

View File

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