Skip to content

Commit

Permalink
Merge branch 'main' of github.com:TU-Wien-dataLAB/config-server-operator
Browse files Browse the repository at this point in the history
  • Loading branch information
joseftaha committed Sep 29, 2024
2 parents 5b45830 + c76800e commit f5c5cc3
Show file tree
Hide file tree
Showing 10 changed files with 502 additions and 77 deletions.
51 changes: 51 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,56 @@ jobs:
- id: outputStep
run: echo "changeDirs=${{ steps.changeDirsStep.outputs.all_changed_files }}" >> $GITHUB_OUTPUT


server-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: '3.11'

- name: Install dependencies
run: |
pip install -r testing.requirements.txt
pip install -r srv/requirements.txt
- name: Run tests
run: pytest tests/server_tests

operator-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: '3.11'

- name: Setup k3s test cluster
uses: nolar/setup-k3d-k3s@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

- name: Install dependencies
run: |
pip install -r testing.requirements.txt
pip install -r opr/requirements.txt
- name: Run tests
run: pytest tests/operator_tests

docker:
runs-on: ubuntu-latest
needs:
- changed-dirs
- server-tests
- operator-tests
if: contains(needs.changed-dirs.outputs.changeDirs, 'opr') || contains(needs.changed-dirs.outputs.changeDirs, 'srv')
strategy:
matrix:
Expand All @@ -56,8 +102,10 @@ jobs:
# generate Docker tags based on the following events/attributes
tags: |
type=ref,event=branch
type=ref,event=tag
type=match,pattern=${{ matrix.pattern }},group=1
type=sha
type=raw,value=latest,enable={{is_default_branch}}
- name: Checkout repository
uses: actions/checkout@v4
Expand Down Expand Up @@ -88,6 +136,9 @@ jobs:

helm:
runs-on: ubuntu-latest
needs:
- server-tests
- operator-tests
steps:
- name: Checkout repository
uses: actions/checkout@v4
Expand Down
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,24 @@

![Python Version from PEP 621 TOML](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2FTU-Wien-dataLAB%2Fconfig-server-operator%2Fmain%2Fpyproject.toml)

![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/TU-Wien-dataLAB/config-server-operator/ci.yml?label=CI)


This operator provides CRDs to create key/value pairs (`KeyValuePair`). The operator combines these configuration objects into a single `ConfigMap` and deploys a REST API that can be used to access the individual values with the corresponding keys.

