Skip to content

Commit fcac7e3

Browse files
committed
Add basic joblogs integration tests (#33, PR #57)
1 parent 6b7bb8b commit fcac7e3

11 files changed

Lines changed: 321 additions & 10 deletions

.github/workflows/test.yml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ jobs:
5050
run: |
5151
for test in scrapyd_k8s/tests/integration/test_*.py; do
5252
echo; echo "# $test"
53+
localconfig=`echo "$test" | sed 's/\.py$/\.local.sh/'`
54+
[ -x "$localconfig" ] && "$localconfig" up || true
5355
# run scrapyd-k8s with test-specific configuration file
5456
cfg=`echo "$test" | sed 's/\.py$/.conf/'`
5557
python -m scrapyd_k8s -c scrapyd_k8s.sample-docker.conf -c "$cfg" &
@@ -59,6 +61,8 @@ jobs:
5961
pytest -vv --color=yes "$test"
6062
# stop scrapyd-k8s again
6163
kill %1; wait %1 || true
64+
# cleanup
65+
[ -x "$localconfig" ] && "$localconfig" down || true
6266
done
6367
6468
test-manifest:
@@ -128,15 +132,13 @@ jobs:
128132
129133
- name: Run tests
130134
run: |
131-
# setup for in-cluster k8s
132-
# for each integration test file
133135
for test in scrapyd_k8s/tests/integration/test_*.py; do
134136
echo; echo "# $test"
135137
# run scrapyd-k8s with test-specific configuration file, run k8s patch if available
136138
cfg=`echo "$test" | sed 's/\.py$/.conf/'`
137139
kubectl create cm scrapyd-k8s-testcfg --from-file=scrapyd_k8s.test.conf="$cfg"
138140
k8sconfig=`echo "$test" | sed 's/\.py$/\.k8s.sh/'`
139-
[ -x "$k8sconfig" ] && "$k8sconfig" up
141+
[ -x "$k8sconfig" ] && "$k8sconfig" up || true
140142
kubectl scale --replicas=1 deploy/scrapyd-k8s
141143
# wait for scrapyd-k8s to become ready
142144
kubectl wait --for=condition=Available deploy/scrapyd-k8s --timeout=60s
@@ -153,7 +155,7 @@ jobs:
153155
kubectl scale --replicas=0 deploy/scrapyd-k8s
154156
kubectl wait --for=delete pod -l app.kubernetes.io/name=scrapyd-k8s --timeout=90s
155157
kubectl delete cm scrapyd-k8s-testcfg --wait
156-
[ -x "$k8sconfig" ] && "$k8sconfig" down
158+
[ -x "$k8sconfig" ] && "$k8sconfig" down || true
157159
done
158160
159161
test-k8s:
@@ -188,6 +190,8 @@ jobs:
188190
run: |
189191
for test in scrapyd_k8s/tests/integration/test_*.py; do
190192
echo "# $test"
193+
localconfig=`echo "$test" | sed 's/\.py$/\.local.sh/'`
194+
[ -x "$localconfig" ] && "$localconfig" up || true
191195
# run scrapyd-k8s with test-specific configuration file
192196
cfg=`echo "$test" | sed 's/\.py$/.conf/'`
193197
[ -e "$cfg" ] || cfg=/dev/null
@@ -201,4 +205,6 @@ jobs:
201205
pytest -vv --color=yes "$test"
202206
# stop scrapyd-k8s again
203207
kill %1; wait %1 || true
208+
# cleanup
209+
[ -x "$localconfig" ] && "$localconfig" down || true
204210
done

kubernetes.yaml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,6 @@ data:
104104
requests_memory = 0.2G
105105
limits_cpu = 0.8
106106
limits_memory = 0.5G
107-
108-
[joblogs]
109-
logs_dir = /data/joblogs
110107
---
111108
apiVersion: v1
112109
kind: Secret
@@ -169,7 +166,7 @@ rules:
169166
verbs: ["get", "list", "watch"]
170167
- apiGroups: [""]
171168
resources: ["pods/exec"]
172-
verbs: ["get"]
169+
verbs: ["get", "create"]
173170
- apiGroups: [""]
174171
resources: ["pods/log"]
175172
verbs: ["get"]

requirements.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
docker>=5.0.0
2-
kubernetes>=27.2.0 # introduction of suspend in jobspec
2+
kubernetes>=27.2.0,<36.0.0 # introduction of suspend in jobspec; 36 has issues with joblogs
33
flask>=2.0.0
44
natsort>=8.0.0
55
Flask-BasicAuth>=0.2.0
66
MarkupSafe>=2.1.5
77
apache-libcloud>=3.8.0
8-
brotli>=1.1.0 # only if you plan to use brotli compression for log files in joblogs module
8+
fasteners>=0.20 # for libcloud local provider
9+
brotli>=1.1.0 # only if you plan to use brotli compression for log files in joblogs module
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# additional scrapyd-k8s configuration for test_joblogs.py
2+
[joblogs]
3+
logs_dir = /tmp/joblogs/live
4+
storage_provider = local
5+
container_name = a
6+
7+
[joblogs.storage.local]
8+
key = /tmp/joblogs/container
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/bin/sh
2+
# Kubernetes cluster preparation for test_joblogs.py
3+
# Called with "up" argument on setup, with "down" argument afterwards.
4+
#
5+
# Creates the joblogs container path before starting the pod.
6+
# Tightly bound to kubernetes.yaml.
7+
# Assumes no minikind mounts are active.
8+
# Assumes scrapy container runs as user nobody (uid 65534).
9+
10+
if [ "$1" = "up" ]; then
11+
mkdir -p /tmp/joblogs/container/a
12+
mkdir -p /tmp/joblogs/live
13+
minikube mount /tmp/joblogs:/tmp/joblogs --uid=65534 &
14+
kubectl patch deploy scrapyd-k8s --type=json -p='[
15+
{
16+
"op": "add",
17+
"path": "/spec/template/spec/volumes/0",
18+
"value": {
19+
"name": "joblogs",
20+
"hostPath": {
21+
"path": "/tmp/joblogs"
22+
}
23+
}
24+
},
25+
{
26+
"op": "add",
27+
"path": "/spec/template/spec/containers/0/volumeMounts/0",
28+
"value": {
29+
"mountPath": "/tmp/joblogs",
30+
"name": "joblogs"
31+
}
32+
},
33+
{
34+
"op": "add",
35+
"path": "/spec/template/spec/securityContext/fsUser",
36+
"value": 65534
37+
}
38+
]'
39+
elif [ "$1" = "down" ]; then
40+
kubectl patch deploy scrapyd-k8s --type=json -p='[
41+
{
42+
"op": "remove",
43+
"path": "/spec/template/spec/volumes/0"
44+
},
45+
{
46+
"op": "remove",
47+
"path": "/spec/template/spec/containers/0/volumeMounts/0"
48+
},
49+
{
50+
"op": "remove",
51+
"path": "/spec/template/spec/securityContext/fsUser"
52+
}
53+
]'
54+
minikube mount --kill=true
55+
rm -Rf /tmp/joblogs
56+
else
57+
echo "Usage: $0 up|down"
58+
exit 1
59+
fi
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/bin/sh
2+
# Environment preparation for test_joblogs.py (all test configurations).
3+
# Called with "up" argument on setup, with "down" argument afterwards.
4+
#
5+
# Creates the joblogs container paths, as it is required to start scrapyd-k8s.
6+
7+
if [ "$1" = "up" ]; then
8+
mkdir -p /tmp/joblogs/container/a
9+
mkdir -p /tmp/joblogs/live
10+
elif [ "$1" = "down" ]; then
11+
rm -Rf /tmp/joblogs
12+
else
13+
echo "Usage: $0 up|down"
14+
exit 1
15+
fi
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
#!/usr/bin/env python3
2+
import os
3+
import pytest
4+
import requests
5+
import shutil
6+
import time
7+
8+
BASE_URL = os.getenv('TEST_BASE_URL', 'http://localhost:6800')
9+
RUN_PROJECT = os.getenv('TEST_RUN_PROJECT', 'example')
10+
RUN_VERSION = os.getenv('TEST_RUN_VERSION', 'latest')
11+
RUN_SPIDER = os.getenv('TEST_RUN_SPIDER', 'static')
12+
MAX_WAIT = int(os.getenv('TEST_MAX_WAIT', '6'))
13+
STATIC_SLEEP = float(os.getenv('TEST_STATIC_SLEEP', '2'))
14+
WITH_K8S = bool(os.getenv('TEST_WITH_K8S'))
15+
LOG_BASE = os.getenv('TEST_LOG_BASE', '/tmp/joblogs')
16+
17+
LOG_DIR_LIVE = os.path.join(LOG_BASE, 'live')
18+
LOG_DIR_CONT = os.path.join(LOG_BASE, 'container', 'a')
19+
20+
@pytest.mark.skipif(not WITH_K8S, reason="joblogs only implemented for Kubernetes")
21+
def test_live():
22+
# Schedule a job
23+
response = requests.post(BASE_URL + '/schedule.json', data={
24+
'project': RUN_PROJECT, 'spider': RUN_SPIDER, '_version': RUN_VERSION,
25+
'setting': 'STATIC_SLEEP=%d' % STATIC_SLEEP
26+
})
27+
assert_response_ok(response)
28+
jobid = response.json()['jobid']
29+
assert jobid is not None
30+
# Make sure the job is running
31+
listjobs_wait(jobid, 'running')
32+
33+
# Check that log file is present and has sensible content
34+
log_path_live = os.path.join(LOG_DIR_LIVE, jobid + '.txt')
35+
assert(os.path.isfile(log_path_live))
36+
with open(log_path_live, 'r') as f:
37+
assert('(bot: %s)' % RUN_PROJECT in f.read(2048))
38+
39+
# Wait until finished
40+
listjobs_wait(jobid, 'finished', max_wait=STATIC_SLEEP+MAX_WAIT)
41+
42+
@pytest.mark.skipif(not WITH_K8S, reason="joblogs only implemented for Kubernetes")
43+
def test_container():
44+
# Schedule a job
45+
response = requests.post(BASE_URL + '/schedule.json', data={
46+
'project': RUN_PROJECT, 'spider': RUN_SPIDER, '_version': RUN_VERSION,
47+
'setting': 'STATIC_SLEEP=%d' % STATIC_SLEEP
48+
})
49+
assert_response_ok(response)
50+
jobid = response.json()['jobid']
51+
assert jobid is not None
52+
53+
# Wait until finished, and a bit more to finish log processing
54+
listjobs_wait(jobid, 'finished', max_wait=STATIC_SLEEP+MAX_WAIT)
55+
time.sleep(1)
56+
57+
# Make sure we find logfile in container storage (configured as local storage)
58+
log_path_cont = os.path.join(LOG_DIR_CONT, 'logs', RUN_PROJECT, RUN_SPIDER, jobid + '.log')
59+
assert(os.path.isfile(log_path_cont))
60+
with open(log_path_cont, 'r') as f:
61+
log = f.read()
62+
# bot name in first lines
63+
assert('(bot: %s)' % RUN_PROJECT in log)
64+
# 'Spider finished' in last lines
65+
assert('Spider closed (finished)' in log)
66+
67+
def assert_response_ok(response):
68+
assert response.status_code == 200
69+
assert response.headers['Content-Type'] == 'application/json'
70+
assert response.json()['status'] == 'ok'
71+
72+
def listjobs_wait(jobid, state, max_wait=MAX_WAIT):
73+
started = time.monotonic()
74+
while time.monotonic() - started < max_wait:
75+
response = requests.get(BASE_URL + '/listjobs.json')
76+
assert_response_ok(response)
77+
for j in response.json()[state]:
78+
if j['id'] == jobid:
79+
return True
80+
time.sleep(0.5)
81+
assert False, 'Timeout waiting for job state change'
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# additional scrapyd-k8s configuration for test_joblogs.py
2+
[joblogs]
3+
logs_dir = /tmp/joblogs/live
4+
storage_provider = local
5+
container_name = a
6+
compression_method = gzip
7+
8+
[joblogs.storage.local]
9+
key = /tmp/joblogs/container
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/bin/sh
2+
# Kubernetes cluster preparation for test_joblogs_gzip.py
3+
# Called with "up" argument on setup, with "down" argument afterwards.
4+
#
5+
# Creates the joblogs container path before starting the pod.
6+
# Tightly bound to kubernetes.yaml
7+
# Assumes no minikind mounts are active.
8+
# Assumes scrapy container runs as user nobody (uid 65534).
9+
10+
if [ "$1" = "up" ]; then
11+
mkdir -p /tmp/joblogs/container/a
12+
mkdir -p /tmp/joblogs/live
13+
minikube mount /tmp/joblogs:/tmp/joblogs --uid=65534 &
14+
kubectl patch deploy scrapyd-k8s --type=json -p='[
15+
{
16+
"op": "add",
17+
"path": "/spec/template/spec/volumes/0",
18+
"value": {
19+
"name": "joblogs",
20+
"hostPath": {
21+
"path": "/tmp/joblogs"
22+
}
23+
}
24+
},
25+
{
26+
"op": "add",
27+
"path": "/spec/template/spec/containers/0/volumeMounts/0",
28+
"value": {
29+
"mountPath": "/tmp/joblogs",
30+
"name": "joblogs"
31+
}
32+
},
33+
{
34+
"op": "add",
35+
"path": "/spec/template/spec/securityContext/fsUser",
36+
"value": 65534
37+
}
38+
]'
39+
elif [ "$1" = "down" ]; then
40+
kubectl patch deploy scrapyd-k8s --type=json -p='[
41+
{
42+
"op": "remove",
43+
"path": "/spec/template/spec/volumes/0"
44+
},
45+
{
46+
"op": "remove",
47+
"path": "/spec/template/spec/containers/0/volumeMounts/0"
48+
},
49+
{
50+
"op": "remove",
51+
"path": "/spec/template/spec/securityContext/fsUser"
52+
}
53+
]'
54+
minikube mount --kill=true
55+
rm -Rf /tmp/joblogs
56+
else
57+
echo "Usage: $0 up|down"
58+
exit 1
59+
fi
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/bin/sh
2+
# Environment preparation for test_joblogs.py (all test configurations).
3+
# Called with "up" argument on setup, with "down" argument afterwards.
4+
#
5+
# Creates the joblogs container paths, as it is required to start scrapyd-k8s.
6+
7+
if [ "$1" = "up" ]; then
8+
mkdir -p /tmp/joblogs/container/a
9+
mkdir -p /tmp/joblogs/live
10+
elif [ "$1" = "down" ]; then
11+
rm -Rf /tmp/joblogs
12+
else
13+
echo "Usage: $0 up|down"
14+
exit 1
15+
fi
16+

0 commit comments

Comments
 (0)