11from collections import Counter
2- import json
32import logging
3+ import re
44import os
5+ import sys
56import os .path
67import shlex
78import shutil
1415
1516def _find_sonobuoy ():
1617 """find sonobuoy in PATH, but also in a fixed location at ~/.local/bin to simplify use with Ansible"""
17- result = shutil .which (' sonobuoy' )
18+ result = shutil .which (" sonobuoy" )
1819 if result :
1920 return result
20- logger .debug (' sonobuoy not in PATH, trying $HOME/.local/bin' )
21- result = os .path .join (os .path .expanduser ('~' ), ' .local' , ' bin' , ' sonobuoy' )
21+ logger .debug (" sonobuoy not in PATH, trying $HOME/.local/bin" )
22+ result = os .path .join (os .path .expanduser ("~" ), " .local" , " bin" , " sonobuoy" )
2223 if os .path .exists (result ):
2324 return result
24- logger .debug (' sonobuoy executable not found; expect errors' )
25+ logger .debug (" sonobuoy executable not found; expect errors" )
2526
2627
2728def _fmt_result (counter ):
28- return ', ' .join (f"{ counter .get (key , 0 )} { key } " for key in ('passed' , 'failed' , 'skipped' ))
29+ return ", " .join (
30+ f"{ counter .get (key , 0 )} { key } " for key in ("passed" , "failed" , "skipped" )
31+ )
2932
3033
3134class SonobuoyHandler :
@@ -69,20 +72,10 @@ def _sonobuoy_run(self):
6972 def _sonobuoy_delete (self ):
7073 self ._invoke_sonobuoy ("delete" , "--wait" )
7174
72- def _sonobuoy_status_result (self ):
73- process = self ._invoke_sonobuoy ("status" , "--json" , capture_output = True )
74- json_data = json .loads (process .stdout )
75- counter = Counter ()
76- for entry in json_data ["plugins" ]:
77- logger .debug (f"plugin { entry ['plugin' ]} : { _fmt_result (entry ['result-counts' ])} " )
78- for key , value in entry ["result-counts" ].items ():
79- counter [key ] += value
80- return counter
81-
8275 def _eval_result (self , counter ):
8376 """evaluate test results and return return code"""
8477 result_message = f"sonobuoy reports { _fmt_result (counter )} "
85- if counter [' failed' ]:
78+ if counter [" failed" ]:
8679 logger .error (result_message )
8780 return 3
8881 logger .info (result_message )
@@ -95,34 +88,21 @@ def _preflight_check(self):
9588 if not self .sonobuoy :
9689 raise RuntimeError ("sonobuoy executable not found; is it in PATH?" )
9790
98- def _sonobuoy_retrieve_result (self , plugin = ' e2e' ):
91+ def _sonobuoy_retrieve_result (self , plugin = " e2e" ):
9992 """
10093 Invoke sonobuoy to retrieve results and to store them in a subdirectory of
10194 the working directory. Analyze the results yaml file for given `plugin` and
102- log each failure as ERROR. Return summary dict like `_sonobuoy_status_result` .
95+ log each failure as ERROR. Return summary dict.
10396 """
10497 logger .debug (f"retrieving results to { self .result_dir_name } " )
10598 result_dir = os .path .join (self .working_directory , self .result_dir_name )
10699 os .makedirs (result_dir , exist_ok = True )
107100
108101 self ._invoke_sonobuoy ("retrieve" , "-x" , result_dir )
109- yaml_path = os .path .join (result_dir , ' plugins' , plugin , ' sonobuoy_results.yaml' )
102+ yaml_path = os .path .join (result_dir , " plugins" , plugin , " sonobuoy_results.yaml" )
110103 logger .debug (f"parsing results from { yaml_path } " )
111- with open (yaml_path , "r" ) as fileobj :
112- result_obj = yaml .load (fileobj .read (), yaml .SafeLoader )
113- counter = Counter ()
114- for item1 in result_obj .get ('items' , ()):
115- # file ...
116- for item2 in item1 .get ('items' , ()):
117- # suite ...
118- for item in item2 .get ('items' , ()):
119- # testcase ... or so
120- status = item .get ('status' , 'skipped' )
121- counter [status ] += 1
122- if status == 'failed' :
123- logger .error (f"FAILED: { item ['name' ]} " ) # <-- this is why this method exists!
124- logger .info (f"{ plugin } results: { _fmt_result (counter )} " )
125- return counter
104+
105+ return sonobuoy_parse_result (plugin , yaml_path )
126106
127107 def run (self ):
128108 """
@@ -132,16 +112,106 @@ def run(self):
132112 self ._preflight_check ()
133113 try :
134114 self ._sonobuoy_run ()
135- return_code = self ._eval_result (self ._sonobuoy_status_result ())
115+ counter = self ._sonobuoy_retrieve_result ()
116+ return_code = self ._eval_result (counter )
136117 print (self .check_name + ": " + ("PASS" , "FAIL" )[min (1 , return_code )])
137- try :
138- self ._sonobuoy_retrieve_result ()
139- except Exception :
140- # swallow exception for the time being
141- logger .debug ('problem retrieving results' , exc_info = True )
142118 return return_code
143119 except BaseException :
144120 logger .exception ("something went wrong" )
145121 return 112
146122 finally :
147123 self ._sonobuoy_delete ()
124+
125+
126+ def sonobuoy_parse_result (plugin , sonobuoy_results_yaml_path ):
127+ with open (sonobuoy_results_yaml_path , "r" ) as fileobj :
128+ result_obj = yaml .load (fileobj .read (), yaml .SafeLoader )
129+ counter = Counter ()
130+ for item1 in result_obj .get ("items" , ()):
131+ # file ...
132+ for item2 in item1 .get ("items" , ()):
133+ # suite ...
134+ for item in item2 .get ("items" , ()):
135+ # testcase ... or so
136+ status = item .get ("status" , "skipped" )
137+ if status == "failed" :
138+ if ok_to_fail (item ["name" ]):
139+ status = "failed_ok"
140+ else :
141+ logger .error (f"FAILED: { item ['name' ]} " )
142+ counter [status ] += 1
143+
144+ logger .info (f"{ plugin } results: { _fmt_result (counter )} " )
145+ return counter
146+
147+
148+ def ok_to_fail (test_name ):
149+ # TODO: Make this configurable
150+ regexs_and_reason = [
151+ # InternalIP still used in tests. Having only an ExternalIP is considered valid by SCS:
152+ (
153+ "HostPort validates that there is no conflict between pods with same hostPort but different hostIP and protocol" ,
154+ "Fails when a cluster has no InternalIP (only ExternalIP): https://github.com/kubernetes/kubernetes/issues/136626" ,
155+ ),
156+ (
157+ "Services should be able to switch session affinity for NodePort service" ,
158+ "Fails when a cluster has no InternalIP (only ExternalIP): https://github.com/kubernetes/kubernetes/issues/136626" ,
159+ ),
160+ (
161+ "Services should have session affinity work for NodePort service" ,
162+ "Fails when a cluster has no InternalIP (only ExternalIP): https://github.com/kubernetes/kubernetes/issues/136626" ,
163+ ),
164+ (
165+ "validates that there exists conflict between pods with same hostPort and protocol but one using 0.0.0.0 hostIP" ,
166+ "Fails when a cluster has no InternalIP (only ExternalIP): https://github.com/kubernetes/kubernetes/issues/136626" ,
167+ ),
168+ # Was flaky
169+ (
170+ "Netpol NetworkPolicy between server and client should allow ingress access from updated namespace" ,
171+ "Flaky test. Fix in v1.36 (https://github.com/kubernetes/kubernetes/pull/136715)" ,
172+ ),
173+ # SCTP is optional
174+ (
175+ "Feature:SCTPConnectivity" ,
176+ "SCTPConnectivity is optional. Currently Cilium does not support it by default." ,
177+ ),
178+ # Tests skipped by Cilium: https://github.com/cilium/cilium/blob/main/.github/workflows/k8s-kind-network-policies-e2e.yaml#L177-L185
179+ (
180+ "should.allow.egress.access.to.server.in.CIDR.block" ,
181+ "https://github.com/cilium/cilium/issues/9209" ,
182+ ),
183+ (
184+ "should.ensure.an.IP.overlapping.both.IPBlock.CIDR.and.IPBlock.Except.is.allowed" ,
185+ "https://github.com/cilium/cilium/issues/9209" ,
186+ ),
187+ (
188+ "should.enforce.except.clause.while.egress.access.to.server.in.CIDR.block" ,
189+ "https://github.com/cilium/cilium/issues/9209" ,
190+ ),
191+ ]
192+ name = test_name
193+ for regex , _reason in regexs_and_reason :
194+ if re .search (regex , name ):
195+ return True
196+ return False
197+
198+
199+ def usage ():
200+ print (
201+ f"""{ sys .argv [0 ]} path-to/sonobuoy_results.yaml
202+ Read sonobuoy_results.yaml and print the results.
203+ """
204+ )
205+ sys .exit (3 )
206+
207+
208+ if __name__ == "__main__" :
209+ if len (sys .argv ) < 2 :
210+ usage ()
211+ sonobuoy_results_yaml_path = sys .argv [1 ]
212+ if not os .path .exists (sonobuoy_results_yaml_path ):
213+ print (f"{ sonobuoy_results_yaml_path } does not exist" )
214+ usage ()
215+ counter = sonobuoy_parse_result ("" , sys .argv [1 ])
216+ for key , value in counter .items ():
217+ print (f"{ key } : { value } " )
0 commit comments