The operator is written in the [Kubernetes Operators Framework (Kopf)](https://kopf.readthedocs.io/en/stable/index.html#) while the HTTP server for the REST API is written in [Tornado](https://www.tornadoweb.org/en/stable/). The code for the operator is found in the `/opr` directory while the code for the Tornado server is located in the `/srv` directory.

The `/tests` directory contains unit and integration tests for the server and operator. The `/examples` directory contains simple examples on how to use the custom resources to deploy a config server instance. The `/chart` directory contains the definitions for a [Helm](https://helm.sh/) chart.

## Deployment

See [Kopf documentation](https://kopf.readthedocs.io/en/stable/deployment/). The Dockerfile for the deployment is part of this repository.

This repository also contains a Helm chart which can be deployed with:

```bash
TODO: how?
```

## Run Locally

Expand All @@ -31,13 +41,13 @@ Go to the project directory
Install dependencies

```bash
pip install kopf
pip install kopf kubernetes
```

Add the CRDs to the cluster

```bash
kubectl apply -f opr/crd.yaml
kubectl apply -f chart/config-server-operator/crds/
```

Run the operator
Expand Down
10 changes: 10 additions & 0 deletions chart/config-server-operator/templates/config-server.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
apiVersion: datalab.tuwien.ac.at/v1
kind: ConfigServer
metadata:
name: config-server
namespace: "{{ .Values.namespace }}"
spec:
image: ghcr.io/tu-wien-datalab/config-server:main
imagePullPolicy: Always
containerPort: 80
configMountPath: /var/lib/config-server
69 changes: 0 additions & 69 deletions opr/crd.yaml

This file was deleted.

16 changes: 13 additions & 3 deletions opr/operator.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
import json
import logging
import os

import kopf
import kubernetes
from kubernetes.client import ApiClient, CoreV1Api, AppsV1Api, CustomObjectsApi, V1ConfigMap, V1Service


def load_kubernetes_config():
if 'KUBERNETES_SERVICE_HOST' in os.environ:
# We're running inside a Kubernetes cluster
return kubernetes.config.load_incluster_config()
else:
# We're running outside the cluster
return kubernetes.config.load_kube_config()


@kopf.on.create('configserver')
def create_fn(meta, spec, **kwargs):
client = ApiClient(configuration=kubernetes.config.load_incluster_config())
client = ApiClient(configuration=load_kubernetes_config())
api = CoreV1Api(api_client=client)
apps_api = AppsV1Api(api_client=client)
crd_api = CustomObjectsApi(api_client=client)
Expand Down Expand Up @@ -100,7 +110,7 @@ def create_fn(meta, spec, **kwargs):

@kopf.on.delete('configserver')
def delete_fn(meta, spec, **kwargs):
client = ApiClient(configuration=kubernetes.config.load_incluster_config())
client = ApiClient(configuration=load_kubernetes_config())
api = CoreV1Api(api_client=client)
apps_api = AppsV1Api(api_client=client)

Expand All @@ -122,7 +132,7 @@ def delete_fn(meta, spec, **kwargs):


def _get_config_map(config_name: str, namespace: str, logger: logging.Logger) -> tuple[V1ConfigMap | None, CoreV1Api]:
client = kubernetes.client.api_client.ApiClient(configuration=kubernetes.config.load_incluster_config())
client = kubernetes.client.api_client.ApiClient(configuration=load_kubernetes_config())
api = kubernetes.client.CoreV1Api(api_client=client)
try:
config_map = api.read_namespaced_config_map(name=f"{config_name}-values", namespace=namespace)
Expand Down
88 changes: 88 additions & 0 deletions tests/operator_tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import os
import uuid

import kubernetes
import pytest
from kubernetes.client import ApiClient, CoreV1Api, V1Namespace, V1ObjectMeta, AppsV1Api, CustomObjectsApi, \
ApiextensionsV1Api


def delete_all(namespace, list_func, delete_func, patch_func):
resources = list_func(namespace=namespace)
try:
resources_list = resources["items"]
except TypeError:
resources_list = resources.items
for resource in resources_list:
# Operator is not running in fixtures, so we need a force-delete (or this patch).
patch_body = {
"metadata": {
"finalizers": []
}
}

try:
name = resource.metadata.name
except AttributeError:
name = resource["metadata"]["name"]

try:
patch_func(name=name, namespace=namespace, body=patch_body)
except kubernetes.client.exceptions.ApiException as e:
if e.status == 404:
pass

try:
delete_func(name=name, namespace=namespace)
except kubernetes.client.exceptions.ApiException as e:
if e.status == 404:
pass


def delete_all_custom_objects(crd_api, namespace, plural):
def list_cr(namespace):
return crd_api.list_namespaced_custom_object("datalab.tuwien.ac.at", "v1", namespace, plural)

def delete_cr(name, namespace):
return crd_api.delete_namespaced_custom_object("datalab.tuwien.ac.at", "v1", namespace, plural, name=name)

def patch_cr(name, namespace, body):
return crd_api.patch_namespaced_custom_object("datalab.tuwien.ac.at", "v1", namespace, plural, name=name, body=body)

delete_all(namespace, list_cr, delete_cr, patch_cr)


@pytest.fixture(scope="function")
def random_namespace():
client = ApiClient(configuration=kubernetes.config.load_kube_config())
api = CoreV1Api(api_client=client)

namespace = f'test-namespace-{uuid.uuid4().hex[:10]}'
try:
body = V1Namespace(metadata=V1ObjectMeta(name=namespace))
api.create_namespace(body=body)
yield namespace
finally:
apps_api = AppsV1Api(api_client=client)
crd_api = CustomObjectsApi(api_client=client)
extensions_api = ApiextensionsV1Api(api_client=client)

delete_all(namespace, api.list_namespaced_pod, api.delete_namespaced_pod, api.patch_namespaced_pod)
delete_all(namespace, api.list_namespaced_config_map, api.delete_namespaced_config_map, api.patch_namespaced_config_map)
delete_all(namespace, api.list_namespaced_service, api.delete_namespaced_service, api.patch_namespaced_service)
delete_all(namespace, apps_api.list_namespaced_deployment, apps_api.delete_namespaced_deployment, apps_api.patch_namespaced_deployment)

delete_all_custom_objects(crd_api, namespace, "keyvaluepairs")
delete_all_custom_objects(crd_api, namespace, "configservers")

config_server_crds = ["configservers.datalab.tuwien.ac.at", "keyvaluepairs.datalab.tuwien.ac.at"]
for name in config_server_crds:
extensions_api.delete_custom_resource_definition(name=name)

api.delete_namespace(name=namespace)
client.close()


@pytest.fixture(scope="session")
def operator_file():
return os.path.abspath(os.path.join(os.path.dirname(__file__), '../../opr/operator.py'))
Loading

0 comments on commit f5c5cc3

Please sign in to comment.