Skip to content

Commit

Permalink
Merge pull request #1125 from DSD-DBS/pdb
Browse files Browse the repository at this point in the history
feat(k8s): Add Pod disruption budgets
  • Loading branch information
MoritzWeber0 authored Oct 31, 2023
2 parents bc7cfed + 9130511 commit fa1068b
Show file tree
Hide file tree
Showing 17 changed files with 375 additions and 41 deletions.
64 changes: 64 additions & 0 deletions backend/capellacollab/sessions/operators/k8s.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ def __init__(self) -> None:
self.v1_apps = client.AppsV1Api(api_client=self.client)
self.v1_batch = client.BatchV1Api(api_client=self.client)
self.v1_networking = client.NetworkingV1Api(api_client=self.client)
self.v1_policy = client.PolicyV1Api(api_client=self.client)
self._openshift = None

@property
Expand Down Expand Up @@ -200,6 +201,11 @@ def start_session(
limits=limits,
)

self._create_disruption_budget(
name=_id,
deployment_name=_id,
)

service = self._create_service(
name=_id,
deployment_name=_id,
Expand All @@ -226,6 +232,13 @@ def kill_session(self, _id: str):
"Deleted deployment %s with status %s", _id, dep_status.status
)

if disrupt_status := self._delete_disruptionbudget(name=_id):
log.info(
"Deleted Pod discruption budget %s with status %s",
_id,
disrupt_status.status,
)

