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:
parent
1fd5873dc9
commit
b34d7822e4
6 changed files with 139 additions and 14 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,4 @@ RUN chmod +x server.py
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
CMD ["python3", "server.py"]
|
CMD ["python3", "server.py"]
|
||||||
66
examples/fixtures/header-echo/README.md
Normal file
66
examples/fixtures/header-echo/README.md
Normal 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`
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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'
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue