1
0
Fork 0

Remove Python app example and replace with header-echo server for testing

- Deleted the `/examples/docker/python-app` directory and associated files.
- Integrated a custom `header-echo` server for Kubernetes and Docker Compose tests.
- Updated Kubernetes and Docker Compose configurations to use the new `header-echo-server:test` image.
- Enhanced Kubernetes integration tests to validate JSON responses and Cloudflare IP translation behavior.
This commit is contained in:
Joao Gilberto Magalhaes 2026-02-12 16:23:01 -05:00
parent 1fd5873dc9
commit b34d7822e4
6 changed files with 139 additions and 14 deletions

View file

@ -66,7 +66,7 @@ services:
# Web application behind Cloudflare (header-echo server for testing) # Web application behind Cloudflare (header-echo server for testing)
webapp: webapp:
build: ./python-app build: ../fixtures/header-echo
labels: labels:
easyhaproxy.http.host: myapp.local easyhaproxy.http.host: myapp.local
easyhaproxy.http.port: 80 easyhaproxy.http.port: 80

View file

@ -8,4 +8,4 @@ RUN chmod +x server.py
EXPOSE 8080 EXPOSE 8080
CMD ["python3", "server.py"] CMD ["python3", "server.py"]

View file

@ -0,0 +1,66 @@
# Header Echo Server - Test Fixture
A lightweight Python HTTP server that echoes all request headers as JSON. Used for testing HAProxy plugins that manipulate headers and client IPs.
## Purpose
This test fixture is used by both Docker Compose and Kubernetes test suites to verify:
- Header manipulation (e.g., X-Forwarded-For, CF-Connecting-IP)
- IP restoration plugins (Cloudflare, custom CDN integrations)
- Request routing and backend visibility
## Usage
### Docker Compose
```yaml
services:
webapp:
build: ../fixtures/header-echo
ports:
- "8080:8080"
```
### Kubernetes
```bash
# Build and load into kind cluster
docker build -t header-echo-server:test .
kind load docker-image header-echo-server:test --name your-cluster
# Use in deployment
spec:
containers:
- name: webapp
image: header-echo-server:test
imagePullPolicy: Never
```
### Manual Testing
```bash
# Start the server
python3 server.py
# Test it
curl http://localhost:8080
# Returns JSON with all headers, client IP, and X-Forwarded-For value
```
## Response Format
```json
{
"headers": {
"Host": "localhost:8080",
"User-Agent": "curl/7.81.0",
"Accept": "*/*"
},
"client_ip": "127.0.0.1",
"x_forwarded_for": "NOT SET"
}
```
## Used By
- `examples/docker/docker-compose-cloudflare.yml`
- `examples/docker/test_docker_compose.py::TestCloudflare`
- `examples/kubernetes/cloudflare.yml`
- `examples/kubernetes/test_kubernetes.py::TestCloudflare`

View file

@ -30,4 +30,4 @@ if __name__ == '__main__':
port = 8080 port = 8080
server = HTTPServer(('0.0.0.0', port), HeaderEchoHandler) server = HTTPServer(('0.0.0.0', port), HeaderEchoHandler)
print(f'Header echo server running on port {port}...') print(f'Header echo server running on port {port}...')
server.serve_forever() server.serve_forever()

View file

@ -48,10 +48,11 @@
# # Test via port-forward # # Test via port-forward
# kubectl port-forward -n easyhaproxy deployment/easyhaproxy 8080:80 # kubectl port-forward -n easyhaproxy deployment/easyhaproxy 8080:80
# curl -H "Host: myapp.example.local" http://localhost:8080 # curl -H "Host: myapp.example.local" http://localhost:8080
# # Expected: 200 OK with "App Behind Cloudflare" # # Expected: 200 OK with JSON response containing headers, client_ip, and x_forwarded_for
# #
# # In production behind Cloudflare, the plugin will restore real client IPs # # Test IP translation with CF-Connecting-IP header
# # from the CF-Connecting-IP header # curl -H "Host: myapp.example.local" -H "CF-Connecting-IP: 1.2.3.4" http://localhost:8080
# # Expected: x_forwarded_for should be "1.2.3.4"
# ``` # ```
# #
# CLEAN UP: # CLEAN UP:
@ -93,12 +94,10 @@ spec:
spec: spec:
containers: containers:
- name: webapp - name: webapp
image: byjg/static-httpserver image: header-echo-server:test
imagePullPolicy: Never
ports: ports:
- containerPort: 8080 - containerPort: 8080
env:
- name: TITLE
value: "App Behind Cloudflare"
resources: resources:
limits: limits:
cpu: '0.1' cpu: '0.1'

