Update kubernetes_service_patch to v1
v1 of the kubernetes_service_patch lib will patches the service definition on `status_update` event. This helps when Juju refreshes the patched services to their initial state. Depends-On: https://review.opendev.org/c/openstack/charm-ops-sunbeam/+/880270 Change-Id: I44ee58cb9f5ddee6339bcd207365a2dcb49ac006 Signed-off-by: Guillaume Boutry <guillaume.boutry@canonical.com>
This commit is contained in:
parent
294e26135c
commit
069a9daca6
@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
echo "INFO: Fetching libs from charmhub."
|
echo "INFO: Fetching libs from charmhub."
|
||||||
charmcraft fetch-lib charms.observability_libs.v0.kubernetes_service_patch
|
charmcraft fetch-lib charms.observability_libs.v1.kubernetes_service_patch
|
||||||
charmcraft fetch-lib charms.ovn_central_k8s.v0.ovsdb
|
charmcraft fetch-lib charms.ovn_central_k8s.v0.ovsdb
|
||||||
charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates
|
charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates
|
||||||
|
@ -9,21 +9,20 @@ service named after the application in the namespace (named after the Juju model
|
|||||||
default contains a "placeholder" port, which is 65536/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
|
charm. In this case, any modifications to the default service (created during deployment), will be
|
||||||
be overwritten during a charm upgrade.
|
overwritten during a charm upgrade.
|
||||||
|
|
||||||
When initialised, this library binds a handler to the parent charm's `install` and `upgrade_charm`
|
When initialised, this library binds a handler to the parent charm's `install` and `upgrade_charm`
|
||||||
events which applies the patch to the cluster. This should ensure that the service ports are
|
events which applies the patch to the cluster. This should ensure that the service ports are
|
||||||
correct throughout the charm's life.
|
correct throughout the charm's life.
|
||||||
|
|
||||||
The constructor simply takes a reference to the parent charm, and a list of tuples that each define
|
The constructor simply takes a reference to the parent charm, and a list of
|
||||||
a port for the service, where each tuple contains:
|
[`lightkube`](https://github.com/gtsystem/lightkube) ServicePorts that each define a port for the
|
||||||
|
service. For information regarding the `lightkube` `ServicePort` model, please visit the
|
||||||
|
`lightkube` [docs](https://gtsystem.github.io/lightkube-models/1.23/models/core_v1/#serviceport).
|
||||||
|
|
||||||
- a name for the port
|
Optionally, a name of the service (in case service name needs to be patched as well), labels,
|
||||||
- port for the service to listen on
|
selectors, and annotations can be provided as keyword arguments.
|
||||||
- optionally: a targetPort for the service (the port in the container!)
|
|
||||||
- optionally: a nodePort for the service (for NodePort or LoadBalancer services only!)
|
|
||||||
- optionally: a name of the service (in case service name needs to be patched as well)
|
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
@ -32,8 +31,8 @@ that you also need to add `lightkube` and `lightkube-models` to your charm's `re
|
|||||||
|
|
||||||
```shell
|
```shell
|
||||||
cd some-charm
|
cd some-charm
|
||||||
charmcraft fetch-lib charms.observability_libs.v0.kubernetes_service_patch
|
charmcraft fetch-lib charms.observability_libs.v1.kubernetes_service_patch
|
||||||
echo <<-EOF >> requirements.txt
|
cat << EOF >> requirements.txt
|
||||||
lightkube
|
lightkube
|
||||||
lightkube-models
|
lightkube-models
|
||||||
EOF
|
EOF
|
||||||
@ -41,28 +40,71 @@ EOF
|
|||||||
|
|
||||||
Then, to initialise the library:
|
Then, to initialise the library:
|
||||||
|
|
||||||
For ClusterIP services:
|
For `ClusterIP` services:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# ...
|
# ...
|
||||||
from charms.observability_libs.v0.kubernetes_service_patch import KubernetesServicePatch
|
from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
|
||||||
|
from lightkube.models.core_v1 import ServicePort
|
||||||
|
|
||||||
class SomeCharm(CharmBase):
|
class SomeCharm(CharmBase):
|
||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
# ...
|
# ...
|
||||||
self.service_patcher = KubernetesServicePatch(self, [(f"{self.app.name}", 8080)])
|
port = ServicePort(443, name=f"{self.app.name}")
|
||||||
|
self.service_patcher = KubernetesServicePatch(self, [port])
|
||||||
# ...
|
# ...
|
||||||
```
|
```
|
||||||
|
|
||||||
For LoadBalancer/NodePort services:
|
For `LoadBalancer`/`NodePort` services:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# ...
|
# ...
|
||||||
from charms.observability_libs.v0.kubernetes_service_patch import KubernetesServicePatch
|
from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
|
||||||
|
from lightkube.models.core_v1 import ServicePort
|
||||||
|
|
||||||
class SomeCharm(CharmBase):
|
class SomeCharm(CharmBase):
|
||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
# ...
|
# ...
|
||||||
|
port = ServicePort(443, name=f"{self.app.name}", targetPort=443, nodePort=30666)
|
||||||
self.service_patcher = KubernetesServicePatch(
|
self.service_patcher = KubernetesServicePatch(
|
||||||
self, [(f"{self.app.name}", 443, 443, 30666)], "LoadBalancer"
|
self, [port], "LoadBalancer"
|
||||||
|
)
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Port protocols can also be specified. Valid protocols are `"TCP"`, `"UDP"`, and `"SCTP"`
|
||||||
|
|
||||||
|
```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):
|
||||||
|
# ...
|
||||||
|
tcp = ServicePort(443, name=f"{self.app.name}-tcp", protocol="TCP")
|
||||||
|
udp = ServicePort(443, name=f"{self.app.name}-udp", protocol="UDP")
|
||||||
|
sctp = ServicePort(443, name=f"{self.app.name}-sctp", protocol="SCTP")
|
||||||
|
self.service_patcher = KubernetesServicePatch(self, [tcp, udp, sctp])
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Bound with custom events by providing `refresh_event` argument:
|
||||||
|
For example, you would like to have a configurable port in your charm and want to apply
|
||||||
|
service patch every time charm config is changed.
|
||||||
|
|
||||||
|
```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],
|
||||||
|
refresh_event=self.on.config_changed
|
||||||
)
|
)
|
||||||
# ...
|
# ...
|
||||||
```
|
```
|
||||||
@ -83,15 +125,16 @@ def setUp(self, *unused):
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from types import MethodType
|
from types import MethodType
|
||||||
from typing import Literal, Sequence, Tuple, Union
|
from typing import List, Literal, Optional, Union
|
||||||
|
|
||||||
from lightkube import ApiError, Client
|
from lightkube import ApiError, Client
|
||||||
|
from lightkube.core import exceptions
|
||||||
from lightkube.models.core_v1 import ServicePort, ServiceSpec
|
from lightkube.models.core_v1 import ServicePort, ServiceSpec
|
||||||
from lightkube.models.meta_v1 import ObjectMeta
|
from lightkube.models.meta_v1 import ObjectMeta
|
||||||
from lightkube.resources.core_v1 import Service
|
from lightkube.resources.core_v1 import Service
|
||||||
from lightkube.types import PatchType
|
from lightkube.types import PatchType
|
||||||
from ops.charm import CharmBase
|
from ops.charm import CharmBase
|
||||||
from ops.framework import Object
|
from ops.framework import BoundEvent, Object
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -99,13 +142,12 @@ logger = logging.getLogger(__name__)
|
|||||||
LIBID = "0042f86d0a874435adef581806cddbbb"
|
LIBID = "0042f86d0a874435adef581806cddbbb"
|
||||||
|
|
||||||
# Increment this major API version when introducing breaking changes
|
# Increment this major API version when introducing breaking changes
|
||||||
LIBAPI = 0
|
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 = 6
|
LIBPATCH = 6
|
||||||
|
|
||||||
PortDefinition = Union[Tuple[str, int], Tuple[str, int, int], Tuple[str, int, int, int]]
|
|
||||||
ServiceType = Literal["ClusterIP", "LoadBalancer"]
|
ServiceType = Literal["ClusterIP", "LoadBalancer"]
|
||||||
|
|
||||||
|
|
||||||
@ -115,18 +157,20 @@ class KubernetesServicePatch(Object):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
charm: CharmBase,
|
charm: CharmBase,
|
||||||
ports: Sequence[PortDefinition],
|
ports: List[ServicePort],
|
||||||
service_name: str = None,
|
service_name: Optional[str] = None,
|
||||||
service_type: ServiceType = "ClusterIP",
|
service_type: ServiceType = "ClusterIP",
|
||||||
additional_labels: dict = None,
|
additional_labels: Optional[dict] = None,
|
||||||
additional_selectors: dict = None,
|
additional_selectors: Optional[dict] = None,
|
||||||
additional_annotations: dict = None,
|
additional_annotations: Optional[dict] = None,
|
||||||
|
*,
|
||||||
|
refresh_event: Optional[Union[BoundEvent, List[BoundEvent]]] = None,
|
||||||
):
|
):
|
||||||
"""Constructor for KubernetesServicePatch.
|
"""Constructor for KubernetesServicePatch.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
charm: the charm that is instantiating the library.
|
charm: the charm that is instantiating the library.
|
||||||
ports: a list of tuples (name, port, targetPort, nodePort) for every service port.
|
ports: a list of ServicePorts
|
||||||
service_name: allows setting custom name to the patched service. If none given,
|
service_name: allows setting custom name to the patched service. If none given,
|
||||||
application name will be used.
|
application name will be used.
|
||||||
service_type: desired type of K8s service. Default value is in line with ServiceSpec's
|
service_type: desired type of K8s service. Default value is in line with ServiceSpec's
|
||||||
@ -136,6 +180,9 @@ class KubernetesServicePatch(Object):
|
|||||||
additional_selectors: Selectors to be added to the kubernetes service (by default only
|
additional_selectors: Selectors to be added to the kubernetes service (by default only
|
||||||
"app.kubernetes.io/name" is set to the service name)
|
"app.kubernetes.io/name" is set to the service name)
|
||||||
additional_annotations: Annotations to be added to the kubernetes service.
|
additional_annotations: Annotations to be added to the kubernetes service.
|
||||||
|
refresh_event: an optional bound event or list of bound events which
|
||||||
|
will be observed to re-apply the patch (e.g. on port change).
|
||||||
|
The `install` and `upgrade-charm` events would be observed regardless.
|
||||||
"""
|
"""
|
||||||
super().__init__(charm, "kubernetes-service-patch")
|
super().__init__(charm, "kubernetes-service-patch")
|
||||||
self.charm = charm
|
self.charm = charm
|
||||||
@ -154,23 +201,29 @@ class KubernetesServicePatch(Object):
|
|||||||
# Ensure this patch is applied during the 'install' and 'upgrade-charm' events
|
# Ensure this patch is applied during the 'install' and 'upgrade-charm' events
|
||||||
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)
|
||||||
|
|
||||||
|
# apply user defined events
|
||||||
|
if refresh_event:
|
||||||
|
if not isinstance(refresh_event, list):
|
||||||
|
refresh_event = [refresh_event]
|
||||||
|
|
||||||
|
for evt in refresh_event:
|
||||||
|
self.framework.observe(evt, self._patch)
|
||||||
|
|
||||||
def _service_object(
|
def _service_object(
|
||||||
self,
|
self,
|
||||||
ports: Sequence[PortDefinition],
|
ports: List[ServicePort],
|
||||||
service_name: str = None,
|
service_name: Optional[str] = None,
|
||||||
service_type: ServiceType = "ClusterIP",
|
service_type: ServiceType = "ClusterIP",
|
||||||
additional_labels: dict = None,
|
additional_labels: Optional[dict] = None,
|
||||||
additional_selectors: dict = None,
|
additional_selectors: Optional[dict] = None,
|
||||||
additional_annotations: dict = None,
|
additional_annotations: Optional[dict] = None,
|
||||||
) -> Service:
|
) -> Service:
|
||||||
"""Creates a valid Service representation.
|
"""Creates a valid Service representation.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ports: a list of tuples of the form (name, port) or (name, port, targetPort)
|
ports: a list of ServicePorts
|
||||||
or (name, port, targetPort, nodePort) for every service port. If the 'targetPort'
|
|
||||||
is omitted, it is assumed to be equal to 'port', with the exception of NodePort
|
|
||||||
and LoadBalancer services, where all port numbers have to be specified.
|
|
||||||
service_name: allows setting custom name to the patched service. If none given,
|
service_name: allows setting custom name to the patched service. If none given,
|
||||||
application name will be used.
|
application name will be used.
|
||||||
service_type: desired type of K8s service. Default value is in line with ServiceSpec's
|
service_type: desired type of K8s service. Default value is in line with ServiceSpec's
|
||||||
@ -203,15 +256,7 @@ class KubernetesServicePatch(Object):
|
|||||||
),
|
),
|
||||||
spec=ServiceSpec(
|
spec=ServiceSpec(
|
||||||
selector=selector,
|
selector=selector,
|
||||||
ports=[
|
ports=ports,
|
||||||
ServicePort(
|
|
||||||
name=p[0],
|
|
||||||
port=p[1],
|
|
||||||
targetPort=p[2] if len(p) > 2 else p[1], # type: ignore[misc]
|
|
||||||
nodePort=p[3] if len(p) > 3 else None, # type: ignore[arg-type, misc]
|
|
||||||
)
|
|
||||||
for p in ports
|
|
||||||
],
|
|
||||||
type=service_type,
|
type=service_type,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -222,11 +267,15 @@ class KubernetesServicePatch(Object):
|
|||||||
Raises:
|
Raises:
|
||||||
PatchFailed: if patching fails due to lack of permissions, or otherwise.
|
PatchFailed: if patching fails due to lack of permissions, or otherwise.
|
||||||
"""
|
"""
|
||||||
if not self.charm.unit.is_leader():
|
try:
|
||||||
|
client = Client()
|
||||||
|
except exceptions.ConfigError as e:
|
||||||
|
logger.warning("Error creating k8s client: %s", e)
|
||||||
return
|
return
|
||||||
|
|
||||||
client = Client()
|
|
||||||
try:
|
try:
|
||||||
|
if self._is_patched(client):
|
||||||
|
return
|
||||||
if self.service_name != self._app:
|
if self.service_name != self._app:
|
||||||
self._delete_and_create_service(client)
|
self._delete_and_create_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)
|
||||||
@ -252,12 +301,25 @@ class KubernetesServicePatch(Object):
|
|||||||
bool: A boolean indicating if the service patch has been applied.
|
bool: A boolean indicating if the service patch has been applied.
|
||||||
"""
|
"""
|
||||||
client = Client()
|
client = Client()
|
||||||
|
return self._is_patched(client)
|
||||||
|
|
||||||
|
def _is_patched(self, client: Client) -> bool:
|
||||||
# Get the relevant service from the cluster
|
# Get the relevant service from the cluster
|
||||||
service = client.get(Service, name=self.service_name, namespace=self._namespace)
|
try:
|
||||||
|
service = client.get(Service, name=self.service_name, namespace=self._namespace)
|
||||||
|
except ApiError as e:
|
||||||
|
if e.status.code == 404 and self.service_name != self._app:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
logger.error("Kubernetes service get failed: %s", str(e))
|
||||||
|
raise
|
||||||
|
|
||||||
# Construct a list of expected ports, should the patch be applied
|
# Construct a list of expected ports, should the patch be applied
|
||||||
expected_ports = [(p.port, p.targetPort) for p in self.service.spec.ports]
|
expected_ports = [(p.port, p.targetPort) for p in self.service.spec.ports]
|
||||||
# Construct a list in the same manner, using the fetched service
|
# Construct a list in the same manner, using the fetched service
|
||||||
fetched_ports = [(p.port, p.targetPort) for p in service.spec.ports] # type: ignore[attr-defined] # noqa: E501
|
fetched_ports = [
|
||||||
|
(p.port, p.targetPort) for p in service.spec.ports # type: ignore[attr-defined]
|
||||||
|
] # noqa: E501
|
||||||
return expected_ports == fetched_ports
|
return expected_ports == fetched_ports
|
||||||
|
|
||||||
@property
|
@property
|
@ -42,9 +42,12 @@ import ops_sunbeam.ovn.config_contexts as ovn_ctxts
|
|||||||
import ops_sunbeam.ovn.container_handlers as ovn_chandlers
|
import ops_sunbeam.ovn.container_handlers as ovn_chandlers
|
||||||
import ops_sunbeam.ovn.relation_handlers as ovn_relation_handlers
|
import ops_sunbeam.ovn.relation_handlers as ovn_relation_handlers
|
||||||
import ops_sunbeam.relation_handlers as sunbeam_rhandlers
|
import ops_sunbeam.relation_handlers as sunbeam_rhandlers
|
||||||
from charms.observability_libs.v0.kubernetes_service_patch import (
|
from charms.observability_libs.v1.kubernetes_service_patch import (
|
||||||
KubernetesServicePatch,
|
KubernetesServicePatch,
|
||||||
)
|
)
|
||||||
|
from lightkube.models.core_v1 import (
|
||||||
|
ServicePort,
|
||||||
|
)
|
||||||
from ops.main import (
|
from ops.main import (
|
||||||
main,
|
main,
|
||||||
)
|
)
|
||||||
@ -97,9 +100,7 @@ class OVNRelayOperatorCharm(ovn_charm.OSBaseOVNOperatorCharm):
|
|||||||
super().__init__(framework)
|
super().__init__(framework)
|
||||||
self.service_patcher = KubernetesServicePatch(
|
self.service_patcher = KubernetesServicePatch(
|
||||||
self,
|
self,
|
||||||
[
|
[ServicePort(6642, name="southbound")],
|
||||||
("southbound", 6642),
|
|
||||||
],
|
|
||||||
service_type="LoadBalancer",
|
service_type="LoadBalancer",
|
||||||
)
|
)
|
||||||
self.framework.observe(
|
self.framework.observe(
|
||||||
|
Loading…
Reference in New Issue
Block a user