Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion operator/Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
VERSION ?= 0.13.1
VERSION ?= 0.14.0
GIT_TAG := operator_v$(VERSION)
KEIP_INTEGRATION_IMAGE ?= ghcr.io/codice/keip/minimal-app:latest

Expand Down
3 changes: 2 additions & 1 deletion operator/controller/core-controller.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,10 @@ spec:
labels:
app: integrationroute-webhook
spec:
serviceAccountName: keip-controller-service
containers:
- name: webhook
image: ghcr.io/codice/keip/webapp:0.16.0
image: ghcr.io/codice/keip/webapp:0.17.0
ports:
- containerPort: 7080
name: webhook-http
Expand Down
35 changes: 35 additions & 0 deletions operator/controller/core-privileges.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,39 @@ subjects:
roleRef:
kind: Role
name: spring-cloud-kubernetes
apiGroup: ""
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: keip-controller-service
namespace: keip
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: controller-kubernetes-manager
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["keip.codice.org"]
resources: ["integrationroutes"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: controller-kubernetes-binding
subjects:
- kind: ServiceAccount
name: keip-controller-service
namespace: keip
apiGroup: ""
roleRef:
kind: ClusterRole
name: controller-kubernetes-manager
apiGroup: ""
2 changes: 1 addition & 1 deletion operator/webapp/Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
VERSION ?= 0.16.0
VERSION ?= 0.17.0
HOST_PORT ?= 7080
GIT_TAG := webapp_v$(VERSION)

Expand Down
26 changes: 22 additions & 4 deletions operator/webapp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@

A Python web server that implements the following endpoints:
- `/webhook`: A [lambda controller from the Metacontroller API](https://metacontroller.github.io/metacontroller/concepts.html#lambda-controller).
The webhook will be called as part of the Metacontroller control loop when `IntegrationRoute` parent
resources are detected.

The webhook contains two endpoints, `/webhook/sync` and `/webhook/addons/certmanager/sync`.
- `/route`: Deploys a route from an XML file.

The webhook contains two endpoints, `/webhook/sync` and `/webhook/addons/certmanager/sync`.
- `/webhook/sync`: The core logic that creates a `Deployment` from `IntegrationRoute` resources.
- `/webhook/addons/certmanager/sync`: An add-on that creates
a [cert-manager.io/v1.Certificate](https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.Certificate)
Expand All @@ -15,6 +13,12 @@ resources are detected.
The format for the request and response JSON payloads can be
seen [here](https://metacontroller.github.io/metacontroller/api/compositecontroller.html#sync-hook)

## Deployment

This web server is designed to be run as a service within a Kubernetes cluster. It is intended to be used with [Metacontroller](https://metacontroller.github.io/metacontroller/), which will call the `/webhook` endpoint to manage `IntegrationRoute` custom resources.

The `/route` endpoint is provided for convenience to deploy routes from XML files.

## Developer Guide

Requirements:
Expand Down Expand Up @@ -64,6 +68,20 @@ a [pre-commit git hook](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks
make precommit
```

### Docker

To build the Docker image, run:

```shell
make build
```

To run the Docker container:

```shell
make run-container
```

### Windows Development

There are Windows-compatible equivalents for most of the `make` commands listed above, prefixed with `win-` (
Expand Down
17 changes: 14 additions & 3 deletions operator/webapp/app.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,37 @@
import logging.config

from starlette.applications import Starlette
from starlette.routing import Route, Mount
from starlette.responses import JSONResponse
from starlette.types import ASGIApp

import config as cfg
from routes import webhook
from routes.deploy import deploy_route
from logconf import LOG_CONF


_LOGGER = logging.getLogger(__name__)


async def status(request):
return JSONResponse({"status": "UP"})


def create_app() -> ASGIApp:
logging.config.dictConfig(LOG_CONF)

if cfg.DEBUG:
_LOGGER.warning("Running server with debug mode. NOT SUITABLE FOR PRODUCTION!")

app = Starlette(debug=cfg.DEBUG)
app.mount("/webhook", webhook.router)
routes = [
Route("/route", deploy_route, methods=["PUT"]),
Route("/status", status, methods=["GET"]),
Mount(path="/webhook", routes=webhook.routes),
]
starlette_app = Starlette(debug=cfg.DEBUG, routes=routes)

return app
return starlette_app


app = create_app()
196 changes: 196 additions & 0 deletions operator/webapp/core/k8s_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
from typing import Tuple
from kubernetes import config, client
from kubernetes.client.rest import ApiException
import logging
import os

from models import RouteData, Resource, Status

ROUTE_API_GROUP = "keip.codice.org"
ROUTE_API_VERSION = "v1alpha1"
ROUTE_PLURAL = "integrationroutes"
WEBHOOK_CONTROLLER_PREFIX = "integrationroute-webhook"


_LOGGER = logging.getLogger(__name__)

try:
(
config.load_kube_config(os.getenv("KUBECONFIG"))
if os.getenv("KUBECONFIG")
else config.load_incluster_config()
)
except config.ConfigException:
# Fall back to local kubeconfig
_LOGGER.error(
msg="Failed to configure the k8s_client. Keip will be unable to deploy integration routes.",
)


v1 = client.CoreV1Api()
routeApi = client.CustomObjectsApi()


def _check_cluster_reachable() -> bool:
"""
Checks if the Kubernetes cluster is reachable by attempting to retrieve API resources.

This function attempts to call the Kubernetes API to list available API resources.
If the call succeeds, it confirms that the cluster is reachable and returns True.
If the call fails or raises an exception, it indicates that the cluster is unreachable
and returns False.

Returns:
bool: True if the cluster is reachable, False otherwise.
"""
try:
v1.get_api_resources()
return True
except ApiException:
return False


def _create_integration_route(route_data: RouteData, configmap_name: str) -> Resource:
"""Create or update a new Integration Route with the provided configmap"""
if not _check_cluster_reachable():
raise ApiException(
status=500,
reason="Kubernetes cluster not reachable. Verify the cluster is running",
)

existing_route = routeApi.list_namespaced_custom_object(
group=ROUTE_API_GROUP,
version=ROUTE_API_VERSION,
namespace=route_data.namespace,
plural=ROUTE_PLURAL,
field_selector=f"metadata.name={route_data.route_name}",
)

body = {
"apiVersion": "keip.codice.org/v1alpha1",
"kind": "IntegrationRoute",
"metadata": {
"name": route_data.route_name,
"labels": {"app.kubernetes.io/created-by": "keip"},
},
"spec": {"routeConfigMap": configmap_name},
}

status = Status.CREATED

if existing_route["items"]:
# Delete existing route
routeApi.delete_namespaced_custom_object(
group=ROUTE_API_GROUP,
version=ROUTE_API_VERSION,
namespace=route_data.namespace,
plural=ROUTE_PLURAL,
name=existing_route["items"][0]["metadata"]["name"],
)
status = Status.RECREATED

# Create new route
routeApi.create_namespaced_custom_object(
group=ROUTE_API_GROUP,
version=ROUTE_API_VERSION,
namespace=route_data.namespace,
plural=ROUTE_PLURAL,
body=body,
)

return Resource(status=status, name=route_data.route_name)


def _create_route_configmap(route_data: RouteData) -> Resource:
"""
Creates or updates a ConfigMap containing an XML route payload for an integration route.

This function generates a ConfigMap with the provided route configuration and creates or updates
it in the specified namespace.

Args:
route_data (RouteData): The route data containing the route name, namespace, and XML route file.

Returns:
Resource: A Resource object indicating the status (CREATED or UPDATED) and name of the created/updated ConfigMap.

Raises:
ApiException: If the Kubernetes cluster is unreachable or if there is an error during the API call.
Exception: If an unexpected error occurs during processing.
"""
if not _check_cluster_reachable():
raise ApiException(
status=500,
reason="Kubernetes cluster not reachable. Verify the cluster is running",
)

configmap_name = f"{route_data.route_name}-cm"
configmap = client.V1ConfigMap(
metadata=client.V1ObjectMeta(
name=configmap_name,
namespace=route_data.namespace,
labels={"app.kubernetes.io/created-by": "keip"},
),
data={"integrationRoute.xml": route_data.route_xml},
)

result = v1.list_namespaced_config_map(
namespace=route_data.namespace, field_selector=f"metadata.name={configmap_name}"
)

updated = False

if len(result.items) > 0:
# Update if exists
_LOGGER.info(
"Route ConfigMap '%s' already exists and will be updated", configmap_name
)

v1.replace_namespaced_config_map(
name=configmap_name, namespace=route_data.namespace, body=configmap
)
updated = True
else:
# Create if doesn't exist
_LOGGER.info(
"Route ConfigMap '%s' does not exist and will be created", configmap_name
)
v1.create_namespaced_config_map(
namespace=route_data.namespace, body=configmap
)

status = Status.UPDATED if updated else Status.CREATED
return Resource(status=status, name=configmap_name)


def create_route_resources(route_data: RouteData) -> Tuple[Resource, Resource]:
"""
Creates both a ConfigMap and an Integration Route resource for the specified route configuration.

This function orchestrates the creation of two Kubernetes resources:
1. A ConfigMap containing the XML route payload for the integration route
2. An Integration Route resource that routes traffic based on the provided configuration

The function first creates the ConfigMap using the provided route data, then creates the Integration Route
using the ConfigMap name as the routeConfigMap reference. If a ConfigMap with the same name already exists,
it is updated rather than recreated. The Integration Route is created or updated based on the existing state.

Args:
route_data (RouteData): The route data containing the route name, namespace, and XML route file.
Must include all required fields to properly configure the integration route.

Returns:
Tuple[Resource]: A list containing two Resource objects:
- The created/updated ConfigMap resource
- The created/updated Integration Route resource
The resources are returned in the order: [ConfigMap, Integration Route]

Raises:
ApiException: If the Kubernetes cluster is unreachable or if there is an error during API calls.
Exception: If an unexpected error occurs during processing or resource creation.
"""
route_cm = _create_route_configmap(route_data=route_data)
route = _create_integration_route(
route_data=route_data, configmap_name=route_cm.name
)
return route_cm, route
5 changes: 1 addition & 4 deletions operator/webapp/core/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,6 @@ def _spring_app_config_env_var(parent) -> Optional[Mapping]:


def _get_keystore_password_env(tls) -> Mapping[str, Any]:

keystore = tls.get("keystore")

if not keystore:
Expand All @@ -276,7 +275,6 @@ def _get_keystore_password_env(tls) -> Mapping[str, Any]:


def _get_java_jdk_options(tls) -> Optional[Mapping[str, str]]:

truststore = tls.get("truststore")

if not truststore:
Expand Down Expand Up @@ -312,7 +310,6 @@ def _generate_container_env_vars(parent) -> List[Mapping[str, str]]:


def _create_pod_template(parent, labels, integration_image) -> Mapping[str, Any]:

vol_config = VolumeConfig(parent["spec"])

has_tls = _has_tls(parent)
Expand Down Expand Up @@ -429,7 +426,7 @@ def _new_actuator_service(parent):
"integration-route": parent_metadata["name"],
"prometheus-metrics-enabled": "true",
},
"name": f'{parent_metadata["name"]}-actuator',
"name": f"{parent_metadata['name']}-actuator",
},
"spec": {
"ports": [
Expand Down
Loading