|
8 | 8 | import psycopg2 |
9 | 9 | import requests |
10 | 10 | import yaml |
| 11 | +from lightkube import codecs |
| 12 | +from lightkube.core.client import Client |
| 13 | +from lightkube.core.exceptions import ApiError |
| 14 | +from lightkube.generic_resource import GenericNamespacedResource |
| 15 | +from lightkube.resources.core_v1 import Endpoints, Service |
11 | 16 | from pytest_operator.plugin import OpsTest |
12 | 17 |
|
13 | 18 | METADATA = yaml.safe_load(Path("./metadata.yaml").read_text()) |
@@ -196,6 +201,110 @@ def get_application_units(ops_test: OpsTest, application_name: str) -> List[str] |
196 | 201 | ] |
197 | 202 |
|
198 | 203 |
|
| 204 | +def get_charm_resources(namespace: str, application: str) -> List[GenericNamespacedResource]: |
| 205 | + """Return the list of k8s resources from resources.yaml file. |
| 206 | +
|
| 207 | + Args: |
| 208 | + namespace: namespace related to the model where |
| 209 | + the charm was deployed. |
| 210 | + application: application name. |
| 211 | +
|
| 212 | + Returns: |
| 213 | + list of existing charm/Patroni specific k8s resources. |
| 214 | + """ |
| 215 | + # Define the context needed for the k8s resources lists load. |
| 216 | + context = {"namespace": namespace, "app_name": application} |
| 217 | + |
| 218 | + # Load the list of the resources from resources.yaml. |
| 219 | + with open("src/resources.yaml") as f: |
| 220 | + return codecs.load_all_yaml(f, context=context) |
| 221 | + |
| 222 | + |
| 223 | +def get_existing_k8s_resources(namespace: str, application: str) -> set: |
| 224 | + """Return the list of k8s resources that were created by the charm and Patroni. |
| 225 | +
|
| 226 | + Args: |
| 227 | + namespace: namespace related to the model where |
| 228 | + the charm was deployed. |
| 229 | + application: application name. |
| 230 | +
|
| 231 | + Returns: |
| 232 | + list of existing charm/Patroni specific k8s resources. |
| 233 | + """ |
| 234 | + # Create a k8s API client instance. |
| 235 | + client = Client(namespace=namespace) |
| 236 | + |
| 237 | + # Retrieve the k8s resources the charm should create. |
| 238 | + charm_resources = get_charm_resources(namespace, application) |
| 239 | + |
| 240 | + # Add only the resources that currently exist. |
| 241 | + resources = set( |
| 242 | + map( |
| 243 | + # Build an identifier for each resource (using its type and name). |
| 244 | + lambda x: f"{type(x).__name__}/{x.metadata.name}", |
| 245 | + filter( |
| 246 | + lambda x: (resource_exists(client, x)), |
| 247 | + charm_resources, |
| 248 | + ), |
| 249 | + ) |
| 250 | + ) |
| 251 | + |
| 252 | + # Include the resources created by the charm and Patroni. |
| 253 | + for kind in [Endpoints, Service]: |
| 254 | + extra_resources = client.list( |
| 255 | + kind, |
| 256 | + namespace=namespace, |
| 257 | + labels={"app.juju.is/created-by": application}, |
| 258 | + ) |
| 259 | + resources.update( |
| 260 | + set( |
| 261 | + map( |
| 262 | + # Build an identifier for each resource (using its type and name). |
| 263 | + lambda x: f"{kind.__name__}/{x.metadata.name}", |
| 264 | + extra_resources, |
| 265 | + ) |
| 266 | + ) |
| 267 | + ) |
| 268 | + |
| 269 | + return resources |
| 270 | + |
| 271 | + |
| 272 | +def get_expected_k8s_resources(namespace: str, application: str) -> set: |
| 273 | + """Return the list of expected k8s resources when the charm is deployed. |
| 274 | +
|
| 275 | + Args: |
| 276 | + namespace: namespace related to the model where |
| 277 | + the charm was deployed. |
| 278 | + application: application name. |
| 279 | +
|
| 280 | + Returns: |
| 281 | + list of existing charm/Patroni specific k8s resources. |
| 282 | + """ |
| 283 | + # Retrieve the k8s resources created by the charm. |
| 284 | + charm_resources = get_charm_resources(namespace, application) |
| 285 | + |
| 286 | + # Build an identifier for each resource (using its type and name). |
| 287 | + resources = set( |
| 288 | + map( |
| 289 | + lambda x: f"{type(x).__name__}/{x.metadata.name}", |
| 290 | + charm_resources, |
| 291 | + ) |
| 292 | + ) |
| 293 | + |
| 294 | + # Include the resources created by the charm and Patroni. |
| 295 | + resources.update( |
| 296 | + [ |
| 297 | + f"Endpoints/patroni-{application}-config", |
| 298 | + f"Endpoints/patroni-{application}", |
| 299 | + f"Endpoints/{application}-primary", |
| 300 | + f"Endpoints/{application}-replicas", |
| 301 | + f"Service/patroni-{application}-config", |
| 302 | + ] |
| 303 | + ) |
| 304 | + |
| 305 | + return resources |
| 306 | + |
| 307 | + |
199 | 308 | async def get_operator_password(ops_test: OpsTest): |
200 | 309 | """Retrieve the operator user password using the action.""" |
201 | 310 | unit = ops_test.model.units.get(f"{DATABASE_APP_NAME}/0") |
@@ -235,6 +344,23 @@ async def get_unit_address(ops_test: OpsTest, unit_name: str) -> str: |
235 | 344 | return status["applications"][unit_name.split("/")[0]].units[unit_name]["address"] |
236 | 345 |
|
237 | 346 |
|
| 347 | +def resource_exists(client: Client, resource: GenericNamespacedResource) -> bool: |
| 348 | + """Check whether a specific resource exists. |
| 349 | +
|
| 350 | + Args: |
| 351 | + client: k8s API client instance. |
| 352 | + resource: k8s resource. |
| 353 | +
|
| 354 | + Returns: |
| 355 | + whether the resource exists. |
| 356 | + """ |
| 357 | + try: |
| 358 | + client.get(type(resource), name=resource.metadata.name) |
| 359 | + return True |
| 360 | + except ApiError: |
| 361 | + return False |
| 362 | + |
| 363 | + |
238 | 364 | async def scale_application(ops_test: OpsTest, application_name: str, scale: int) -> None: |
239 | 365 | """Scale a given application to a specific unit count. |
240 | 366 |
|
|
0 commit comments