2023-04-21 17:14:03 +00:00
|
|
|
#!/usr/bin/env python
|
|
|
|
import asyncio
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
import requests
|
2023-04-21 18:54:37 +00:00
|
|
|
import websockets
|
2023-04-21 17:14:03 +00:00
|
|
|
from websockets.client import connect, WebSocketClientProtocol
|
|
|
|
from conftest import CfdModes
|
|
|
|
from constants import MAX_RETRIES, BACKOFF_SECS
|
|
|
|
from retrying import retry
|
|
|
|
from cli import CloudflaredCli
|
|
|
|
from util import LOGGER, start_cloudflared, write_config, wait_tunnel_ready
|
|
|
|
|
|
|
|
class TestTail:
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
async def test_start_stop_streaming(self, tmp_path, component_tests_config):
|
|
|
|
"""
|
|
|
|
Validates that a websocket connection to management.argotunnel.com/logs can be opened
|
|
|
|
with the access token and start and stop streaming on-demand.
|
|
|
|
"""
|
|
|
|
print("test_start_stop_streaming")
|
|
|
|
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
|
|
|
LOGGER.debug(config)
|
|
|
|
config_path = write_config(tmp_path, config.full_config)
|
|
|
|
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True):
|
2023-04-24 16:39:26 +00:00
|
|
|
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
|
2023-04-21 17:14:03 +00:00
|
|
|
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
|
|
|
url = cfd_cli.get_management_wsurl("logs", config, config_path)
|
|
|
|
async with connect(url, open_timeout=5, close_timeout=3) as websocket:
|
|
|
|
await websocket.send('{"type": "start_streaming"}')
|
|
|
|
await websocket.send('{"type": "stop_streaming"}')
|
|
|
|
await websocket.send('{"type": "start_streaming"}')
|
|
|
|
await websocket.send('{"type": "stop_streaming"}')
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
async def test_streaming_logs(self, tmp_path, component_tests_config):
|
|
|
|
"""
|
|
|
|
Validates that a streaming logs connection will stream logs
|
|
|
|
"""
|
|
|
|
print("test_streaming_logs")
|
|
|
|
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
|
|
|
LOGGER.debug(config)
|
|
|
|
config_path = write_config(tmp_path, config.full_config)
|
|
|
|
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True):
|
2023-04-24 16:39:26 +00:00
|
|
|
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
|
2023-04-21 17:14:03 +00:00
|
|
|
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
|
|
|
url = cfd_cli.get_management_wsurl("logs", config, config_path)
|
|
|
|
async with connect(url, open_timeout=5, close_timeout=5) as websocket:
|
|
|
|
# send start_streaming
|
|
|
|
await websocket.send('{"type": "start_streaming"}')
|
|
|
|
# send some http requests to the tunnel to trigger some logs
|
2023-04-24 16:39:26 +00:00
|
|
|
await generate_and_validate_http_events(websocket, config.get_url(), 10)
|
2023-04-21 17:14:03 +00:00
|
|
|
# send stop_streaming
|
|
|
|
await websocket.send('{"type": "stop_streaming"}')
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
async def test_streaming_logs_filters(self, tmp_path, component_tests_config):
|
|
|
|
"""
|
|
|
|
Validates that a streaming logs connection will stream logs
|
|
|
|
but not http when filters applied.
|
|
|
|
"""
|
|
|
|
print("test_streaming_logs_filters")
|
|
|
|
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
|
|
|
LOGGER.debug(config)
|
|
|
|
config_path = write_config(tmp_path, config.full_config)
|
|
|
|
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True):
|
2023-04-24 16:39:26 +00:00
|
|
|
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
|
2023-04-21 17:14:03 +00:00
|
|
|
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
|
|
|
url = cfd_cli.get_management_wsurl("logs", config, config_path)
|
|
|
|
async with connect(url, open_timeout=5, close_timeout=5) as websocket:
|
2023-04-24 16:39:26 +00:00
|
|
|
# send start_streaming with tcp logs only
|
2023-04-21 17:14:03 +00:00
|
|
|
await websocket.send(json.dumps({
|
|
|
|
"type": "start_streaming",
|
|
|
|
"filters": {
|
2023-04-24 16:39:26 +00:00
|
|
|
"events": ["tcp"],
|
|
|
|
"level": "debug"
|
2023-04-21 17:14:03 +00:00
|
|
|
}
|
|
|
|
}))
|
|
|
|
# don't expect any http logs
|
|
|
|
await generate_and_validate_no_log_event(websocket, config.get_url())
|
|
|
|
# send stop_streaming
|
|
|
|
await websocket.send('{"type": "stop_streaming"}')
|
2023-04-24 16:39:26 +00:00
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
async def test_streaming_logs_sampling(self, tmp_path, component_tests_config):
|
|
|
|
"""
|
|
|
|
Validates that a streaming logs connection will stream logs with sampling.
|
|
|
|
"""
|
|
|
|
print("test_streaming_logs_sampling")
|
|
|
|
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
|
|
|
LOGGER.debug(config)
|
|
|
|
config_path = write_config(tmp_path, config.full_config)
|
|
|
|
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True):
|
|
|
|
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
|
|
|
|
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
|
|
|
url = cfd_cli.get_management_wsurl("logs", config, config_path)
|
|
|
|
async with connect(url, open_timeout=5, close_timeout=5) as websocket:
|
|
|
|
# send start_streaming with info logs only
|
|
|
|
await websocket.send(json.dumps({
|
|
|
|
"type": "start_streaming",
|
|
|
|
"filters": {
|
|
|
|
"sampling": 0.5
|
|
|
|
}
|
|
|
|
}))
|
|
|
|
# don't expect any http logs
|
|
|
|
count = await generate_and_validate_http_events(websocket, config.get_url(), 10)
|
|
|
|
assert count < (10 * 2) # There are typically always two log lines for http requests (request and response)
|
|
|
|
# send stop_streaming
|
|
|
|
await websocket.send('{"type": "stop_streaming"}')
|
2023-04-21 17:14:03 +00:00
|
|
|
|
2023-04-21 18:54:37 +00:00
|
|
|
@pytest.mark.asyncio
|
|
|
|
async def test_streaming_logs_actor_override(self, tmp_path, component_tests_config):
|
|
|
|
"""
|
|
|
|
Validates that a streaming logs session can be overriden by the same actor
|
|
|
|
"""
|
|
|
|
print("test_streaming_logs_actor_override")
|
|
|
|
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
|
|
|
LOGGER.debug(config)
|
|
|
|
config_path = write_config(tmp_path, config.full_config)
|
|
|
|
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True):
|
2023-04-24 16:39:26 +00:00
|
|
|
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
|
2023-04-21 18:54:37 +00:00
|
|
|
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
|
|
|
url = cfd_cli.get_management_wsurl("logs", config, config_path)
|
|
|
|
task = asyncio.ensure_future(start_streaming_to_be_remotely_closed(url))
|
|
|
|
override_task = asyncio.ensure_future(start_streaming_override(url))
|
|
|
|
await asyncio.wait([task, override_task])
|
|
|
|
assert task.exception() == None, task.exception()
|
|
|
|
assert override_task.exception() == None, override_task.exception()
|
|
|
|
|
|
|
|
async def start_streaming_to_be_remotely_closed(url):
|
|
|
|
async with connect(url, open_timeout=5, close_timeout=5) as websocket:
|
|
|
|
try:
|
|
|
|
await websocket.send(json.dumps({"type": "start_streaming"}))
|
|
|
|
await asyncio.sleep(10)
|
|
|
|
assert websocket.closed, "expected this request to be forcibly closed by the override"
|
|
|
|
except websockets.ConnectionClosed:
|
|
|
|
# we expect the request to be closed
|
|
|
|
pass
|
|
|
|
|
|
|
|
async def start_streaming_override(url):
|
|
|
|
# wait for the first connection to be established
|
|
|
|
await asyncio.sleep(1)
|
|
|
|
async with connect(url, open_timeout=5, close_timeout=5) as websocket:
|
|
|
|
await websocket.send(json.dumps({"type": "start_streaming"}))
|
|
|
|
await asyncio.sleep(1)
|
|
|
|
await websocket.send(json.dumps({"type": "stop_streaming"}))
|
|
|
|
await asyncio.sleep(1)
|
2023-04-21 17:14:03 +00:00
|
|
|
|
|
|
|
# Every http request has two log lines sent
|
2023-04-24 16:39:26 +00:00
|
|
|
async def generate_and_validate_http_events(websocket: WebSocketClientProtocol, url: str, count_send: int):
|
|
|
|
for i in range(count_send):
|
|
|
|
send_request(url)
|
|
|
|
# There are typically always two log lines for http requests (request and response)
|
|
|
|
count = 0
|
|
|
|
while True:
|
|
|
|
try:
|
|
|
|
req_line = await asyncio.wait_for(websocket.recv(), 2)
|
|
|
|
log_line = json.loads(req_line)
|
|
|
|
assert log_line["type"] == "logs"
|
|
|
|
assert log_line["logs"][0]["event"] == "http"
|
|
|
|
count += 1
|
|
|
|
except asyncio.TimeoutError:
|
|
|
|
# ignore timeout from waiting for recv
|
|
|
|
break
|
|
|
|
return count
|
2023-04-21 17:14:03 +00:00
|
|
|
|
|
|
|
# Every http request has two log lines sent
|
|
|
|
async def generate_and_validate_no_log_event(websocket: WebSocketClientProtocol, url: str):
|
|
|
|
send_request(url)
|
|
|
|
try:
|
|
|
|
# wait for 5 seconds and make sure we hit the timeout and not recv any events
|
|
|
|
req_line = await asyncio.wait_for(websocket.recv(), 5)
|
|
|
|
assert req_line == None, "expected no logs for the specified filters"
|
|
|
|
except asyncio.TimeoutError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
|
|
|
|
def send_request(url, headers={}):
|
|
|
|
with requests.Session() as s:
|
|
|
|
resp = s.get(url, timeout=BACKOFF_SECS, headers=headers)
|
|
|
|
assert resp.status_code == 200, f"{url} returned {resp}"
|
|
|
|
return resp.status_code == 200
|