gen_docs_integrations.py 13 KB

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