mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Handle namespace and group query string params in Ruler API (#91533)
* Handle namespace and group query string params in Ruler API * Use the new namespace and group query params when slashes in names * Add validation, add group handling in GMA Api * Move constants * Use checkForPathSeparator function * Fix linter issue
This commit is contained in:
parent
d54fdba322
commit
b67bcdb9b8
@ -85,8 +85,14 @@ func (srv RulerSrv) RouteDeleteAlertRules(c *contextmodel.ReqContext, namespaceU
|
|||||||
"namespaceUid",
|
"namespaceUid",
|
||||||
namespace.UID,
|
namespace.UID,
|
||||||
}
|
}
|
||||||
if group != "" {
|
|
||||||
loggerCtx = append(loggerCtx, "group", group)
|
finalGroup, err := getRulesGroupParam(c, group)
|
||||||
|
if err != nil {
|
||||||
|
return ErrResp(http.StatusBadRequest, err, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
if finalGroup != "" {
|
||||||
|
loggerCtx = append(loggerCtx, "group", finalGroup)
|
||||||
}
|
}
|
||||||
logger := srv.log.New(loggerCtx...)
|
logger := srv.log.New(loggerCtx...)
|
||||||
|
|
||||||
@ -97,11 +103,11 @@ func (srv RulerSrv) RouteDeleteAlertRules(c *contextmodel.ReqContext, namespaceU
|
|||||||
|
|
||||||
err = srv.xactManager.InTransaction(c.Req.Context(), func(ctx context.Context) error {
|
err = srv.xactManager.InTransaction(c.Req.Context(), func(ctx context.Context) error {
|
||||||
deletionCandidates := map[ngmodels.AlertRuleGroupKey]ngmodels.RulesGroup{}
|
deletionCandidates := map[ngmodels.AlertRuleGroupKey]ngmodels.RulesGroup{}
|
||||||
if group != "" {
|
if finalGroup != "" {
|
||||||
key := ngmodels.AlertRuleGroupKey{
|
key := ngmodels.AlertRuleGroupKey{
|
||||||
OrgID: c.SignedInUser.GetOrgID(),
|
OrgID: c.SignedInUser.GetOrgID(),
|
||||||
NamespaceUID: namespace.UID,
|
NamespaceUID: namespace.UID,
|
||||||
RuleGroup: group,
|
RuleGroup: finalGroup,
|
||||||
}
|
}
|
||||||
rules, err := srv.getAuthorizedRuleGroup(ctx, c, key)
|
rules, err := srv.getAuthorizedRuleGroup(ctx, c, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -218,9 +224,14 @@ func (srv RulerSrv) RouteGetRulesGroupConfig(c *contextmodel.ReqContext, namespa
|
|||||||
return toNamespaceErrorResponse(err)
|
return toNamespaceErrorResponse(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
finalRuleGroup, err := getRulesGroupParam(c, ruleGroup)
|
||||||
|
if err != nil {
|
||||||
|
return ErrResp(http.StatusBadRequest, err, "")
|
||||||
|
}
|
||||||
|
|
||||||
rules, err := srv.getAuthorizedRuleGroup(c.Req.Context(), c, ngmodels.AlertRuleGroupKey{
|
rules, err := srv.getAuthorizedRuleGroup(c.Req.Context(), c, ngmodels.AlertRuleGroupKey{
|
||||||
OrgID: c.SignedInUser.GetOrgID(),
|
OrgID: c.SignedInUser.GetOrgID(),
|
||||||
RuleGroup: ruleGroup,
|
RuleGroup: finalRuleGroup,
|
||||||
NamespaceUID: namespace.UID,
|
NamespaceUID: namespace.UID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -234,7 +245,7 @@ func (srv RulerSrv) RouteGetRulesGroupConfig(c *contextmodel.ReqContext, namespa
|
|||||||
|
|
||||||
result := apimodels.RuleGroupConfigResponse{
|
result := apimodels.RuleGroupConfigResponse{
|
||||||
// nolint:staticcheck
|
// nolint:staticcheck
|
||||||
GettableRuleGroupConfig: toGettableRuleGroupConfig(ruleGroup, rules, provenanceRecords),
|
GettableRuleGroupConfig: toGettableRuleGroupConfig(finalRuleGroup, rules, provenanceRecords),
|
||||||
}
|
}
|
||||||
return response.JSON(http.StatusAccepted, result)
|
return response.JSON(http.StatusAccepted, result)
|
||||||
}
|
}
|
||||||
|
@ -68,12 +68,18 @@ func (r *LotexRuler) RouteDeleteNamespaceRulesConfig(ctx *contextmodel.ReqContex
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrResp(500, err, "")
|
return ErrResp(500, err, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
finalNamespace, err := getRulesNamespaceParam(ctx, namespace)
|
||||||
|
if err != nil {
|
||||||
|
return ErrResp(http.StatusBadRequest, err, "")
|
||||||
|
}
|
||||||
|
|
||||||
return r.requester.withReq(
|
return r.requester.withReq(
|
||||||
ctx,
|
ctx,
|
||||||
http.MethodDelete,
|
http.MethodDelete,
|
||||||
withPath(
|
withPath(
|
||||||
*ctx.Req.URL,
|
*ctx.Req.URL,
|
||||||
fmt.Sprintf("%s/%s", legacyRulerPrefix, url.PathEscape(namespace)),
|
fmt.Sprintf("%s/%s", legacyRulerPrefix, url.PathEscape(finalNamespace)),
|
||||||
),
|
),
|
||||||
nil,
|
nil,
|
||||||
messageExtractor,
|
messageExtractor,
|
||||||
@ -86,6 +92,17 @@ func (r *LotexRuler) RouteDeleteRuleGroupConfig(ctx *contextmodel.ReqContext, na
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrResp(500, err, "")
|
return ErrResp(500, err, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
finalNamespace, err := getRulesNamespaceParam(ctx, namespace)
|
||||||
|
if err != nil {
|
||||||
|
return ErrResp(http.StatusBadRequest, err, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
finalGroup, err := getRulesGroupParam(ctx, group)
|
||||||
|
if err != nil {
|
||||||
|
return ErrResp(http.StatusBadRequest, err, "")
|
||||||
|
}
|
||||||
|
|
||||||
return r.requester.withReq(
|
return r.requester.withReq(
|
||||||
ctx,
|
ctx,
|
||||||
http.MethodDelete,
|
http.MethodDelete,
|
||||||
@ -94,8 +111,8 @@ func (r *LotexRuler) RouteDeleteRuleGroupConfig(ctx *contextmodel.ReqContext, na
|
|||||||
fmt.Sprintf(
|
fmt.Sprintf(
|
||||||
"%s/%s/%s",
|
"%s/%s/%s",
|
||||||
legacyRulerPrefix,
|
legacyRulerPrefix,
|
||||||
url.PathEscape(namespace),
|
url.PathEscape(finalNamespace),
|
||||||
url.PathEscape(group),
|
url.PathEscape(finalGroup),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
nil,
|
nil,
|
||||||
@ -109,6 +126,12 @@ func (r *LotexRuler) RouteGetNamespaceRulesConfig(ctx *contextmodel.ReqContext,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrResp(500, err, "")
|
return ErrResp(500, err, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
finalNamespace, err := getRulesNamespaceParam(ctx, namespace)
|
||||||
|
if err != nil {
|
||||||
|
return ErrResp(http.StatusBadRequest, err, "")
|
||||||
|
}
|
||||||
|
|
||||||
return r.requester.withReq(
|
return r.requester.withReq(
|
||||||
ctx,
|
ctx,
|
||||||
http.MethodGet,
|
http.MethodGet,
|
||||||
@ -117,7 +140,7 @@ func (r *LotexRuler) RouteGetNamespaceRulesConfig(ctx *contextmodel.ReqContext,
|
|||||||
fmt.Sprintf(
|
fmt.Sprintf(
|
||||||
"%s/%s",
|
"%s/%s",
|
||||||
legacyRulerPrefix,
|
legacyRulerPrefix,
|
||||||
url.PathEscape(namespace),
|
url.PathEscape(finalNamespace),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
nil,
|
nil,
|
||||||
@ -131,6 +154,17 @@ func (r *LotexRuler) RouteGetRulegGroupConfig(ctx *contextmodel.ReqContext, name
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrResp(500, err, "")
|
return ErrResp(500, err, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
finalNamespace, err := getRulesNamespaceParam(ctx, namespace)
|
||||||
|
if err != nil {
|
||||||
|
return ErrResp(http.StatusBadRequest, err, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
finalGroup, err := getRulesGroupParam(ctx, group)
|
||||||
|
if err != nil {
|
||||||
|
return ErrResp(http.StatusBadRequest, err, "")
|
||||||
|
}
|
||||||
|
|
||||||
return r.requester.withReq(
|
return r.requester.withReq(
|
||||||
ctx,
|
ctx,
|
||||||
http.MethodGet,
|
http.MethodGet,
|
||||||
@ -139,8 +173,8 @@ func (r *LotexRuler) RouteGetRulegGroupConfig(ctx *contextmodel.ReqContext, name
|
|||||||
fmt.Sprintf(
|
fmt.Sprintf(
|
||||||
"%s/%s/%s",
|
"%s/%s/%s",
|
||||||
legacyRulerPrefix,
|
legacyRulerPrefix,
|
||||||
url.PathEscape(namespace),
|
url.PathEscape(finalNamespace),
|
||||||
url.PathEscape(group),
|
url.PathEscape(finalGroup),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
nil,
|
nil,
|
||||||
@ -177,7 +211,13 @@ func (r *LotexRuler) RoutePostNameRulesConfig(ctx *contextmodel.ReqContext, conf
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrResp(500, err, "Failed marshal rule group")
|
return ErrResp(500, err, "Failed marshal rule group")
|
||||||
}
|
}
|
||||||
u := withPath(*ctx.Req.URL, fmt.Sprintf("%s/%s", legacyRulerPrefix, ns))
|
|
||||||
|
finalNamespace, err := getRulesNamespaceParam(ctx, ns)
|
||||||
|
if err != nil {
|
||||||
|
return ErrResp(http.StatusBadRequest, err, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
u := withPath(*ctx.Req.URL, fmt.Sprintf("%s/%s", legacyRulerPrefix, url.PathEscape(finalNamespace)))
|
||||||
return r.requester.withReq(ctx, http.MethodPost, u, bytes.NewBuffer(yml), jsonExtractor(nil), nil)
|
return r.requester.withReq(ctx, http.MethodPost, u, bytes.NewBuffer(yml), jsonExtractor(nil), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,6 +26,11 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/web"
|
"github.com/grafana/grafana/pkg/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
namespaceQueryTag = "QUERY_NAMESPACE"
|
||||||
|
groupQueryTag = "QUERY_GROUP"
|
||||||
|
)
|
||||||
|
|
||||||
var searchRegex = regexp.MustCompile(`\{(\w+)\}`)
|
var searchRegex = regexp.MustCompile(`\{(\w+)\}`)
|
||||||
|
|
||||||
func toMacaronPath(path string) string {
|
func toMacaronPath(path string) string {
|
||||||
@ -240,3 +245,29 @@ func getHash(hashSlice []string) uint64 {
|
|||||||
hash := sum.Sum64()
|
hash := sum.Sum64()
|
||||||
return hash
|
return hash
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getRulesGroupParam(ctx *contextmodel.ReqContext, pathGroup string) (string, error) {
|
||||||
|
if pathGroup == groupQueryTag {
|
||||||
|
group := ctx.Query("group")
|
||||||
|
if group == "" {
|
||||||
|
return "", fmt.Errorf("group query parameter is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
return group, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathGroup, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRulesNamespaceParam(ctx *contextmodel.ReqContext, pathNamespace string) (string, error) {
|
||||||
|
if pathNamespace == namespaceQueryTag {
|
||||||
|
namespace := ctx.Query("namespace")
|
||||||
|
if namespace == "" {
|
||||||
|
return "", fmt.Errorf("namespace query parameter is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
return namespace, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathNamespace, nil
|
||||||
|
}
|
||||||
|
@ -27,7 +27,7 @@ import {
|
|||||||
FetchPromRulesFilter,
|
FetchPromRulesFilter,
|
||||||
groupRulesByFileName,
|
groupRulesByFileName,
|
||||||
paramsWithMatcherAndState,
|
paramsWithMatcherAndState,
|
||||||
prepareRulesFilterQueryParams,
|
getRulesFilterSearchParams,
|
||||||
} from './prometheus';
|
} from './prometheus';
|
||||||
import { FetchRulerRulesFilter, rulerUrlBuilder } from './ruler';
|
import { FetchRulerRulesFilter, rulerUrlBuilder } from './ruler';
|
||||||
|
|
||||||
@ -153,7 +153,8 @@ export const alertRuleApi = alertingApi.injectEndpoints({
|
|||||||
searchParams.set(PrometheusAPIFilters.RuleGroup, identifier.groupName);
|
searchParams.set(PrometheusAPIFilters.RuleGroup, identifier.groupName);
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = prepareRulesFilterQueryParams(searchParams, filter);
|
const filterParams = getRulesFilterSearchParams(filter);
|
||||||
|
const params = { ...filterParams, ...Object.fromEntries(searchParams) };
|
||||||
|
|
||||||
return { url: PROM_RULES_URL, params: paramsWithMatcherAndState(params, state, matcher) };
|
return { url: PROM_RULES_URL, params: paramsWithMatcherAndState(params, state, matcher) };
|
||||||
},
|
},
|
||||||
|
@ -37,8 +37,9 @@ export function prometheusUrlBuilder(dataSourceConfig: PrometheusDataSourceConfi
|
|||||||
searchParams.set('rule_group', identifier.groupName);
|
searchParams.set('rule_group', identifier.groupName);
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = prepareRulesFilterQueryParams(searchParams, filter);
|
const filterParams = getRulesFilterSearchParams(filter);
|
||||||
|
|
||||||
|
const params = { ...filterParams, ...Object.fromEntries(searchParams) };
|
||||||
return {
|
return {
|
||||||
url: `/api/prometheus/${getDatasourceAPIUid(dataSourceName)}/api/v1/rules`,
|
url: `/api/prometheus/${getDatasourceAPIUid(dataSourceName)}/api/v1/rules`,
|
||||||
params: paramsWithMatcherAndState(params, state, matcher),
|
params: paramsWithMatcherAndState(params, state, matcher),
|
||||||
@ -47,18 +48,17 @@ export function prometheusUrlBuilder(dataSourceConfig: PrometheusDataSourceConfi
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function prepareRulesFilterQueryParams(
|
export function getRulesFilterSearchParams(filter?: FetchPromRulesFilter): Record<string, string> {
|
||||||
params: URLSearchParams,
|
const filterParams: Record<string, string> = {};
|
||||||
filter?: FetchPromRulesFilter
|
|
||||||
): Record<string, string> {
|
|
||||||
if (filter?.dashboardUID) {
|
if (filter?.dashboardUID) {
|
||||||
params.set('dashboard_uid', filter.dashboardUID);
|
filterParams.dashboard_uid = filter.dashboardUID;
|
||||||
if (filter?.panelId) {
|
if (filter?.panelId) {
|
||||||
params.set('panel_id', String(filter.panelId));
|
filterParams.panel_id = String(filter.panelId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.fromEntries(params);
|
return filterParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function paramsWithMatcherAndState(
|
export function paramsWithMatcherAndState(
|
||||||
|
@ -1,15 +1,28 @@
|
|||||||
import { RulerDataSourceConfig } from 'app/types/unified-alerting';
|
import { RulerDataSourceConfig } from 'app/types/unified-alerting';
|
||||||
|
|
||||||
import { getDatasourceAPIUid } from '../utils/datasource';
|
import { mockDataSource } from '../mocks';
|
||||||
|
import { setupDataSources } from '../testSetup/datasources';
|
||||||
|
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||||
|
|
||||||
import { rulerUrlBuilder } from './ruler';
|
import { rulerUrlBuilder } from './ruler';
|
||||||
|
|
||||||
jest.mock('../utils/datasource');
|
const grafanaConfig: RulerDataSourceConfig = {
|
||||||
|
dataSourceName: GRAFANA_RULES_SOURCE_NAME,
|
||||||
const mocks = {
|
apiVersion: 'legacy',
|
||||||
getDatasourceAPIUId: jest.mocked(getDatasourceAPIUid),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mimirConfig: RulerDataSourceConfig = {
|
||||||
|
dataSourceName: 'Mimir-cloud',
|
||||||
|
apiVersion: 'config',
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
setupDataSources(
|
||||||
|
mockDataSource({ type: DataSourceType.Prometheus, name: 'Mimir-cloud', uid: 'mimir-1' }),
|
||||||
|
mockDataSource({ type: DataSourceType.Prometheus, name: 'Cortex', uid: 'cortex-1' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
describe('rulerUrlBuilder', () => {
|
describe('rulerUrlBuilder', () => {
|
||||||
it('Should use /api/v1/rules endpoint with subtype = cortex param for legacy api version', () => {
|
it('Should use /api/v1/rules endpoint with subtype = cortex param for legacy api version', () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
@ -18,8 +31,6 @@ describe('rulerUrlBuilder', () => {
|
|||||||
apiVersion: 'legacy',
|
apiVersion: 'legacy',
|
||||||
};
|
};
|
||||||
|
|
||||||
mocks.getDatasourceAPIUId.mockReturnValue('ds-uid');
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const builder = rulerUrlBuilder(config);
|
const builder = rulerUrlBuilder(config);
|
||||||
|
|
||||||
@ -28,54 +39,38 @@ describe('rulerUrlBuilder', () => {
|
|||||||
const group = builder.namespaceGroup('test-ns', 'test-gr');
|
const group = builder.namespaceGroup('test-ns', 'test-gr');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(rules.path).toBe('/api/ruler/ds-uid/api/v1/rules');
|
expect(rules.path).toBe('/api/ruler/cortex-1/api/v1/rules');
|
||||||
expect(rules.params).toMatchObject({ subtype: 'cortex' });
|
expect(rules.params).toMatchObject({ subtype: 'cortex' });
|
||||||
|
|
||||||
expect(namespace.path).toBe('/api/ruler/ds-uid/api/v1/rules/test-ns');
|
expect(namespace.path).toBe('/api/ruler/cortex-1/api/v1/rules/test-ns');
|
||||||
expect(namespace.params).toMatchObject({ subtype: 'cortex' });
|
expect(namespace.params).toMatchObject({ subtype: 'cortex' });
|
||||||
|
|
||||||
expect(group.path).toBe('/api/ruler/ds-uid/api/v1/rules/test-ns/test-gr');
|
expect(group.path).toBe('/api/ruler/cortex-1/api/v1/rules/test-ns/test-gr');
|
||||||
expect(group.params).toMatchObject({ subtype: 'cortex' });
|
expect(group.params).toMatchObject({ subtype: 'cortex' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should use /api/v1/rules endpoint with subtype = mimir parameter for config api version', () => {
|
it('Should use /api/v1/rules endpoint with subtype = mimir parameter for config api version', () => {
|
||||||
// Arrange
|
|
||||||
const config: RulerDataSourceConfig = {
|
|
||||||
dataSourceName: 'Cortex v2',
|
|
||||||
apiVersion: 'config',
|
|
||||||
};
|
|
||||||
|
|
||||||
mocks.getDatasourceAPIUId.mockReturnValue('ds-uid');
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const builder = rulerUrlBuilder(config);
|
const builder = rulerUrlBuilder(mimirConfig);
|
||||||
|
|
||||||
const rules = builder.rules();
|
const rules = builder.rules();
|
||||||
const namespace = builder.namespace('test-ns');
|
const namespace = builder.namespace('test-ns');
|
||||||
const group = builder.namespaceGroup('test-ns', 'test-gr');
|
const group = builder.namespaceGroup('test-ns', 'test-gr');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(rules.path).toBe('/api/ruler/ds-uid/api/v1/rules');
|
expect(rules.path).toBe('/api/ruler/mimir-1/api/v1/rules');
|
||||||
expect(rules.params).toMatchObject({ subtype: 'mimir' });
|
expect(rules.params).toMatchObject({ subtype: 'mimir' });
|
||||||
|
|
||||||
expect(namespace.path).toBe('/api/ruler/ds-uid/api/v1/rules/test-ns');
|
expect(namespace.path).toBe('/api/ruler/mimir-1/api/v1/rules/test-ns');
|
||||||
expect(namespace.params).toMatchObject({ subtype: 'mimir' });
|
expect(namespace.params).toMatchObject({ subtype: 'mimir' });
|
||||||
|
|
||||||
expect(group.path).toBe('/api/ruler/ds-uid/api/v1/rules/test-ns/test-gr');
|
expect(group.path).toBe('/api/ruler/mimir-1/api/v1/rules/test-ns/test-gr');
|
||||||
expect(group.params).toMatchObject({ subtype: 'mimir' });
|
expect(group.params).toMatchObject({ subtype: 'mimir' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should append source=rules parameter when custom ruler enabled', () => {
|
it('Should append subtype parameter when custom ruler enabled', () => {
|
||||||
// Arrange
|
|
||||||
const config: RulerDataSourceConfig = {
|
|
||||||
dataSourceName: 'Cortex v2',
|
|
||||||
apiVersion: 'config',
|
|
||||||
};
|
|
||||||
|
|
||||||
mocks.getDatasourceAPIUId.mockReturnValue('ds-uid');
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const builder = rulerUrlBuilder(config);
|
const builder = rulerUrlBuilder(mimirConfig);
|
||||||
|
|
||||||
const rules = builder.rules();
|
const rules = builder.rules();
|
||||||
const namespace = builder.namespace('test-ns');
|
const namespace = builder.namespace('test-ns');
|
||||||
@ -88,19 +83,52 @@ describe('rulerUrlBuilder', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Should append dashboard_uid and panel_id for rules endpoint when specified', () => {
|
it('Should append dashboard_uid and panel_id for rules endpoint when specified', () => {
|
||||||
// Arrange
|
|
||||||
const config: RulerDataSourceConfig = {
|
|
||||||
dataSourceName: 'Cortex v2',
|
|
||||||
apiVersion: 'config',
|
|
||||||
};
|
|
||||||
|
|
||||||
mocks.getDatasourceAPIUId.mockReturnValue('ds-uid');
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const builder = rulerUrlBuilder(config);
|
const builder = rulerUrlBuilder(mimirConfig);
|
||||||
const rules = builder.rules({ dashboardUID: 'dashboard-uid', panelId: 1234 });
|
const rules = builder.rules({ dashboardUID: 'dashboard-uid', panelId: 1234 });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(rules.params).toMatchObject({ dashboard_uid: 'dashboard-uid', panel_id: '1234', subtype: 'mimir' });
|
expect(rules.params).toMatchObject({ dashboard_uid: 'dashboard-uid', panel_id: '1234', subtype: 'mimir' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('When slash in namespace or group', () => {
|
||||||
|
it('Should use QUERY_NAMESPACE and QUERY_GROUP path placeholders and include names in query string params', () => {
|
||||||
|
// Act
|
||||||
|
const builder = rulerUrlBuilder(mimirConfig);
|
||||||
|
|
||||||
|
const namespace = builder.namespace('test/ns');
|
||||||
|
const group = builder.namespaceGroup('test/ns', 'test/gr');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(namespace.path).toBe('/api/ruler/mimir-1/api/v1/rules/QUERY_NAMESPACE');
|
||||||
|
expect(namespace.params).toMatchObject({ subtype: 'mimir', namespace: 'test/ns' });
|
||||||
|
|
||||||
|
expect(group.path).toBe('/api/ruler/mimir-1/api/v1/rules/QUERY_NAMESPACE/QUERY_GROUP');
|
||||||
|
expect(group.params).toMatchObject({ subtype: 'mimir', namespace: 'test/ns', group: 'test/gr' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should use the tag replacement only when the slash is present', () => {
|
||||||
|
// Act
|
||||||
|
const builder = rulerUrlBuilder(mimirConfig);
|
||||||
|
|
||||||
|
const group = builder.namespaceGroup('test-ns', 'test/gr');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(group.path).toBe('/api/ruler/mimir-1/api/v1/rules/test-ns/QUERY_GROUP');
|
||||||
|
expect(group.params).toMatchObject({ subtype: 'mimir', group: 'test/gr' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// GMA uses folderUIDs as namespaces and they should never contain slashes
|
||||||
|
it('Should only replace the group segment for Grafana-managed rules', () => {
|
||||||
|
// Act
|
||||||
|
const builder = rulerUrlBuilder(grafanaConfig);
|
||||||
|
|
||||||
|
const group = builder.namespaceGroup('test/ns', 'test/gr');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(group.path).toBe(`/api/ruler/grafana/api/v1/rules/${encodeURIComponent('test/ns')}/QUERY_GROUP`);
|
||||||
|
expect(group.params).toHaveProperty('group');
|
||||||
|
expect(group.params).not.toHaveProperty('namespace');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -5,10 +5,11 @@ import { FetchResponse, getBackendSrv } from '@grafana/runtime';
|
|||||||
import { RulerDataSourceConfig } from 'app/types/unified-alerting';
|
import { RulerDataSourceConfig } from 'app/types/unified-alerting';
|
||||||
import { PostableRulerRuleGroupDTO, RulerRuleGroupDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
|
import { PostableRulerRuleGroupDTO, RulerRuleGroupDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
|
import { checkForPathSeparator } from '../components/rule-editor/util';
|
||||||
import { RULER_NOT_SUPPORTED_MSG } from '../utils/constants';
|
import { RULER_NOT_SUPPORTED_MSG } from '../utils/constants';
|
||||||
import { getDatasourceAPIUid, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
import { getDatasourceAPIUid, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||||
|
|
||||||
import { prepareRulesFilterQueryParams } from './prometheus';
|
import { getRulesFilterSearchParams } from './prometheus';
|
||||||
|
|
||||||
interface ErrorResponseMessage {
|
interface ErrorResponseMessage {
|
||||||
message?: string;
|
message?: string;
|
||||||
@ -20,34 +21,92 @@ export interface RulerRequestUrl {
|
|||||||
params?: Record<string, string>;
|
params?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QUERY_NAMESPACE_TAG = 'QUERY_NAMESPACE';
|
||||||
|
const QUERY_GROUP_TAG = 'QUERY_GROUP';
|
||||||
|
|
||||||
export function rulerUrlBuilder(rulerConfig: RulerDataSourceConfig) {
|
export function rulerUrlBuilder(rulerConfig: RulerDataSourceConfig) {
|
||||||
const grafanaServerPath = `/api/ruler/${getDatasourceAPIUid(rulerConfig.dataSourceName)}`;
|
const rulerPath = getRulerPath(rulerConfig);
|
||||||
|
const queryDetailsProvider = getQueryDetailsProvider(rulerConfig);
|
||||||
|
|
||||||
const rulerPath = `${grafanaServerPath}/api/v1/rules`;
|
const subtype = rulerConfig.apiVersion === 'legacy' ? 'cortex' : 'mimir';
|
||||||
const rulerSearchParams = new URLSearchParams();
|
|
||||||
|
|
||||||
rulerSearchParams.set('subtype', rulerConfig.apiVersion === 'legacy' ? 'cortex' : 'mimir');
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rules: (filter?: FetchRulerRulesFilter): RulerRequestUrl => {
|
rules: (filter?: FetchRulerRulesFilter): RulerRequestUrl => ({
|
||||||
const params = prepareRulesFilterQueryParams(rulerSearchParams, filter);
|
path: rulerPath,
|
||||||
|
params: { subtype, ...getRulesFilterSearchParams(filter) },
|
||||||
|
}),
|
||||||
|
|
||||||
|
namespace: (namespace: string): RulerRequestUrl => {
|
||||||
|
// To handle slashes we need to convert namespace to a query parameter
|
||||||
|
const { namespace: finalNs, searchParams: nsParams } = queryDetailsProvider.namespace(namespace);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
path: `${rulerPath}`,
|
path: `${rulerPath}/${encodeURIComponent(finalNs)}`,
|
||||||
params: params,
|
params: { subtype, ...nsParams },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
namespaceGroup: (namespaceUID: string, group: string): RulerRequestUrl => {
|
||||||
|
const { namespace: finalNs, searchParams: nsParams } = queryDetailsProvider.namespace(namespaceUID);
|
||||||
|
const { group: finalGroup, searchParams: groupParams } = queryDetailsProvider.group(group);
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: `${rulerPath}/${encodeURIComponent(finalNs)}/${encodeURIComponent(finalGroup)}`,
|
||||||
|
params: { subtype, ...nsParams, ...groupParams },
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
namespace: (namespace: string): RulerRequestUrl => ({
|
|
||||||
path: `${rulerPath}/${encodeURIComponent(namespace)}`,
|
|
||||||
params: Object.fromEntries(rulerSearchParams),
|
|
||||||
}),
|
|
||||||
namespaceGroup: (namespaceUID: string, group: string): RulerRequestUrl => ({
|
|
||||||
path: `${rulerPath}/${encodeURIComponent(namespaceUID)}/${encodeURIComponent(group)}`,
|
|
||||||
params: Object.fromEntries(rulerSearchParams),
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface NamespaceUrlParams {
|
||||||
|
namespace: string;
|
||||||
|
searchParams: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GroupUrlParams {
|
||||||
|
group: string;
|
||||||
|
searchParams: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RulerQueryDetailsProvider {
|
||||||
|
namespace: (namespace: string) => NamespaceUrlParams;
|
||||||
|
group: (group: string) => GroupUrlParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getQueryDetailsProvider(rulerConfig: RulerDataSourceConfig): RulerQueryDetailsProvider {
|
||||||
|
const isGrafanaDatasource = rulerConfig.dataSourceName === GRAFANA_RULES_SOURCE_NAME;
|
||||||
|
|
||||||
|
const groupParamRewrite = (group: string): GroupUrlParams => {
|
||||||
|
if (checkForPathSeparator(group) !== true) {
|
||||||
|
return { group: QUERY_GROUP_TAG, searchParams: { group } };
|
||||||
|
}
|
||||||
|
return { group, searchParams: {} };
|
||||||
|
};
|
||||||
|
|
||||||
|
// GMA uses folderUID as namespace identifiers so we need to rewrite them
|
||||||
|
if (isGrafanaDatasource) {
|
||||||
|
return {
|
||||||
|
namespace: (namespace: string) => ({ namespace, searchParams: {} }),
|
||||||
|
group: groupParamRewrite,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
namespace: (namespace: string): NamespaceUrlParams => {
|
||||||
|
if (checkForPathSeparator(namespace) !== true) {
|
||||||
|
return { namespace: QUERY_NAMESPACE_TAG, searchParams: { namespace } };
|
||||||
|
}
|
||||||
|
return { namespace, searchParams: {} };
|
||||||
|
},
|
||||||
|
group: groupParamRewrite,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRulerPath(rulerConfig: RulerDataSourceConfig) {
|
||||||
|
const grafanaServerPath = `/api/ruler/${getDatasourceAPIUid(rulerConfig.dataSourceName)}`;
|
||||||
|
return `${grafanaServerPath}/api/v1/rules`;
|
||||||
|
}
|
||||||
|
|
||||||
// upsert a rule group. use this to update rule
|
// upsert a rule group. use this to update rule
|
||||||
export async function setRulerRuleGroup(
|
export async function setRulerRuleGroup(
|
||||||
rulerConfig: RulerDataSourceConfig,
|
rulerConfig: RulerDataSourceConfig,
|
||||||
|
Loading…
Reference in New Issue
Block a user