Skip to content

Commit 4b356e7

Browse files
committed
ROX-30437: add integration tests
Added tests will validate events generated on an overlayfs file properly shows the event on the upper layer and the access to the underlying FS. They also validate a mounted path on a container resolves to the correct host path. While developing these tests, it became painfully obvious getting the information of the process running inside the container is not straightforward. Because containers tend to be fairly static, we should be able to manually create the information statically in the test and still have everything work correctly. In order to minimize the amount of changes on existing tests, the default Process constructor now takes fields directly and there is a from_proc class method that builds a new Process object from /proc. Additionally, getting the pid of a process in a container is virtually impossible, so we make the pid check optional.
1 parent 43e3076 commit 4b356e7

File tree

5 files changed

+283
-75
lines changed

5 files changed

+283
-75
lines changed

tests/conftest.py

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def dump_logs(container, file):
8484
def fact_config(request, monitored_dir, logs_dir):
8585
cwd = os.getcwd()
8686
config = {
87-
'paths': [monitored_dir],
87+
'paths': [monitored_dir, '/mounted', '/container-dir'],
8888
'grpc': {
8989
'url': 'http://127.0.0.1:9999',
9090
},
@@ -106,6 +106,31 @@ def fact_config(request, monitored_dir, logs_dir):
106106
config_file.close()
107107

108108

109+
@pytest.fixture
110+
def test_container(request, docker_client, ignored_dir):
111+
"""
112+
Run a container for triggering events in.
113+
"""
114+
container = docker_client.containers.run(
115+
'quay.io/fedora/fedora:43',
116+
detach=True,
117+
tty=True,
118+
volumes={
119+
ignored_dir: {
120+
'bind': '/mounted',
121+
'mode': 'z',
122+
},
123+
},
124+
name='fedora',
125+
)
126+
container.exec_run('mkdir /mounted /container-dir')
127+
128+
yield container
129+
130+
container.stop(timeout=1)
131+
container.remove()
132+
133+
109134
@pytest.fixture
110135
def fact(request, docker_client, fact_config, server, logs_dir):
111136
"""
@@ -124,20 +149,8 @@ def fact(request, docker_client, fact_config, server, logs_dir):
124149
network_mode='host',
125150
privileged=True,
126151
volumes={
127-
'/sys/kernel/security': {
128-
'bind': '/host/sys/kernel/security',
129-
'mode': 'ro',
130-
},
131-
'/etc': {
132-
'bind': '/host/etc',
133-
'mode': 'ro',
134-
},
135-
'/proc/sys/kernel': {
136-
'bind': '/host/proc/sys/kernel',
137-
'mode': 'ro',
138-
},
139-
'/usr/lib/os-release': {
140-
'bind': '/host/usr/lib/os-release',
152+
'/': {
153+
'bind': '/host',
141154
'mode': 'ro',
142155
},
143156
config_file: {

tests/event.py

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,31 @@ class Process:
3737
Represents a process with its attributes.
3838
"""
3939

40-
def __init__(self, pid: int | None = None):
41-
self._pid: int = pid if pid is not None else os.getpid()
42-
proc_dir = os.path.join('/proc', str(self._pid))
43-
40+
def __init__(self,
41+
pid: int | None,
42+
uid: int,
43+
gid: int,
44+
exe_path: str,
45+
args: str,
46+
name: str,
47+
container_id: str,
48+
loginuid: int):
49+
self._pid: int | None = pid
50+
self._uid: int = uid
51+
self._gid: int = gid
52+
self._exe_path: str = exe_path
53+
self._args: str = args
54+
self._name: str = name
55+
self._container_id: str = container_id
56+
self._loginuid: int = loginuid
57+
58+
@classmethod
59+
def from_proc(cls, pid: int | None = None):
60+
pid: int = pid if pid is not None else os.getpid()
61+
proc_dir = os.path.join('/proc', str(pid))
62+
63+
uid = 0
64+
gid = 0
4465
with open(os.path.join(proc_dir, 'status'), 'r') as f:
4566
def get_id(line: str, wanted_id: str) -> int | None:
4667
if line.startswith(f'{wanted_id}:'):
@@ -50,27 +71,36 @@ def get_id(line: str, wanted_id: str) -> int | None:
5071
return None
5172

5273
for line in f.readlines():
53-
if (uid := get_id(line, 'Uid')) is not None:
54-
self._uid: int = uid
55-
elif (gid := get_id(line, 'Gid')) is not None:
56-
self._gid: int = gid
74+
if (id := get_id(line, 'Uid')) is not None:
75+
uid = id
76+
elif (id := get_id(line, 'Gid')) is not None:
77+
gid = id
5778

58-
self._exe_path: str = os.path.realpath(os.path.join(proc_dir, 'exe'))
79+
exe_path = os.path.realpath(os.path.join(proc_dir, 'exe'))
5980

6081
with open(os.path.join(proc_dir, 'cmdline'), 'rb') as f:
6182
content = f.read(4096)
6283
args = [arg.decode('utf-8')
6384
for arg in content.split(b'\x00') if arg]
64-
self._args: str = ' '.join(args)
85+
args = ' '.join(args)
6586

6687
with open(os.path.join(proc_dir, 'comm'), 'r') as f:
67-
self._name: str = f.read().strip()
88+
name = f.read().strip()
6889

6990
with open(os.path.join(proc_dir, 'cgroup'), 'r') as f:
70-
self._container_id: str = extract_container_id(f.read())
91+
container_id = extract_container_id(f.read())
7192

7293
with open(os.path.join(proc_dir, 'loginuid'), 'r') as f:
73-
self._loginuid: int = int(f.read())
94+
loginuid = int(f.read())
95+
96+
return Process(pid=pid,
97+
uid=uid,
98+
gid=gid,
99+
exe_path=exe_path,
100+
args=args,
101+
name=name,
102+
container_id=container_id,
103+
loginuid=loginuid)
74104

75105
@property
76106
def uid(self) -> int:
@@ -81,7 +111,7 @@ def gid(self) -> int:
81111
return self._gid
82112

83113
@property
84-
def pid(self) -> int:
114+
def pid(self) -> int | None:
85115
return self._pid
86116

87117
@property
@@ -107,10 +137,12 @@ def loginuid(self) -> int:
107137
@override
108138
def __eq__(self, other: Any) -> bool:
109139
if isinstance(other, ProcessSignal):
140+
if self.pid is not None and self.pid != other.pid:
141+
return False
142+
110143
return (
111144
self.uid == other.uid and
112145
self.gid == other.gid and
113-
self.pid == other.pid and
114146
self.exe_path == other.exec_file_path and
115147
self.args == other.args and
116148
self.name == other.name and
@@ -124,7 +156,7 @@ def __str__(self) -> str:
124156
return (f'Process(uid={self.uid}, gid={self.gid}, pid={self.pid}, '
125157
f'exe_path={self.exe_path}, args={self.args}, '
126158
f'name={self.name}, container_id={self.container_id}, '
127-
f'loginuid={self.loginuid}')
159+
f'loginuid={self.loginuid})')
128160

129161

130162
class Event:
@@ -136,10 +168,12 @@ class Event:
136168
def __init__(self,
137169
process: Process,
138170
event_type: EventType,
139-
file: str):
171+
file: str,
172+
host_path: str = ''):
140173
self._type: EventType = event_type
141174
self._process: Process = process
142175
self._file: str = file
176+
self._host_path: str = host_path
143177

144178
@property
145179
def event_type(self) -> EventType:
@@ -153,22 +187,30 @@ def process(self) -> Process:
153187
def file(self) -> str:
154188
return self._file
155189

190+
@property
191+
def host_path(self) -> str:
192+
return self._host_path
193+
156194
@override
157195
def __eq__(self, other: Any) -> bool:
158196
if isinstance(other, FileActivity):
159197
if self.process != other.process or self.event_type.name.lower() != other.WhichOneof('file'):
160198
return False
161199

162200
if self.event_type == EventType.CREATION:
163-
return self.file == other.creation.activity.path
201+
return self.file == other.creation.activity.path and \
202+
self.host_path == other.creation.activity.host_path
164203
elif self.event_type == EventType.OPEN:
165-
return self.file == other.open.activity.path
204+
return self.file == other.open.activity.path and \
205+
self.host_path == other.open.activity.host_path
166206
elif self.event_type == EventType.UNLINK:
167-
return self.file == other.unlink.activity.path
207+
return self.file == other.unlink.activity.path and \
208+
self.host_path == other.unlink.activity.host_path
168209
return False
169210
raise NotImplementedError
170211

171212
@override
172213
def __str__(self) -> str:
173214
return (f'Event(event_type={self.event_type.name}, '
174-
f'process={self.process}, file="{self.file}")')
215+
f'process={self.process}, file="{self.file}", '
216+
f'host_path="{self.host_path}")')

