Revert "Upgrade external libraries"

This reverts commit 6eb7f3b72b08d146df180063c8df4ac81f73a61d.

Reason for revert: Kubernetes service patch broke ovn-* relations

Upgrade to kubernetes_service_patch changed the way the service loadbalancer is setup. Before, it updated the application service, now it creates a new one. This broke ovn-* relations to outside the cluster because the `ingress-bound-address` is now filled with a k8s svc ip address, not reachable from the outside.

Change-Id: I38819cfcf647215e1eec595c2457300cb9058f90
This commit is contained in:
Guillaume Boutry 2024-07-09 15:55:55 +00:00 committed by Gerrit Code Review
parent 6eb7f3b72b
commit 407bc49f13
9 changed files with 49 additions and 150 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 = 38 LIBPATCH = 37
PYDEPS = ["ops>=2.0.0"] PYDEPS = ["ops>=2.0.0"]
@ -2606,14 +2606,6 @@ 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."""
@ -2850,21 +2842,6 @@ 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 = 36 LIBPATCH = 35
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -1050,7 +1050,6 @@ 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 65535/TCP. default contains a "placeholder" port, which is 65536/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,26 +109,6 @@ 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`:
@ -166,7 +146,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 = 11 LIBPATCH = 9
ServiceType = Literal["ClusterIP", "LoadBalancer"] ServiceType = Literal["ClusterIP", "LoadBalancer"]
@ -206,15 +186,10 @@ 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 or self._app self.service_name = service_name if service_name else 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,
self.service_name, service_name,
service_type, service_type,
additional_labels, additional_labels,
additional_selectors, additional_selectors,
@ -227,7 +202,6 @@ 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:
@ -303,10 +277,7 @@ 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:
if not self.service_type == "LoadBalancer": self._delete_and_create_service(client)
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:
@ -323,12 +294,6 @@ 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.
@ -356,30 +321,6 @@ 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 = 7 LIBPATCH = 5
# Regex to locate 7-bit C1 ANSI sequences # Regex to locate 7-bit C1 ANSI sequences
@ -319,10 +319,7 @@ class Snap(object):
Default is to return a string. Default is to return a string.
""" """
if typed: if typed:
args = ["-d"] config = json.loads(self._snap("get", ["-d", key]))
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
@ -587,16 +584,13 @@ 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)
logger.info("The snap installation completed successfully") else:
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 refresh completed successfully") logger.info("The snap installation 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`
- `metric_relabel_configs` - `metrics_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 = 47 LIBPATCH = 46
PYDEPS = ["cosl"] PYDEPS = ["cosl"]
@ -377,7 +377,7 @@ ALLOWED_KEYS = {
"scrape_timeout", "scrape_timeout",
"proxy_url", "proxy_url",
"relabel_configs", "relabel_configs",
"metric_relabel_configs", "metrics_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 = 17 LIBPATCH = 15
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(ipaddress.ip_address(san)) for san in sans_ip]) _sans.extend([x509.IPAddress(IPv4Address(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,16 +1109,25 @@ 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.
""" """
csr_object = x509.load_pem_x509_csr(csr.encode("utf-8")) try:
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( 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 = 13 LIBPATCH = 12
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") results[ingress_relation.app.name] = ingress_data.ingress.model_dump(mode=json) # type: ignore
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(refresh=True) secret_content = secret.get_content()
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(refresh=True)["nonce"] nonce = secret.get_content()["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 = 7 LIBPATCH = 5
PYDEPS = ["pydantic", "pytest-interface-tester"] PYDEPS = ["pydantic", "pytest-interface-tester"]
@ -163,9 +163,6 @@ 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."
@ -355,18 +352,7 @@ class VaultKvProvides(ops.Object):
relation.data[self.charm.app]["mount"] = mount relation.data[self.charm.app]["mount"] = mount
def set_egress_subnet(self, relation: ops.Relation, egress_subnet: str): def set_unit_credentials(self, relation: ops.Relation, nonce: str, secret: ops.Secret):
"""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
@ -540,11 +526,7 @@ 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._handle_relation, self._on_vault_kv_relation_joined,
)
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,
@ -563,20 +545,17 @@ 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 _handle_relation(self, event: ops.EventBase): def _on_vault_kv_relation_joined(self, event: ops.RelationJoinedEvent):
"""Run when a new unit joins the relation or when the address of the unit changes. """Handle relation joined.
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.
Emit the connected event. Always update the egress_subnet in the unit databag.
""" """
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():
relation.data[self.charm.app]["mount_suffix"] = self.mount_suffix event.relation.data[self.charm.app]["mount_suffix"] = self.mount_suffix
self.on.connected.emit( self.on.connected.emit(
relation.id, event.relation.id,
relation.name, event.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.
32faabc5-4c45-430a-827e-9d917c2a6c3b 03381028-42a3-4a2d-9231-7a2642ede8c7