1
0
Fork 0

Add Kubernetes integration tests for EasyHAProxy examples

- Introduced a suite of pytest-based integration tests for Kubernetes using `kind`.
- Automated local installation of dependencies (`kind`, `kubectl`, and `helm`) when missing.
- Implemented fixtures for Kubernetes resource management and TLS secrets.
- Added end-to-end tests for HTTP and HTTPS ingress functionality.
This commit is contained in:
Joao Gilberto Magalhaes 2026-02-11 23:32:44 -05:00
parent 5767e55dea
commit 90b01b1f13
18 changed files with 2978 additions and 24 deletions

9
examples/kubernetes/.gitignore vendored Normal file
View file

@ -0,0 +1,9 @@
# kind installation directory
.kind/
# kubectl config
kubeconfig
# Test artifacts
*.log
service_tls_generated.yml

View file

@ -100,7 +100,8 @@ metadata:
# Allow specific IPs and networks
# UPDATE THIS with your actual office/VPN IPs!
easyhaproxy.plugin.ip_whitelist.allowed_ips: "203.0.113.0/24,198.51.100.42,10.0.0.0/8"
# For testing: includes localhost and Docker/Kubernetes private networks
easyhaproxy.plugin.ip_whitelist.allowed_ips: "127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,203.0.113.0/24,198.51.100.42"
# Status code to return for blocked IPs
easyhaproxy.plugin.ip_whitelist.status_code: "403"
@ -114,9 +115,10 @@ spec:
- host: admin.example.local
http:
paths:
- backend:
- path: /
pathType: Prefix
backend:
service:
name: admin-service
port:
number: 8080
pathType: ImplementationSpecific

View file

@ -0,0 +1,135 @@
# Example demonstrating JWT validator with Kubernetes secret
# This shows the recommended way to provide JWT public keys in Kubernetes
#
# IMPORTANT: Before applying this manifest, generate JWT keys by running:
# cd /path/to/examples && bash generate-keys.sh
#
# Then create the secrets with your generated keys:
# kubectl create secret generic jwt-pubkey-secret \
# --from-file=pubkey=docker/jwt_pubkey.pem -n default
# kubectl create secret generic jwt-custom-secret \
# --from-file=rsa-public-key=docker/jwt_pubkey.pem -n default
#
# TWO ANNOTATION FORMATS:
# 1. Auto-detect key (tries common variations):
# easyhaproxy.plugin.jwt_validator.k8s_secret.pubkey: "jwt-pubkey-secret"
# Tries keys: pubkey, public-key, jwt.pub, tls.crt
#
# 2. Explicit key (no variations):
# easyhaproxy.plugin.jwt_validator.k8s_secret.pubkey: "jwt-pubkey-secret/rsa-public-key"
# Only tries key: rsa-public-key
---
# NOTE: Secrets should be created separately using your generated JWT keys
# See instructions at the top of this file
# The test fixture creates these secrets automatically
---
# Deployment for API service
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: byjg/static-httpserver
ports:
- containerPort: 8080
env:
- name: TITLE
value: "Protected API - JWT Required"
---
# Service to be protected with JWT
apiVersion: v1
kind: Service
metadata:
name: api-service
namespace: default
spec:
selector:
app: api
ports:
- port: 8080
targetPort: 8080
---
# Ingress Example 1: Auto-detect key (uses standard key name "pubkey")
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress-jwt-auto
namespace: default
annotations:
# Enable JWT validator plugin
easyhaproxy.plugins: "jwt_validator"
# JWT validator configuration
easyhaproxy.plugin.jwt_validator.algorithm: "RS256"
easyhaproxy.plugin.jwt_validator.issuer: "https://auth.example.com/"
easyhaproxy.plugin.jwt_validator.audience: "https://api.example.com"
# Auto-detect: tries pubkey, public-key, jwt.pub, tls.crt
easyhaproxy.plugin.jwt_validator.k8s_secret.pubkey: "jwt-pubkey-secret"
spec:
ingressClassName: easyhaproxy
rules:
- host: api.example.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 8080
---
# Ingress Example 2: Explicit key (uses custom key name "rsa-public-key")
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress-jwt-explicit
namespace: default
annotations:
# Enable JWT validator plugin
easyhaproxy.plugins: "jwt_validator"
# JWT validator configuration
easyhaproxy.plugin.jwt_validator.algorithm: "RS256"
easyhaproxy.plugin.jwt_validator.issuer: "https://auth.example.com/"
easyhaproxy.plugin.jwt_validator.audience: "https://api.example.com"
# Explicit key: only tries "rsa-public-key" from the secret
easyhaproxy.plugin.jwt_validator.k8s_secret.pubkey: "jwt-custom-secret/rsa-public-key"
# Optional: Protect only specific paths
# easyhaproxy.plugin.jwt_validator.paths: "/api,/admin"
# Optional: Allow anonymous access (JWT validated only if present)
# easyhaproxy.plugin.jwt_validator.allow_anonymous: "true"
spec:
ingressClassName: easyhaproxy
rules:
- host: api-custom.example.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 8080