tests/test_config_hotreload.py

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,9 @@ def test_output_grpc_address_change(fact, fact_config, monitored_dir, server, al
9797
with open(fut, 'w') as f:
9898
f.write('This is a test')
9999

100-
process = Process()
101-
e = Event(process=process, event_type=EventType.CREATION, file=fut)
100+
process = Process.from_proc()
101+
e = Event(process=process, event_type=EventType.CREATION,
102+
file=fut, host_path=fut)
102103
print(f'Waiting for event: {e}')
103104

104105
server.wait_events([e])
@@ -111,30 +112,32 @@ def test_output_grpc_address_change(fact, fact_config, monitored_dir, server, al
111112
with open(fut, 'w') as f:
112113
f.write('This is another test')
113114

114-
e = Event(process=process, event_type=EventType.OPEN, file=fut)
115+
e = Event(process=process, event_type=EventType.OPEN,
116+
file=fut, host_path=fut)
115117
print(f'Waiting for event on alternate server: {e}')
116118

117119
alternate_server.wait_events([e])
118120

119121

120122
def test_paths(fact, fact_config, monitored_dir, ignored_dir, server):
121-
p = Process()
123+
p = Process.from_proc()
122124

123125
# Ignored file, must not show up in the server
124126
ignored_file = os.path.join(ignored_dir, 'test.txt')
125127
with open(ignored_file, 'w') as f:
126128
f.write('This is to be ignored')
127129

128-
ignored_event = Event(
129-
process=p, event_type=EventType.CREATION, file=ignored_file)
130+
ignored_event = Event(process=p, event_type=EventType.CREATION,
131+
file=ignored_file, host_path=ignored_file)
130132
print(f'Ignoring: {ignored_event}')
131133

132134
# File Under Test
133135
fut = os.path.join(monitored_dir, 'test.txt')
134136
with open(fut, 'w') as f:
135137
f.write('This is a test')
136138

137-
e = Event(process=p, event_type=EventType.CREATION, file=fut)
139+
e = Event(process=p, event_type=EventType.CREATION,
140+
file=fut, host_path=fut)
138141
print(f'Waiting for event: {e}')
139142

140143
server.wait_events([e], ignored=[ignored_event])
@@ -148,38 +151,40 @@ def test_paths(fact, fact_config, monitored_dir, ignored_dir, server):
148151
with open(ignored_file, 'w') as f:
149152
f.write('This is another test')
150153

151-
e = Event(
152-
process=p, event_type=EventType.OPEN, file=ignored_file)
154+
e = Event(process=p, event_type=EventType.OPEN,
155+
file=ignored_file, host_path=ignored_file)
153156
print(f'Waiting for event: {e}')
154157

155158
# File Under Test
156159
with open(fut, 'w') as f:
157160
f.write('This is another ignored event')
158161

159-
ignored_event = Event(process=p, event_type=EventType.OPEN, file=fut)
162+
ignored_event = Event(
163+
process=p, event_type=EventType.OPEN, file=fut, host_path=fut)
160164
print(f'Ignoring: {ignored_event}')
161165

162166
server.wait_events([e], ignored=[ignored_event])
163167

164168

165169
def test_paths_addition(fact, fact_config, monitored_dir, ignored_dir, server):
166-
p = Process()
170+
p = Process.from_proc()
167171

168172
# Ignored file, must not show up in the server
169173
ignored_file = os.path.join(ignored_dir, 'test.txt')
170174
with open(ignored_file, 'w') as f:
171175
f.write('This is to be ignored')
172176

173-
ignored_event = Event(
174-
process=p, event_type=EventType.CREATION, file=ignored_file)
177+
ignored_event = Event(process=p, event_type=EventType.CREATION,
178+
file=ignored_file, host_path=ignored_file)
175179
print(f'Ignoring: {ignored_event}')
176180

177181
# File Under Test
178182
fut = os.path.join(monitored_dir, 'test.txt')
179183
with open(fut, 'w') as f:
180184
f.write('This is a test')
181185

182-
e = Event(process=p, event_type=EventType.CREATION, file=fut)
186+
e = Event(process=p, event_type=EventType.CREATION,
187+
file=fut, host_path=fut)
183188
print(f'Waiting for event: {e}')
184189

185190
server.wait_events([e], ignored=[ignored_event])
@@ -196,8 +201,9 @@ def test_paths_addition(fact, fact_config, monitored_dir, ignored_dir, server):
196201
f.write('This is one final event')
197202

198203
events = [
199-
Event(process=p, event_type=EventType.OPEN, file=ignored_file),
200-
Event(process=p, event_type=EventType.OPEN, file=fut)
204+
Event(process=p, event_type=EventType.OPEN,
205+
file=ignored_file, host_path=ignored_file),
206+
Event(process=p, event_type=EventType.OPEN, file=fut, host_path=fut)
201207
]
202208
print(f'Waiting for events: {events}')
203209

0 commit comments

Comments
 (0)