if loki_enabled and (conf_status := self._delete_config_map(name=_id)):
log.info(
"Deleted config map %s with status %s", _id, conf_status.status
Expand Down Expand Up @@ -572,6 +585,7 @@ def _create_deployment(
metadata=client.V1ObjectMeta(name=name),
spec=client.V1DeploymentSpec(
replicas=1,
strategy=client.V1DeploymentStrategy(type="Recreate"),
selector=client.V1LabelSelector(match_labels={"app": name}),
template=client.V1PodTemplateSpec(
metadata=client.V1ObjectMeta(
Expand Down Expand Up @@ -663,6 +677,41 @@ def _create_job(
)
return self.v1_batch.create_namespaced_job(namespace, job)

def _create_disruption_budget(
self,
name: str,
deployment_name: str,
) -> client.V1PodDisruptionBudget:
"""Disallow any pod discription for the deployment
If the deployment uses the recreate strategy together with
this budget, the cluster operator shall consult the administrator before
termination of the deployment.
More information:
https://kubernetes.io/docs/tasks/run-application/configure-pdb/
"""

discruption_budget: client.V1PodDisruptionBudget = (
client.V1PodDisruptionBudget(
kind="PodDisruptionBudget",
api_version="policy/v1",
metadata=client.V1ObjectMeta(
name=name,
labels={"app": name},
),
spec=client.V1PodDisruptionBudgetSpec(
max_unavailable=0,
selector=client.V1LabelSelector(
match_labels={"app": deployment_name}
),
),
)
)
return self.v1_policy.create_namespaced_pod_disruption_budget(
namespace, discruption_budget
)

def _create_service(
self,
name: str,
Expand Down Expand Up @@ -942,6 +991,21 @@ def _delete_service(self, name: str) -> client.V1Status | None:
log.exception("Error deleting service with name: %s", name)
return None

def _delete_disruptionbudget(self, name: str) -> client.V1Status | None:
try:
return self.v1_policy.delete_namespaced_pod_disruption_budget(
name, namespace
)
except exceptions.ApiException as e:
# Pod disruption budge doesn't exist or was already deleted
# Nothing to do
if not e.status == http.HTTPStatus.NOT_FOUND:
log.exception(
"Error deleting discruptionbudget with name: %s", name
)

return None

def _delete_ingress(self, name: str) -> client.V1Status | None:
try:
return self.v1_networking.delete_namespaced_ingress(
Expand Down
10 changes: 10 additions & 0 deletions backend/tests/sessions/k8s_operator/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager contributors
# SPDX-License-Identifier: Apache-2.0

import kubernetes.config
import pytest


@pytest.fixture(autouse=True)
def mock_k8s_load_config(monkeypatch):
monkeypatch.setattr(kubernetes.config, "load_config", lambda **_: None)
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,14 @@
# SPDX-License-Identifier: Apache-2.0

import base64
import os

import kubernetes.config
import pytest

from capellacollab.sessions.operators.k8s import (
KubernetesOperator,
lazy_b64decode,
)


@pytest.fixture(autouse=True)
def mock_k8s_load_config(monkeypatch):
monkeypatch.setattr(kubernetes.config, "load_config", lambda **_: None)


hello = base64.b64encode(b"hello") # aGVsbG8=


Expand All @@ -36,7 +28,7 @@ def test_lazy_b64_decode():
)


def test_download_file(monkeypatch):
def test_download_file(monkeypatch: pytest.MonkeyPatch):
mock_stream = MockStream([hello.decode("utf-8")])
monkeypatch.setattr(
"kubernetes.stream.stream", lambda *a, **ka: mock_stream
Expand Down Expand Up @@ -65,35 +57,3 @@ def is_open(self):

def read_stdout(self, timeout=None):
return self._blocks.pop(0)


def test_create_job(monkeypatch):
monkeypatch.setattr("kubernetes.config.load_config", lambda **_: None)
operator = KubernetesOperator()
monkeypatch.setattr(
operator.v1_batch, "create_namespaced_job", lambda namespace, job: None
)
result = operator.create_job(
image="fakeimage",
command="fakecmd",
labels={"key": "value"},
environment={"ENVVAR": "value"},
)

assert result


def test_create_cronjob(monkeypatch):
operator = KubernetesOperator()
monkeypatch.setattr(
operator.v1_batch,
"create_namespaced_cron_job",
lambda namespace, job: None,
)
result = operator.create_cronjob(
image="fakeimage",
command="fakecmd",
environment={"ENVVAR": "value"},
)

assert result
153 changes: 153 additions & 0 deletions backend/tests/sessions/k8s_operator/test_session_k8s_operator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager contributors
# SPDX-License-Identifier: Apache-2.0


import datetime

import pytest
from kubernetes import client
from kubernetes.client import exceptions

from capellacollab.sessions.operators import k8s


def test_start_session(monkeypatch: pytest.MonkeyPatch):
operator = k8s.KubernetesOperator()
monkeypatch.setattr(k8s, "loki_enabled", False)

name = "testname"
creation_timestamp = datetime.datetime.now()

deployment_counter = 0
service_counter = 0
disruption_budget_counter = 0

def create_namespaced_deployment(namespace, deployment):
nonlocal deployment_counter
deployment_counter += 1
return client.V1Deployment(
metadata=client.V1ObjectMeta(
name=name, creation_timestamp=creation_timestamp
)
)

monkeypatch.setattr(
operator.v1_apps,
"create_namespaced_deployment",
create_namespaced_deployment,
)

def create_namespaced_service(namespace, service):
nonlocal service_counter
service_counter += 1
return client.V1Service(metadata=client.V1ObjectMeta(name=name))

monkeypatch.setattr(
operator.v1_core,
"create_namespaced_service",
create_namespaced_service,
)

def create_namespaced_pod_disruption_budget(namespace, budget):
nonlocal disruption_budget_counter
disruption_budget_counter += 1

monkeypatch.setattr(
operator.v1_policy,
"create_namespaced_pod_disruption_budget",
create_namespaced_pod_disruption_budget,
)

session = operator.start_session(
image="hello-world",
username="testuser",
session_type="persistent",
tool_name="test tool",
version_name="test version",
environment={},
ports={"rdp": 3389},
volumes=[],
)

assert deployment_counter == 1
assert service_counter == 1
assert disruption_budget_counter == 1

assert session["id"] == "testname"


def test_kill_session(monkeypatch: pytest.MonkeyPatch):
operator = k8s.KubernetesOperator()
monkeypatch.setattr(k8s, "loki_enabled", False)

monkeypatch.setattr(
operator.v1_apps,
"delete_namespaced_deployment",
lambda namespace, name: client.V1Status(),
)

monkeypatch.setattr(
operator.v1_core,
"delete_namespaced_service",
lambda namespace, name: client.V1Status(),
)

monkeypatch.setattr(
operator.v1_policy,
"delete_namespaced_pod_disruption_budget",
lambda namespace, name: client.V1Status(),
)

operator.kill_session("testname")


def test_create_job(monkeypatch: pytest.MonkeyPatch):
operator = k8s.KubernetesOperator()
monkeypatch.setattr(
operator.v1_batch, "create_namespaced_job", lambda namespace, job: None
)
result = operator.create_job(
image="fakeimage",
command="fakecmd",
labels={"key": "value"},
environment={"ENVVAR": "value"},
)

assert result


def test_create_cronjob(monkeypatch: pytest.MonkeyPatch):
operator = k8s.KubernetesOperator()
monkeypatch.setattr(
operator.v1_batch,
"create_namespaced_cron_job",
lambda namespace, job: None,
)
result = operator.create_cronjob(
image="fakeimage",
command="fakecmd",
environment={"ENVVAR": "value"},
)

assert result


def test_delete_disruption_budget_with_api_error(
monkeypatch: pytest.MonkeyPatch,
):
"""Test that _delete_disruptionbudget does not raise an exception if the
Pod disruption budget does not exist.
"""

operator = k8s.KubernetesOperator()

def raise_api_exception(*args, **kwargs):
raise exceptions.ApiException(status=404)

monkeypatch.setattr(
operator.v1_policy,
"delete_namespaced_pod_disruption_budget",
raise_api_exception,
)
result = operator._delete_disruptionbudget("testname")
assert result is None
12 changes: 12 additions & 0 deletions helm/templates/backend/backend.disruptionsbudget.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager contributors
# SPDX-License-Identifier: Apache-2.0

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ .Release.Name }}-backend
spec:
minAvailable: 1
selector:
matchLabels:
id: {{ .Release.Name }}-deployment-backend
3 changes: 3 additions & 0 deletions helm/templates/backend/backend.serviceaccount.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["create", "delete"]
- apiGroups: ["policy"]
resources: ["poddisruptionbudgets"]
verbs: ["create", "delete"]
{{- if eq .Values.cluster.kind "Kubernetes" }}
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses"]
Expand Down
12 changes: 12 additions & 0 deletions helm/templates/backend/postgres.disruptionbudget.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager contributors
# SPDX-License-Identifier: Apache-2.0

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ .Release.Name }}-backend-postgres
spec:
minAvailable: 1
selector:
matchLabels:
id: {{ .Release.Name }}-deployment-backend-postgres
12 changes: 12 additions & 0 deletions helm/templates/docs/docs.discruptionbudget.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager contributors
# SPDX-License-Identifier: Apache-2.0

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ .Release.Name }}-docs
spec:
minAvailable: 1
selector:
matchLabels:
id: {{ .Release.Name }}-deployment-docs
Loading

0 comments on commit fa1068b

Please sign in to comment.