View file

@ -2,6 +2,24 @@
# EXAMPLE: JWT Validator Plugin for Kubernetes
# ==============================================================================
#
# JWT PUBLIC KEY CONFIGURATION OPTIONS:
# There are three ways to provide the JWT public key:
#
# 1. pubkey_path - Mount a file and reference the path (requires ConfigMap or Volume)
# easyhaproxy.plugin.jwt_validator.pubkey_path: "/etc/haproxy/jwt_keys/api_pubkey.pem"
#
# 2. k8s_secret.pubkey - Reference a Kubernetes secret (RECOMMENDED)
# Auto-detect key (tries common variations):
# easyhaproxy.plugin.jwt_validator.k8s_secret.pubkey: "my-jwt-secret"
# Explicit key (no variations):
# easyhaproxy.plugin.jwt_validator.k8s_secret.pubkey: "my-jwt-secret/custom-key-name"
# See jwt-validator-secret-example.yml for full example
#
# 3. pubkey - Inline base64-encoded key (for testing only, not recommended for production)
# easyhaproxy.plugin.jwt_validator.pubkey: "LS0tLS1CRUdJTi..."
#
# This example shows option #1 (pubkey_path) for backward compatibility
#
# WHAT THIS DEMONSTRATES:
# - JWT token validation for API protection in Kubernetes
# - RS256 algorithm signature verification

View file

@ -66,21 +66,23 @@ spec:
- host: example.org
http:
paths:
- backend:
- path: /
pathType: Prefix
backend:
service:
name: container-example
port:
number: 8080
pathType: ImplementationSpecific
- host: www.example.org
http:
paths:
- backend:
- path: /
pathType: Prefix
backend:
service:
name: container-example
port:
number: 8080
pathType: ImplementationSpecific
---
apiVersion: v1

View file

@ -71,12 +71,13 @@ spec:
- host: host2.local
http:
paths:
- backend:
- path: /
pathType: Prefix
backend:
service:
name: tls-example
port:
number: 8080
pathType: ImplementationSpecific
---
apiVersion: v1

View file

