gen_docs_integrations.py 16 KB

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