View file

@ -840,9 +840,28 @@ def k8s_jwt_validator_secret(kind_cluster) -> Generator[dict, None, None]:
@pytest.fixture @pytest.fixture
def k8s_cloudflare(kind_cluster) -> Generator[str, None, None]: def k8s_cloudflare(kind_cluster, kind_cmd) -> Generator[str, None, None]:
"""Fixture for cloudflare.yml with base64-encoded IP list""" """Fixture for cloudflare.yml with base64-encoded IP list"""
kubectl_cmd = kind_cluster["kubectl"] kubectl_cmd = kind_cluster["kubectl"]
cluster_name = kind_cluster["name"]
# Build header-echo server image locally
header_echo_dir = BASE_DIR.parent / "fixtures" / "header-echo"
print(" → Building header-echo-server:test image...")
subprocess.run(
["docker", "build", "-t", "header-echo-server:test", str(header_echo_dir)],
check=True,
capture_output=True
)
# Load image into kind cluster
print(" → Loading header-echo-server:test into kind cluster...")
subprocess.run(
[kind_cmd, "load", "docker-image", "header-echo-server:test",
"--name", cluster_name],
check=True,
capture_output=True
)
# Create a modified cloudflare manifest with base64-encoded test IPs # Create a modified cloudflare manifest with base64-encoded test IPs
# Include 127.0.0.1 and Docker/kind network ranges so test requests work # Include 127.0.0.1 and Docker/kind network ranges so test requests work
@ -1881,7 +1900,7 @@ class TestCloudflare:
"Built-in Cloudflare IP found (ip_list should take precedence)" "Built-in Cloudflare IP found (ip_list should take precedence)"
def test_access_to_webapp(self, k8s_cloudflare): def test_access_to_webapp(self, k8s_cloudflare):
"""Test that the webapp is accessible via the Cloudflare ingress""" """Test that the webapp is accessible and returns JSON"""
kubectl = k8s_cloudflare kubectl = k8s_cloudflare
# Wait for EasyHAProxy to discover and configure the ingress # Wait for EasyHAProxy to discover and configure the ingress
@ -1898,8 +1917,49 @@ class TestCloudflare:
) )
assert result.returncode == 0, f"Curl failed with return code {result.returncode}" assert result.returncode == 0, f"Curl failed with return code {result.returncode}"
assert "App Behind Cloudflare" in result.stdout, \
f"Expected 'App Behind Cloudflare' in response, got: {result.stdout}" # Parse JSON response
data = json.loads(result.stdout)
# Verify JSON structure
assert "headers" in data, "Response should contain 'headers' field"
assert "client_ip" in data, "Response should contain 'client_ip' field"
assert "x_forwarded_for" in data, "Response should contain 'x_forwarded_for' field"
def test_cloudflare_ip_translation_works(self, k8s_cloudflare):
"""Test that Cloudflare plugin actually translates CF-Connecting-IP to X-Forwarded-For"""
kubectl = k8s_cloudflare
# Wait for EasyHAProxy to be ready
assert wait_for_easyhaproxy_discovery(kubectl, "myapp.example.local", timeout=30), \
"EasyHAProxy did not become ready within 30 seconds"
# Send request with CF-Connecting-IP header
test_ip = "203.0.113.50"
result = subprocess.run(
["curl", "-s",
"-H", "Host: myapp.example.local",
"-H", f"CF-Connecting-IP: {test_ip}",
f"http://localhost:{HTTP_PORT}"],
capture_output=True,
text=True,
timeout=10
)
assert result.returncode == 0, f"Curl failed"
# Parse JSON response from header-echo server
data = json.loads(result.stdout)
# VERIFY: X-Forwarded-For was set to the CF-Connecting-IP value
# This proves the Cloudflare plugin actually works, not just that config exists
assert data['x_forwarded_for'] == test_ip, \
f"Expected X-Forwarded-For to be '{test_ip}' (from CF-Connecting-IP), " \
f"got '{data['x_forwarded_for']}'. Cloudflare IP translation NOT working!"
# Verify client_ip is still the HAProxy/ingress IP (connection doesn't change)
assert data['client_ip'] != test_ip, \
f"client_ip should be HAProxy pod IP, not the translated IP"
# ============================================================================= # =============================================================================