rulerClient.ts 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. import { RuleIdentifier, RulerDataSourceConfig, RuleWithLocation } from 'app/types/unified-alerting';
  2. import {
  3. PostableRuleGrafanaRuleDTO,
  4. PostableRulerRuleGroupDTO,
  5. RulerGrafanaRuleDTO,
  6. RulerRuleGroupDTO,
  7. } from 'app/types/unified-alerting-dto';
  8. import { deleteRulerRulesGroup, fetchRulerRulesGroup, fetchRulerRules, setRulerRuleGroup } from '../api/ruler';
  9. import { RuleFormValues } from '../types/rule-form';
  10. import * as ruleId from '../utils/rule-id';
  11. import { GRAFANA_RULES_SOURCE_NAME } from './datasource';
  12. import { formValuesToRulerGrafanaRuleDTO, formValuesToRulerRuleDTO } from './rule-form';
  13. import {
  14. isCloudRuleIdentifier,
  15. isGrafanaRuleIdentifier,
  16. isGrafanaRulerRule,
  17. isPrometheusRuleIdentifier,
  18. } from './rules';
  19. export interface RulerClient {
  20. findEditableRule(ruleIdentifier: RuleIdentifier): Promise<RuleWithLocation | null>;
  21. deleteRule(ruleWithLocation: RuleWithLocation): Promise<void>;
  22. saveLotexRule(values: RuleFormValues, existing?: RuleWithLocation): Promise<RuleIdentifier>;
  23. saveGrafanaRule(values: RuleFormValues, existing?: RuleWithLocation): Promise<RuleIdentifier>;
  24. }
  25. export function getRulerClient(rulerConfig: RulerDataSourceConfig): RulerClient {
  26. const findEditableRule = async (ruleIdentifier: RuleIdentifier): Promise<RuleWithLocation | null> => {
  27. if (isGrafanaRuleIdentifier(ruleIdentifier)) {
  28. const namespaces = await fetchRulerRules(rulerConfig);
  29. // find namespace and group that contains the uid for the rule
  30. for (const [namespace, groups] of Object.entries(namespaces)) {
  31. for (const group of groups) {
  32. const rule = group.rules.find(
  33. (rule) => isGrafanaRulerRule(rule) && rule.grafana_alert?.uid === ruleIdentifier.uid
  34. );
  35. if (rule) {
  36. return {
  37. group,
  38. ruleSourceName: GRAFANA_RULES_SOURCE_NAME,
  39. namespace: namespace,
  40. rule,
  41. };
  42. }
  43. }
  44. }
  45. }
  46. if (isCloudRuleIdentifier(ruleIdentifier)) {
  47. const { ruleSourceName, namespace, groupName } = ruleIdentifier;
  48. const group = await fetchRulerRulesGroup(rulerConfig, namespace, groupName);
  49. if (!group) {
  50. return null;
  51. }
  52. const rule = group.rules.find((rule) => {
  53. const identifier = ruleId.fromRulerRule(ruleSourceName, namespace, group.name, rule);
  54. return ruleId.equal(identifier, ruleIdentifier);
  55. });
  56. if (!rule) {
  57. return null;
  58. }
  59. return {
  60. group,
  61. ruleSourceName,
  62. namespace,
  63. rule,
  64. };
  65. }
  66. if (isPrometheusRuleIdentifier(ruleIdentifier)) {
  67. throw new Error('Native prometheus rules can not be edited in grafana.');
  68. }
  69. return null;
  70. };
  71. const deleteRule = async (ruleWithLocation: RuleWithLocation): Promise<void> => {
  72. const { namespace, group, rule } = ruleWithLocation;
  73. // it was the last rule, delete the entire group
  74. if (group.rules.length === 1) {
  75. await deleteRulerRulesGroup(rulerConfig, namespace, group.name);
  76. return;
  77. }
  78. // post the group with rule removed
  79. await setRulerRuleGroup(rulerConfig, namespace, {
  80. ...group,
  81. rules: group.rules.filter((r) => r !== rule),
  82. });
  83. };
  84. const saveLotexRule = async (values: RuleFormValues, existing?: RuleWithLocation): Promise<RuleIdentifier> => {
  85. const { dataSourceName, group, namespace } = values;
  86. const formRule = formValuesToRulerRuleDTO(values);
  87. if (dataSourceName && group && namespace) {
  88. // if we're updating a rule...
  89. if (existing) {
  90. // refetch it so we always have the latest greatest
  91. const freshExisting = await findEditableRule(ruleId.fromRuleWithLocation(existing));
  92. if (!freshExisting) {
  93. throw new Error('Rule not found.');
  94. }
  95. // if namespace or group was changed, delete the old rule
  96. if (freshExisting.namespace !== namespace || freshExisting.group.name !== group) {
  97. await deleteRule(freshExisting);
  98. } else {
  99. // if same namespace or group, update the group replacing the old rule with new
  100. const payload = {
  101. ...freshExisting.group,
  102. rules: freshExisting.group.rules.map((existingRule) =>
  103. existingRule === freshExisting.rule ? formRule : existingRule
  104. ),
  105. };
  106. await setRulerRuleGroup(rulerConfig, namespace, payload);
  107. return ruleId.fromRulerRule(dataSourceName, namespace, group, formRule);
  108. }
  109. }
  110. // if creating new rule or existing rule was in a different namespace/group, create new rule in target group
  111. const targetGroup = await fetchRulerRulesGroup(rulerConfig, namespace, group);
  112. const payload: RulerRuleGroupDTO = targetGroup
  113. ? {
  114. ...targetGroup,
  115. rules: [...targetGroup.rules, formRule],
  116. }
  117. : {
  118. name: group,
  119. rules: [formRule],
  120. };
  121. await setRulerRuleGroup(rulerConfig, namespace, payload);
  122. return ruleId.fromRulerRule(dataSourceName, namespace, group, formRule);
  123. } else {
  124. throw new Error('Data source and location must be specified');
  125. }
  126. };
  127. const saveGrafanaRule = async (values: RuleFormValues, existingRule?: RuleWithLocation): Promise<RuleIdentifier> => {
  128. const { folder, group, evaluateEvery } = values;
  129. if (!folder) {
  130. throw new Error('Folder must be specified');
  131. }
  132. const newRule = formValuesToRulerGrafanaRuleDTO(values);
  133. const namespace = folder.title;
  134. const groupSpec = { name: group, interval: evaluateEvery };
  135. if (!existingRule) {
  136. return addRuleToNamespaceAndGroup(namespace, groupSpec, newRule);
  137. }
  138. const sameNamespace = existingRule.namespace === namespace;
  139. const sameGroup = existingRule.group.name === values.group;
  140. const sameLocation = sameNamespace && sameGroup;
  141. if (sameLocation) {
  142. // we're update a rule in the same namespace and group
  143. return updateGrafanaRule(existingRule, newRule, evaluateEvery);
  144. } else {
  145. // we're moving a rule to either a different group or namespace
  146. return moveGrafanaRule(namespace, groupSpec, existingRule, newRule);
  147. }
  148. };
  149. const addRuleToNamespaceAndGroup = async (
  150. namespace: string,
  151. group: { name: string; interval: string },
  152. newRule: PostableRuleGrafanaRuleDTO
  153. ): Promise<RuleIdentifier> => {
  154. const existingGroup = await fetchRulerRulesGroup(rulerConfig, namespace, group.name);
  155. if (!existingGroup) {
  156. throw new Error(`No group found with name "${group.name}"`);
  157. }
  158. const payload: PostableRulerRuleGroupDTO = {
  159. name: group.name,
  160. interval: group.interval,
  161. rules: (existingGroup.rules ?? []).concat(newRule as RulerGrafanaRuleDTO),
  162. };
  163. await setRulerRuleGroup(rulerConfig, namespace, payload);
  164. return { uid: newRule.grafana_alert.uid ?? '', ruleSourceName: GRAFANA_RULES_SOURCE_NAME };
  165. };
  166. // move the rule to another namespace / groupname
  167. const moveGrafanaRule = async (
  168. namespace: string,
  169. group: { name: string; interval: string },
  170. existingRule: RuleWithLocation,
  171. newRule: PostableRuleGrafanaRuleDTO
  172. ): Promise<RuleIdentifier> => {
  173. // make sure our updated alert has the same UID as before
  174. // that way the rule is automatically moved to the new namespace / group name
  175. copyGrafanaUID(existingRule, newRule);
  176. // add the new rule to the requested namespace and group
  177. const identifier = await addRuleToNamespaceAndGroup(namespace, group, newRule);
  178. return identifier;
  179. };
  180. const updateGrafanaRule = async (
  181. existingRule: RuleWithLocation,
  182. newRule: PostableRuleGrafanaRuleDTO,
  183. interval: string
  184. ): Promise<RuleIdentifier> => {
  185. // make sure our updated alert has the same UID as before
  186. copyGrafanaUID(existingRule, newRule);
  187. // create the new array of rules we want to send to the group
  188. const newRules = existingRule.group.rules
  189. .filter((rule): rule is RulerGrafanaRuleDTO => isGrafanaRulerRule(rule))
  190. .filter((rule) => rule.grafana_alert.uid !== existingRule.rule.grafana_alert.uid)
  191. .concat(newRule as RulerGrafanaRuleDTO);
  192. await setRulerRuleGroup(rulerConfig, existingRule.namespace, {
  193. name: existingRule.group.name,
  194. interval: interval,
  195. rules: newRules,
  196. });
  197. return { uid: existingRule.rule.grafana_alert.uid, ruleSourceName: GRAFANA_RULES_SOURCE_NAME };
  198. };
  199. // Would be nice to somehow align checking of ruler type between different methods
  200. // Maybe each datasource should have its own ruler client implementation
  201. return {
  202. findEditableRule,
  203. deleteRule,
  204. saveLotexRule,
  205. saveGrafanaRule,
  206. };
  207. }
  208. //copy the Grafana rule UID from the old rule to the new rule
  209. function copyGrafanaUID(
  210. oldRule: RuleWithLocation,
  211. newRule: PostableRuleGrafanaRuleDTO
  212. ): asserts oldRule is RuleWithLocation<RulerGrafanaRuleDTO> {
  213. // type guard to make sure we're working with a Grafana managed rule
  214. if (!isGrafanaRulerRule(oldRule.rule)) {
  215. throw new Error('The rule is not a Grafana managed rule');
  216. }
  217. const uid = oldRule.rule.grafana_alert.uid;
  218. newRule.grafana_alert.uid = uid;
  219. }