gen_docs_integrations.py 16 KB

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