@ -0,0 +1,169 @@
#!/bin/bash
set -e
# Colors for output
GREEN='\033[0;32m'
BLUE='\033[0;34m'
RED='\033[0;31m'
NC='\033[0m' # No Color
CLUSTER_NAME="easyhaproxy-test"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BIN_DIR="${SCRIPT_DIR}/.kind"
KIND_BIN="${BIN_DIR}/kind"
KUBECTL_BIN="${BIN_DIR}/kubectl"
HELM_BIN="${BIN_DIR}/helm"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
# Port configuration (matches test_kubernetes.py)
HTTP_PORT=10080
HTTPS_PORT=10443
STATS_PORT=11936
echo -e "${BLUE}[1/9] Setting up kind cluster '${CLUSTER_NAME}'...${NC}"
# Ensure kind is installed
if [ ! -f "${KIND_BIN}" ]; then
echo "Installing kind locally..."
mkdir -p "${BIN_DIR}"
curl -Lo "${KIND_BIN}" "https://kind.sigs.k8s.io/dl/v0.20.0/kind-linux-amd64"
chmod +x "${KIND_BIN}"
echo -e "${GREEN}✓ kind installed to ${KIND_BIN}${NC}"
fi
# Ensure kubectl is installed
if ! command -v kubectl &> /dev/null; then
if [ ! -f "${KUBECTL_BIN}" ]; then
echo "Installing kubectl locally..."
mkdir -p "${BIN_DIR}"
VERSION=$(curl -L -s https://dl.k8s.io/release/stable.txt)
curl -Lo "${KUBECTL_BIN}" "https://dl.k8s.io/release/${VERSION}/bin/linux/amd64/kubectl"
chmod +x "${KUBECTL_BIN}"
echo -e "${GREEN}✓ kubectl installed to ${KUBECTL_BIN}${NC}"
fi
KUBECTL="${KUBECTL_BIN}"
else
KUBECTL="kubectl"
fi
# Ensure helm is installed
if ! command -v helm &> /dev/null; then
if [ ! -f "${HELM_BIN}" ]; then
echo "Installing helm locally..."
mkdir -p "${BIN_DIR}"
HELM_VERSION="v3.13.3"
HELM_TAR="${BIN_DIR}/helm.tar.gz"
curl -Lo "${HELM_TAR}" "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz"
tar -xzf "${HELM_TAR}" -C "${BIN_DIR}" --strip-components=1 linux-amd64/helm
rm "${HELM_TAR}"
chmod +x "${HELM_BIN}"
echo -e "${GREEN}✓ helm installed to ${HELM_BIN}${NC}"
fi
HELM="${HELM_BIN}"
else
HELM="helm"
fi
# Check if cluster already exists
echo -e "${BLUE}[1/9] Checking for existing cluster...${NC}"
if ${KIND_BIN} get clusters 2>/dev/null | grep -q "^${CLUSTER_NAME}$"; then
echo -e "${BLUE}Cluster '${CLUSTER_NAME}' already exists, deleting it first...${NC}"
${KIND_BIN} delete cluster --name "${CLUSTER_NAME}"
fi
# Create cluster config
echo -e "${BLUE}[1/9] Writing cluster config...${NC}"
CLUSTER_CONFIG="${BIN_DIR}/cluster-config.yaml"
mkdir -p "${BIN_DIR}"
cat > "${CLUSTER_CONFIG}" <<EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 80
hostPort: ${HTTP_PORT}
protocol: TCP
- containerPort: 443
hostPort: ${HTTPS_PORT}
protocol: TCP
- containerPort: 1936
hostPort: ${STATS_PORT}
protocol: TCP
EOF
# Create cluster
echo -e "${BLUE}[2/9] Creating kind cluster (this may take 1-2 minutes)...${NC}"
${KIND_BIN} create cluster --name "${CLUSTER_NAME}" --config "${CLUSTER_CONFIG}"
# Set kubectl context
echo -e "${BLUE}[3/9] Setting kubectl context...${NC}"
${KUBECTL} config use-context "kind-${CLUSTER_NAME}"
# Wait for nodes to be ready
echo -e "${BLUE}[3/9] Waiting for cluster nodes to be ready...${NC}"
${KUBECTL} wait --for=condition=Ready nodes --all --timeout=30s
echo -e "${GREEN}✓ kind cluster '${CLUSTER_NAME}' is ready${NC}"
# Build and load local EasyHAProxy image
echo -e "${BLUE}[4/9] Building local EasyHAProxy image (may take 30-60s)...${NC}"
docker build -t byjg/easy-haproxy:local \
-f "${PROJECT_ROOT}/build/Dockerfile" \
"${PROJECT_ROOT}"
echo -e "${BLUE}[5/9] Loading image into kind cluster (may take 10-20s)...${NC}"
${KIND_BIN} load docker-image byjg/easy-haproxy:local --name "${CLUSTER_NAME}"
# Generate EasyHAProxy manifest using Helm
echo -e "${BLUE}[6/9] Generating EasyHAProxy manifest from Helm...${NC}"
HELM_DIR="${PROJECT_ROOT}/helm"
MANIFEST_PATH="${BIN_DIR}/easyhaproxy-local.yml"
${HELM} template ingress "${HELM_DIR}/easyhaproxy" \
--namespace easyhaproxy \
--set service.create=false \
--set image.tag=local \
--set image.pullPolicy=Never \
> "${MANIFEST_PATH}"
# Install EasyHAProxy
echo -e "${BLUE}[7/9] Creating easyhaproxy namespace...${NC}"
${KUBECTL} create namespace easyhaproxy
echo -e "${BLUE}[7/9] Applying EasyHAProxy manifest...${NC}"
${KUBECTL} apply -f "${MANIFEST_PATH}"
# Label the control-plane node
echo -e "${BLUE}[8/9] Labeling control-plane node...${NC}"
${KUBECTL} label nodes "${CLUSTER_NAME}-control-plane" \
"easyhaproxy/node=master" --overwrite
# Wait for EasyHAProxy to be ready
echo -e "${BLUE}[9/9] Waiting for EasyHAProxy pods to be ready...${NC}"
if ${KUBECTL} wait --for=condition=Ready pods \
-n easyhaproxy -l "app.kubernetes.io/name=easyhaproxy" \
--timeout=30s 2>/dev/null; then
echo -e "${GREEN}✓ EasyHAProxy pods are ready${NC}"
else
echo -e "${RED}✗ Pods not ready within 30s. Checking status...${NC}"
${KUBECTL} get pods -n easyhaproxy -o wide
echo -e "\n${BLUE}Events:${NC}"
${KUBECTL} get events -n easyhaproxy --sort-by=.lastTimestamp
exit 1
fi
echo -e "${GREEN}✓ All setup complete! Cluster is ready.${NC}"
echo ""
echo -e "${BLUE}Cluster Information:${NC}"
echo -e " Cluster name: ${CLUSTER_NAME}"
echo -e " HTTP port: localhost:${HTTP_PORT}"
echo -e " HTTPS port: localhost:${HTTPS_PORT}"
echo -e " Stats port: localhost:${STATS_PORT}"
echo ""
echo -e "${BLUE}Useful commands:${NC}"
echo -e " Apply example ingress: ${KUBECTL} apply -f ${SCRIPT_DIR}/service.yml"
echo -e " Check EasyHAProxy logs: ${KUBECTL} logs -n easyhaproxy -l app.kubernetes.io/name=easyhaproxy -f"
echo -e " Test with curl: curl -H 'Host: example.org' http://localhost:${HTTP_PORT}"
echo -e " Delete cluster: ${SCRIPT_DIR}/teardown-cluster.sh"
echo ""

View file

@ -0,0 +1,39 @@
#!/bin/bash
# Colors for output
GREEN='\033[0;32m'
BLUE='\033[0;34m'
RED='\033[0;31m'
NC='\033[0m' # No Color
CLUSTER_NAME="easyhaproxy-test"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BIN_DIR="${SCRIPT_DIR}/.kind"
KIND_BIN="${BIN_DIR}/kind"
# Check if kind binary exists
if [ ! -f "${KIND_BIN}" ]; then
# Try to use system kind
if command -v kind &> /dev/null; then
KIND_BIN="kind"
else
echo -e "${RED}✗ kind binary not found. Cannot delete cluster.${NC}"
echo " Cluster may not exist or kind is not installed."
exit 1
fi
fi
# Check if cluster exists
if ! ${KIND_BIN} get clusters 2>/dev/null | grep -q "^${CLUSTER_NAME}$"; then
echo -e "${BLUE}Cluster '${CLUSTER_NAME}' does not exist. Nothing to delete.${NC}"
exit 0
fi
echo -e "${BLUE}Deleting kind cluster '${CLUSTER_NAME}'...${NC}"
if ${KIND_BIN} delete cluster --name "${CLUSTER_NAME}"; then
echo -e "${GREEN}✓ Cluster deleted successfully${NC}"
else
echo -e "${RED}✗ Failed to delete cluster${NC}"
exit 1
fi

File diff suppressed because it is too large Load diff