1
1
from __future__ import annotations
2
+ import json
2
3
import shlex
3
4
import os
4
5
@@ -50,11 +51,25 @@ def check_cluster_available() -> bool:
50
51
return run_command (command = shlex .split (f"{ _exec } version" ))[0 ]
51
52
52
53
53
- def get_kind_data (kind : str ) -> Dict [str , Any ]:
54
+ def write_to_file (kind : str , data : Dict [str , Any ], output_debug_file_path : str ) -> None :
55
+ content = {}
56
+ if os .path .isfile (output_debug_file_path ):
57
+ with open (output_debug_file_path ) as fd :
58
+ content = json .load (fd )
59
+
60
+ content .update (data )
61
+ with open (output_debug_file_path , "w" ) as fd :
62
+ json .dump (content , fd , indent = 4 )
63
+
64
+
65
+ def get_kind_data (kind : str , debug : bool = False , output_debug_file_path : str = "" ) -> Dict [str , Any ]:
54
66
"""
55
67
Get oc/kubectl explain output for given kind and if kind is namespaced
56
68
"""
57
69
_ , explain_out , _ = run_command (command = shlex .split (f"oc explain { kind } --recursive" ))
70
+ if debug :
71
+ write_to_file (kind = kind , data = {"explain" : explain_out }, output_debug_file_path = output_debug_file_path )
72
+
58
73
resource_kind = re .search (r".*?KIND:\s+(.*?)\n" , explain_out )
59
74
if not resource_kind :
60
75
LOGGER .error (f"Failed to get resource kind from explain for { kind } " )
@@ -64,6 +79,9 @@ def get_kind_data(kind: str) -> Dict[str, Any]:
64
79
command = shlex .split (f"bash -c 'oc api-resources --namespaced | grep -w { resource_kind .group (1 )} | wc -l'" ),
65
80
check = False ,
66
81
)
82
+ if debug :
83
+ write_to_file (kind = kind , data = {"namespace" : namespace_out }, output_debug_file_path = output_debug_file_path )
84
+
67
85
if namespace_out .strip () == "1" :
68
86
return {"data" : explain_out , "namespaced" : True }
69
87
@@ -75,12 +93,27 @@ def format_resource_kind(resource_kind: str) -> str:
75
93
return re .sub (r"(?<!^)(?<=[a-z])(?=[A-Z])" , "_" , resource_kind ).lower ().strip ()
76
94
77
95
78
- def get_field_description (kind : str , field_name : str , field_under_spec : bool ) -> str :
79
- _ , _out , _ = run_command (
80
- command = shlex .split (f"oc explain { kind } { '.spec' if field_under_spec else '' } .{ field_name } " ),
81
- check = False ,
82
- verify_stderr = False ,
83
- )
96
+ def get_field_description (
97
+ kind : str ,
98
+ field_name : str ,
99
+ field_under_spec : bool ,
100
+ debug : bool ,
101
+ output_debug_file_path : str = "" ,
102
+ debug_content : Optional [Dict [str , str ]] = None ,
103
+ ) -> str :
104
+ if debug_content :
105
+ _out = debug_content [f"explain-{ field_name } " ]
106
+
107
+ else :
108
+ _ , _out , _ = run_command (
109
+ command = shlex .split (f"oc explain { kind } { '.spec' if field_under_spec else '' } .{ field_name } " ),
110
+ check = False ,
111
+ verify_stderr = False ,
112
+ )
113
+
114
+ if debug :
115
+ write_to_file (kind = kind , data = {f"explain-{ field_name } " : _out }, output_debug_file_path = output_debug_file_path )
116
+
84
117
_description = re .search (r"DESCRIPTION:\n\s*(.*)" , _out , re .DOTALL )
85
118
if _description :
86
119
description : str = ""
@@ -102,7 +135,14 @@ def get_field_description(kind: str, field_name: str, field_under_spec: bool) ->
102
135
return "<please add description>"
103
136
104
137
105
- def get_arg_params (field : str , kind : str , field_under_spec : bool = False ) -> Dict [str , Any ]:
138
+ def get_arg_params (
139
+ field : str ,
140
+ kind : str ,
141
+ field_under_spec : bool = False ,
142
+ debug : bool = False ,
143
+ output_debug_file_path : str = "" ,
144
+ debug_content : Optional [Dict [str , str ]] = None ,
145
+ ) -> Dict [str , Any ]:
106
146
splited_field = field .split ()
107
147
_orig_name , _type = splited_field [0 ], splited_field [1 ]
108
148
@@ -134,7 +174,14 @@ def get_arg_params(field: str, kind: str, field_under_spec: bool = False) -> Dic
134
174
"type-for-class-arg" : f"{ name } : { type_from_dict_for_init } " ,
135
175
"required" : required ,
136
176
"type" : type_from_dict ,
137
- "description" : get_field_description (kind = kind , field_name = _orig_name , field_under_spec = field_under_spec ),
177
+ "description" : get_field_description (
178
+ kind = kind ,
179
+ field_name = _orig_name ,
180
+ field_under_spec = field_under_spec ,
181
+ debug = debug ,
182
+ output_debug_file_path = output_debug_file_path ,
183
+ debug_content = debug_content ,
184
+ ),
138
185
}
139
186
140
187
return _res
@@ -188,6 +235,9 @@ def parse_explain(
188
235
api_link : str ,
189
236
output : str ,
190
237
namespaced : Optional [bool ] = None ,
238
+ debug : bool = False ,
239
+ output_debug_file_path : str = "" ,
240
+ debug_content : Optional [Dict [str , str ]] = None ,
191
241
) -> Dict [str , Any ]:
192
242
section_data : str = ""
193
243
sections : List [str ] = []
@@ -249,13 +299,19 @@ def parse_explain(
249
299
first_field_indent = len (re .findall (r" +" , field )[0 ])
250
300
first_field_indent_str = f"{ ' ' * first_field_indent } "
251
301
if not ignored_field and not start_spec_field :
252
- resource_dict [FIELDS_STR ].append (get_arg_params (field = field , kind = kind ))
302
+ resource_dict [FIELDS_STR ].append (
303
+ get_arg_params (field = field , kind = kind , debug = debug , output_debug_file_path = output_debug_file_path )
304
+ )
253
305
254
306
continue
255
307
else :
256
308
if len (re .findall (r" +" , field )[0 ]) == len (first_field_indent_str ):
257
309
if not ignored_field and not start_spec_field :
258
- resource_dict [FIELDS_STR ].append (get_arg_params (field = field , kind = kind ))
310
+ resource_dict [FIELDS_STR ].append (
311
+ get_arg_params (
312
+ field = field , kind = kind , debug = debug , output_debug_file_path = output_debug_file_path
313
+ )
314
+ )
259
315
260
316
if start_spec_field :
261
317
first_field_spec_found = True
@@ -265,7 +321,16 @@ def parse_explain(
265
321
if field_spec_found :
266
322
if not re .findall (rf"^{ first_field_indent_str } \w" , field ):
267
323
if first_field_spec_found :
268
- resource_dict [SPEC_STR ].append (get_arg_params (field = field , kind = kind , field_under_spec = True ))
324
+ resource_dict [SPEC_STR ].append (
325
+ get_arg_params (
326
+ field = field ,
327
+ kind = kind ,
328
+ field_under_spec = True ,
329
+ debug = debug ,
330
+ debug_content = debug_content ,
331
+ output_debug_file_path = output_debug_file_path ,
332
+ )
333
+ )
269
334
270
335
# Get top level keys inside spec indent, need to match only once.
271
336
top_spec_indent = len (re .findall (r" +" , field )[0 ])
@@ -276,7 +341,16 @@ def parse_explain(
276
341
if top_spec_indent_str :
277
342
# Get only top level keys from inside spec
278
343
if re .findall (rf"^{ top_spec_indent_str } \w" , field ):
279
- resource_dict [SPEC_STR ].append (get_arg_params (field = field , kind = kind , field_under_spec = True ))
344
+ resource_dict [SPEC_STR ].append (
345
+ get_arg_params (
346
+ field = field ,
347
+ kind = kind ,
348
+ field_under_spec = True ,
349
+ debug = debug ,
350
+ debug_content = debug_content ,
351
+ output_debug_file_path = output_debug_file_path ,
352
+ )
353
+ )
280
354
continue
281
355
282
356
else :
@@ -288,13 +362,34 @@ def parse_explain(
288
362
289
363
LOGGER .debug (f"\n { yaml .dump (resource_dict )} " )
290
364
291
- if api_group_real_name := resource_dict .get ("GROUP" ):
365
+ api_group_real_name = resource_dict .get ("GROUP" )
366
+ # If API Group is not present in resource, try to get it from VERSION
367
+ if not api_group_real_name :
368
+ version_splited = resource_dict ["VERSION" ].split ("/" )
369
+ if len (version_splited ) == 2 :
370
+ api_group_real_name = version_splited [0 ]
371
+
372
+ if api_group_real_name :
292
373
api_group_for_resource_api_group = api_group_real_name .upper ().replace ("." , "_" )
374
+ resource_dict ["GROUP" ] = api_group_for_resource_api_group
293
375
missing_api_group_in_resource : bool = not hasattr (Resource .ApiGroup , api_group_for_resource_api_group )
294
376
295
377
if missing_api_group_in_resource :
296
378
LOGGER .warning (
297
- f"Missing API Group in Resource\n Please add `Resource.ApiGroup.{ api_group_real_name } = { api_group_real_name } ` manually into ocp_resources/resource.py under Resource class > ApiGroup class."
379
+ f"Missing API Group in Resource\n "
380
+ f"Please add `Resource.ApiGroup.{ api_group_for_resource_api_group } = { api_group_real_name } ` "
381
+ "manually into ocp_resources/resource.py under Resource class > ApiGroup class."
382
+ )
383
+
384
+ else :
385
+ api_version_for_resource_api_version = resource_dict ["VERSION" ].upper ()
386
+ missing_api_version_in_resource : bool = not hasattr (Resource .ApiVersion , api_version_for_resource_api_version )
387
+
388
+ if missing_api_version_in_resource :
389
+ LOGGER .warning (
390
+ f"Missing API Version in Resource\n "
391
+ f"Please add `Resource.ApiVersion.{ api_version_for_resource_api_version } = { resource_dict ['VERSION' ]} ` "
392
+ "manually into ocp_resources/resource.py under Resource class > ApiGroup class."
298
393
)
299
394
300
395
return resource_dict
@@ -344,43 +439,62 @@ def get_user_args_from_interactive() -> Tuple[str, str]:
344
439
is_flag = True ,
345
440
help = "Output file overwrite existing file if passed" ,
346
441
)
347
- @click .option ("-v " , "--verbose " , is_flag = True , help = "Enable debug logs " )
442
+ @click .option ("-d " , "--debug " , is_flag = True , help = "Save all command output to debug file " )
348
443
@click .option ("-i" , "--interactive" , is_flag = True , help = "Enable interactive mode" )
349
444
@click .option ("--dry-run" , is_flag = True , help = "Run the script without writing to file" )
350
- def main (kind : str , api_link : str , verbose : bool , overwrite : bool , interactive : bool , dry_run : bool ) -> None :
445
+ @click .option ("--debug-file" , type = click .Path (exists = True ), help = "Run the script from debug file. (generated by -d)" )
446
+ def main (
447
+ kind : str , api_link : str , overwrite : bool , interactive : bool , dry_run : bool , debug : bool , debug_file : str
448
+ ) -> None :
351
449
"""
352
450
Generates a class for a given Kind.
353
451
"""
354
- LOGGER .setLevel ("DEBUG" if verbose else "INFO" )
355
- if not check_cluster_available ():
356
- LOGGER .error (
357
- "Cluster not available, The script needs a running cluster and admin privileges to get the explain output"
358
- )
359
- return
452
+ debug_content : Dict [str , str ] = {}
453
+ output_debug_file_path = os .path .join (os .path .dirname (__file__ ), "debug" , f"{ kind } -debug.json" )
360
454
361
- if interactive :
362
- kind , api_link = get_user_args_from_interactive ()
455
+ if debug_file :
456
+ dry_run = True
457
+ debug = False
458
+ with open (debug_file ) as fd :
459
+ debug_content = json .load (fd )
363
460
364
- if not kind or not api_link :
365
- LOGGER . error ( "Kind or API link not provided" )
366
- return
461
+ kind_data = debug_content [ "explain" ]
462
+ namespaced = debug_content [ "namespace" ] == "1"
463
+ api_link = "https://debug.explain"
367
464
368
- validate_api_link_schema (value = api_link )
465
+ else :
466
+ if not check_cluster_available ():
467
+ LOGGER .error (
468
+ "Cluster not available, The script needs a running cluster and admin privileges to get the explain output"
469
+ )
470
+ return
369
471
370
- if not check_kind_exists ( kind = kind ) :
371
- return
472
+ if interactive :
473
+ kind , api_link = get_user_args_from_interactive ()
372
474
373
- explain_output = get_kind_data (kind = kind )
374
- if not explain_output :
375
- return
475
+ if not kind or not api_link :
476
+ LOGGER .error ("Kind or API link not provided" )
477
+ return
478
+
479
+ validate_api_link_schema (value = api_link )
376
480
377
- namespaced = explain_output ["namespaced" ]
378
- kind_data = explain_output ["data" ]
481
+ if not check_kind_exists (kind = kind ):
482
+ return
483
+
484
+ explain_output = get_kind_data (kind = kind , debug = debug , output_debug_file_path = output_debug_file_path )
485
+ if not explain_output :
486
+ return
487
+
488
+ namespaced = explain_output ["namespaced" ]
489
+ kind_data = explain_output ["data" ]
379
490
380
491
resource_dict = parse_explain (
381
492
output = kind_data ,
382
493
namespaced = namespaced ,
383
494
api_link = api_link ,
495
+ debug = debug ,
496
+ output_debug_file_path = output_debug_file_path ,
497
+ debug_content = debug_content ,
384
498
)
385
499
if not resource_dict :
386
500
return
@@ -390,6 +504,9 @@ def main(kind: str, api_link: str, verbose: bool, overwrite: bool, interactive:
390
504
if not dry_run :
391
505
run_command (command = shlex .split ("pre-commit run --all-files" ), verify_stderr = False , check = False )
392
506
507
+ if debug :
508
+ LOGGER .info (f"Debug output saved to { output_debug_file_path } " )
509
+
393
510
394
511
if __name__ == "__main__" :
395
512
main ()
0 commit comments