gen_docs_integrations.py 13 KB

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