gen_docs_integrations.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. import json
  2. import re
  3. import shutil
  4. from pathlib import Path
  5. # Dictionary responsible for making the symbolic links at the end of the script's run.
  6. symlink_dict = {}
  7. def cleanup():
  8. """
  9. clean directories that are either data collection or exporting integrations
  10. """
  11. for element in Path("src/go/plugin/go.d/collector").glob('**/*/'):
  12. if "integrations" in str(element):
  13. shutil.rmtree(element)
  14. for element in Path("src/collectors").glob('**/*/'):
  15. # print(element)
  16. if "integrations" in str(element):
  17. shutil.rmtree(element)
  18. for element in Path("src/exporting").glob('**/*/'):
  19. if "integrations" in str(element):
  20. shutil.rmtree(element)
  21. for element in Path("integrations/cloud-notifications").glob('**/*/'):
  22. if "integrations" in str(element) and not "metadata.yaml" in str(element):
  23. shutil.rmtree(element)
  24. for element in Path("integrations/logs").glob('**/*/'):
  25. if "integrations" in str(element) and "metadata.yaml" not in str(element):
  26. shutil.rmtree(element)
  27. for element in Path("integrations/cloud-authentication").glob('**/*/'):
  28. if "integrations" in str(element) and not "metadata.yaml" in str(element):
  29. shutil.rmtree(element)
  30. def generate_category_from_name(category_fragment, category_array):
  31. """
  32. Takes a category ID in splitted form ("." as delimiter) and the array of the categories, and returns the proper category name that Learn expects.
  33. """
  34. category_name = ""
  35. i = 0
  36. dummy_id = category_fragment[0]
  37. while i < len(category_fragment):
  38. for category in category_array:
  39. if dummy_id == category['id']:
  40. category_name = category_name + "/" + category["name"]
  41. try:
  42. # print("equals")
  43. # print(fragment, category_fragment[i+1])
  44. dummy_id = dummy_id + "." + category_fragment[i + 1]
  45. # print(dummy_id)
  46. except IndexError:
  47. return category_name.split("/", 1)[1]
  48. category_array = category['children']
  49. break
  50. i += 1
  51. def clean_and_write(md, path):
  52. """
  53. This function takes care of the special details element, and converts it to the equivalent that md expects.
  54. Then it writes the buffer on the file provided.
  55. """
  56. # clean first, replace
  57. md = md.replace("{% details summary=\"", "<details><summary>")
  58. md = md.replace("{% details open=true summary=\"", "<details open><summary>")
  59. md = md.replace("\" %}", "</summary>\n")
  60. md = md.replace("{% /details %}", "</details>\n")
  61. path.write_text(md)
  62. def add_custom_edit_url(markdown_string, meta_yaml_link, sidebar_label_string, mode='default'):
  63. """
  64. Takes a markdown string and adds a "custom_edit_url" metadata to the metadata field
  65. """
  66. output = ""
  67. path_to_md_file = ""
  68. if mode == 'default':
  69. path_to_md_file = f'{meta_yaml_link.replace("/metadata.yaml", "")}/integrations/{clean_string(sidebar_label_string)}'
  70. elif mode == 'cloud-notification':
  71. path_to_md_file = meta_yaml_link.replace("metadata.yaml", f'integrations/{clean_string(sidebar_label_string)}')
  72. elif mode == 'agent-notification':
  73. path_to_md_file = meta_yaml_link.replace("metadata.yaml", "README")
  74. elif mode == 'cloud-authentication':
  75. path_to_md_file = meta_yaml_link.replace("metadata.yaml", f'integrations/{clean_string(sidebar_label_string)}')
  76. elif mode == 'logs':
  77. path_to_md_file = meta_yaml_link.replace("metadata.yaml", f'integrations/{clean_string(sidebar_label_string)}')
  78. output = markdown_string.replace(
  79. "<!--startmeta",
  80. f'<!--startmeta\ncustom_edit_url: \"{path_to_md_file}.md\"')
  81. return output
  82. def clean_string(string):
  83. """
  84. simple function to get rid of caps, spaces, slashes and parentheses from a given string
  85. The string represents an integration name, as it would be displayed in the final text
  86. """
  87. return string.lower().replace(" ", "_").replace("/", "-").replace("(", "").replace(")", "").replace(":", "")
  88. def read_integrations_js(path_to_file):
  89. """
  90. Open integrations/integrations.js and extract the dictionaries
  91. """
  92. try:
  93. data = Path(path_to_file).read_text()
  94. categories_str = data.split("export const categories = ")[1].split("export const integrations = ")[0]
  95. integrations_str = data.split("export const categories = ")[1].split("export const integrations = ")[1]
  96. return json.loads(categories_str), json.loads(integrations_str)
  97. except FileNotFoundError as e:
  98. print("Exception", e)
  99. def create_overview(integration, filename, overview_key_name="overview"):
  100. # empty overview_key_name to have only image on overview
  101. if not overview_key_name:
  102. return f"""# {integration['meta']['name']}
  103. <img src="https://netdata.cloud/img/{filename}" width="150"/>
  104. """
  105. split = re.split(r'(#.*\n)', integration[overview_key_name], 1)
  106. first_overview_part = split[1]
  107. rest_overview_part = split[2]
  108. if not filename:
  109. return f"""{first_overview_part}{rest_overview_part}
  110. """
  111. return f"""{first_overview_part}
  112. <img src="https://netdata.cloud/img/{filename}" width="150"/>
  113. {rest_overview_part}
  114. """
  115. def build_readme_from_integration(integration, mode=''):
  116. # COLLECTORS
  117. if mode == 'collector':
  118. try:
  119. # initiate the variables for the collector
  120. meta_yaml = integration['edit_link'].replace("blob", "edit")
  121. sidebar_label = integration['meta']['monitored_instance']['name']
  122. learn_rel_path = generate_category_from_name(
  123. integration['meta']['monitored_instance']['categories'][0].split("."), categories).replace(
  124. "Data Collection", "Collecting Metrics")
  125. most_popular = integration['meta']['most_popular']
  126. # build the markdown string
  127. md = \
  128. f"""<!--startmeta
  129. meta_yaml: "{meta_yaml}"
  130. sidebar_label: "{sidebar_label}"
  131. learn_status: "Published"
  132. learn_rel_path: "{learn_rel_path}"
  133. most_popular: {most_popular}
  134. message: "DO NOT EDIT THIS FILE DIRECTLY, IT IS GENERATED BY THE COLLECTOR'S metadata.yaml FILE"
  135. endmeta-->
  136. {create_overview(integration, integration['meta']['monitored_instance']['icon_filename'])}"""
  137. if integration['metrics']:
  138. md += f"""
  139. {integration['metrics']}
  140. """
  141. if integration['alerts']:
  142. md += f"""
  143. {integration['alerts']}
  144. """
  145. if integration['setup']:
  146. md += f"""
  147. {integration['setup']}
  148. """
  149. if integration['troubleshooting']:
  150. md += f"""
  151. {integration['troubleshooting']}
  152. """
  153. except Exception as e:
  154. print("Exception in collector md construction", e, integration['id'])
  155. # EXPORTERS
  156. elif mode == 'exporter':
  157. try:
  158. # initiate the variables for the exporter
  159. meta_yaml = integration['edit_link'].replace("blob", "edit")
  160. sidebar_label = integration['meta']['name']
  161. learn_rel_path = generate_category_from_name(integration['meta']['categories'][0].split("."), categories)
  162. # build the markdown string
  163. md = \
  164. f"""<!--startmeta
  165. meta_yaml: "{meta_yaml}"
  166. sidebar_label: "{sidebar_label}"
  167. learn_status: "Published"
  168. learn_rel_path: "Exporting Metrics"
  169. message: "DO NOT EDIT THIS FILE DIRECTLY, IT IS GENERATED BY THE EXPORTER'S metadata.yaml FILE"
  170. endmeta-->
  171. {create_overview(integration, integration['meta']['icon_filename'])}"""
  172. if integration['setup']:
  173. md += f"""
  174. {integration['setup']}
  175. """
  176. if integration['troubleshooting']:
  177. md += f"""
  178. {integration['troubleshooting']}
  179. """
  180. except Exception as e:
  181. print("Exception in exporter md construction", e, integration['id'])
  182. # NOTIFICATIONS
  183. elif mode == 'agent-notification':
  184. try:
  185. # initiate the variables for the notification method
  186. meta_yaml = integration['edit_link'].replace("blob", "edit")
  187. sidebar_label = integration['meta']['name']
  188. learn_rel_path = generate_category_from_name(integration['meta']['categories'][0].split("."), categories)
  189. # build the markdown string
  190. md = \
  191. f"""<!--startmeta
  192. meta_yaml: "{meta_yaml}"
  193. sidebar_label: "{sidebar_label}"
  194. learn_status: "Published"
  195. learn_rel_path: "{learn_rel_path.replace("notifications", "Alerts & Notifications/Notifications")}"
  196. message: "DO NOT EDIT THIS FILE DIRECTLY, IT IS GENERATED BY THE NOTIFICATION'S metadata.yaml FILE"
  197. endmeta-->
  198. {create_overview(integration, integration['meta']['icon_filename'], "overview")}"""
  199. if integration['setup']:
  200. md += f"""
  201. {integration['setup']}
  202. """
  203. if integration['troubleshooting']:
  204. md += f"""
  205. {integration['troubleshooting']}
  206. """
  207. except Exception as e:
  208. print("Exception in notification md construction", e, integration['id'])
  209. elif mode == 'cloud-notification':
  210. try:
  211. # initiate the variables for the notification method
  212. meta_yaml = integration['edit_link'].replace("blob", "edit")
  213. sidebar_label = integration['meta']['name']
  214. learn_rel_path = generate_category_from_name(integration['meta']['categories'][0].split("."), categories)
  215. # build the markdown string
  216. md = \
  217. f"""<!--startmeta
  218. meta_yaml: "{meta_yaml}"
  219. sidebar_label: "{sidebar_label}"
  220. learn_status: "Published"
  221. learn_rel_path: "{learn_rel_path.replace("notifications", "Alerts & Notifications/Notifications")}"
  222. message: "DO NOT EDIT THIS FILE DIRECTLY, IT IS GENERATED BY THE NOTIFICATION'S metadata.yaml FILE"
  223. endmeta-->
  224. {create_overview(integration, integration['meta']['icon_filename'], "")}"""
  225. if integration['setup']:
  226. md += f"""
  227. {integration['setup']}
  228. """
  229. if integration['troubleshooting']:
  230. md += f"""
  231. {integration['troubleshooting']}
  232. """
  233. except Exception as e:
  234. print("Exception in notification md construction", e, integration['id'])
  235. elif mode == 'logs':
  236. try:
  237. # initiate the variables for the logs integration
  238. meta_yaml = integration['edit_link'].replace("blob", "edit")
  239. sidebar_label = integration['meta']['name']
  240. learn_rel_path = generate_category_from_name(integration['meta']['categories'][0].split("."), categories)
  241. # build the markdown string
  242. md = \
  243. f"""<!--startmeta
  244. meta_yaml: "{meta_yaml}"
  245. sidebar_label: "{sidebar_label}"
  246. learn_status: "Published"
  247. learn_rel_path: "{learn_rel_path.replace("logs", "Logs")}"
  248. message: "DO NOT EDIT THIS FILE DIRECTLY, IT IS GENERATED BY THE LOGS' metadata.yaml FILE"
  249. endmeta-->
  250. {create_overview(integration, integration['meta']['icon_filename'])}"""
  251. if integration['setup']:
  252. md += f"""
  253. {integration['setup']}
  254. """
  255. except Exception as e:
  256. print("Exception in logs md construction", e, integration['id'])
  257. # AUTHENTICATIONS
  258. elif mode == 'authentication':
  259. if True:
  260. # initiate the variables for the authentication method
  261. meta_yaml = integration['edit_link'].replace("blob", "edit")
  262. sidebar_label = integration['meta']['name']
  263. learn_rel_path = generate_category_from_name(integration['meta']['categories'][0].split("."), categories)
  264. # build the markdown string
  265. md = \
  266. f"""<!--startmeta
  267. meta_yaml: "{meta_yaml}"
  268. sidebar_label: "{sidebar_label}"
  269. learn_status: "Published"
  270. learn_rel_path: "{learn_rel_path.replace("authentication", "Netdata Cloud/Authentication & Authorization/Cloud Authentication & Authorization Integrations")}"
  271. message: "DO NOT EDIT THIS FILE DIRECTLY, IT IS GENERATED BY THE AUTHENTICATION'S metadata.yaml FILE"
  272. endmeta-->
  273. {create_overview(integration, integration['meta']['icon_filename'])}"""
  274. if integration['setup']:
  275. md += f"""
  276. {integration['setup']}
  277. """
  278. if integration['troubleshooting']:
  279. md += f"""
  280. {integration['troubleshooting']}
  281. """
  282. # except Exception as e:
  283. # print("Exception in authentication md construction", e, integration['id'])
  284. if "community" in integration['meta'].keys():
  285. community = "<img src=\"https://img.shields.io/badge/maintained%20by-Community-blue\" />"
  286. else:
  287. community = "<img src=\"https://img.shields.io/badge/maintained%20by-Netdata-%2300ab44\" />"
  288. return meta_yaml, sidebar_label, learn_rel_path, md, community
  289. def build_path(meta_yaml_link):
  290. """
  291. function that takes a metadata yaml file link, and makes it into a path that gets used to write to a file.
  292. """
  293. return meta_yaml_link.replace("https://github.com/netdata/", "") \
  294. .split("/", 1)[1] \
  295. .replace("edit/master/", "") \
  296. .replace("/metadata.yaml", "")
  297. def write_to_file(path, md, meta_yaml, sidebar_label, community, mode='default'):
  298. """
  299. takes the arguments needed to write the integration markdown to the proper file.
  300. """
  301. upper, lower = md.split("##", 1)
  302. md = upper + community + f"\n\n##{lower}"
  303. if mode == 'default':
  304. # Only if the path exists, this caters for running the same script on both the go and netdata repos.
  305. if Path(path).exists():
  306. if not Path(f'{path}/integrations').exists():
  307. Path(f'{path}/integrations').mkdir()
  308. try:
  309. md = add_custom_edit_url(md, meta_yaml, sidebar_label)
  310. clean_and_write(
  311. md,
  312. Path(f'{path}/integrations/{clean_string(sidebar_label)}.md')
  313. )
  314. except FileNotFoundError as e:
  315. print("Exception in writing to file", e)
  316. # If we only created one file inside the directory, add the entry to the symlink_dict, so we can make the symbolic link
  317. if len(list(Path(f'{path}/integrations').iterdir())) == 1:
  318. symlink_dict.update(
  319. {path: f'integrations/{clean_string(sidebar_label)}.md'})
  320. else:
  321. try:
  322. symlink_dict.pop(path)
  323. except KeyError:
  324. # We don't need to print something here.
  325. pass
  326. elif mode == 'cloud-notification':
  327. # for cloud notifications we generate them near their metadata.yaml
  328. name = clean_string(integration['meta']['name'])
  329. if not Path(f'{path}/integrations').exists():
  330. Path(f'{path}/integrations').mkdir()
  331. # proper_edit_name = meta_yaml.replace(
  332. # "metadata.yaml", f'integrations/{clean_string(sidebar_label)}.md\"')
  333. md = add_custom_edit_url(md, meta_yaml, sidebar_label, mode='cloud-notification')
  334. finalpath = f'{path}/integrations/{name}.md'
  335. try:
  336. clean_and_write(
  337. md,
  338. Path(finalpath)
  339. )
  340. except FileNotFoundError as e:
  341. print("Exception in writing to file", e)
  342. elif mode == 'agent-notification':
  343. # add custom_edit_url as the md file, so we can have uniqueness in the ingest script
  344. # afterwards the ingest will replace this metadata with meta_yaml
  345. md = add_custom_edit_url(md, meta_yaml, sidebar_label, mode='agent-notification')
  346. finalpath = f'{path}/README.md'
  347. try:
  348. clean_and_write(
  349. md,
  350. Path(finalpath)
  351. )
  352. except FileNotFoundError as e:
  353. print("Exception in writing to file", e)
  354. elif mode == 'logs':
  355. # for logs we generate them near their metadata.yaml
  356. name = clean_string(integration['meta']['name'])
  357. if not Path(f'{path}/integrations').exists():
  358. Path(f'{path}/integrations').mkdir()
  359. # proper_edit_name = meta_yaml.replace(
  360. # "metadata.yaml", f'integrations/{clean_string(sidebar_label)}.md\"')
  361. md = add_custom_edit_url(md, meta_yaml, sidebar_label, mode='logs')
  362. finalpath = f'{path}/integrations/{name}.md'
  363. try:
  364. clean_and_write(
  365. md,
  366. Path(finalpath)
  367. )
  368. except FileNotFoundError as e:
  369. print("Exception in writing to file", e)
  370. elif mode == 'authentication':
  371. name = clean_string(integration['meta']['name'])
  372. if not Path(f'{path}/integrations').exists():
  373. Path(f'{path}/integrations').mkdir()
  374. # proper_edit_name = meta_yaml.replace(
  375. # "metadata.yaml", f'integrations/{clean_string(sidebar_label)}.md\"')
  376. md = add_custom_edit_url(md, meta_yaml, sidebar_label, mode='cloud-authentication')
  377. finalpath = f'{path}/integrations/{name}.md'
  378. try:
  379. clean_and_write(
  380. md,
  381. Path(finalpath)
  382. )
  383. except FileNotFoundError as e:
  384. print("Exception in writing to file", e)
  385. def make_symlinks(symlink_dict):
  386. """
  387. takes a dictionary with directories that have a 1:1 relationship between their README and the integration (only one) inside the "integrations" folder.
  388. """
  389. for element in symlink_dict:
  390. if not Path(f'{element}/README.md').exists():
  391. Path(f'{element}/README.md').touch()
  392. try:
  393. # Remove the README to prevent it being a normal file
  394. Path(f'{element}/README.md').unlink()
  395. except FileNotFoundError:
  396. continue
  397. # and then make a symlink to the actual markdown
  398. Path(f'{element}/README.md').symlink_to(symlink_dict[element])
  399. filepath = Path(f'{element}/{symlink_dict[element]}')
  400. md = filepath.read_text()
  401. # This preserves the custom_edit_url for most files as it was,
  402. # so the existing links don't break, this is vital for link replacement afterwards
  403. filepath.write_text(md.replace(
  404. f'{element}/{symlink_dict[element]}', f'{element}/README.md'))
  405. cleanup()
  406. categories, integrations = read_integrations_js('integrations/integrations.js')
  407. # Iterate through every integration
  408. for integration in integrations:
  409. if integration['integration_type'] == "collector":
  410. meta_yaml, sidebar_label, learn_rel_path, md, community = build_readme_from_integration(
  411. integration, mode='collector')
  412. path = build_path(meta_yaml)
  413. write_to_file(path, md, meta_yaml, sidebar_label, community)
  414. else:
  415. # kind of specific if clause, so we can avoid running excessive code in the go repo
  416. if integration['integration_type'] == "exporter":
  417. meta_yaml, sidebar_label, learn_rel_path, md, community = build_readme_from_integration(
  418. integration, mode='exporter')
  419. path = build_path(meta_yaml)
  420. write_to_file(path, md, meta_yaml, sidebar_label, community)
  421. elif integration['integration_type'] == "agent_notification":
  422. meta_yaml, sidebar_label, learn_rel_path, md, community = build_readme_from_integration(
  423. integration, mode='agent-notification')
  424. path = build_path(meta_yaml)
  425. write_to_file(path, md, meta_yaml, sidebar_label, community, mode='agent-notification')
  426. elif integration['integration_type'] == "cloud_notification":
  427. meta_yaml, sidebar_label, learn_rel_path, md, community = build_readme_from_integration(
  428. integration, mode='cloud-notification')
  429. path = build_path(meta_yaml)
  430. write_to_file(path, md, meta_yaml, sidebar_label, community, mode='cloud-notification')
  431. elif integration['integration_type'] == "logs":
  432. meta_yaml, sidebar_label, learn_rel_path, md, community = build_readme_from_integration(
  433. integration, mode='logs')
  434. path = build_path(meta_yaml)
  435. write_to_file(path, md, meta_yaml, sidebar_label, community, mode='logs')
  436. elif integration['integration_type'] == "authentication":
  437. meta_yaml, sidebar_label, learn_rel_path, md, community = build_readme_from_integration(
  438. integration, mode='authentication')
  439. path = build_path(meta_yaml)
  440. write_to_file(path, md, meta_yaml, sidebar_label, community, mode='authentication')
  441. make_symlinks(symlink_dict)