The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/ngalert/store/alert_rule.go

652 lines
22 KiB

package store
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/util"
)
// AlertRuleMaxTitleLength is the maximum length of the alert rule title
const AlertRuleMaxTitleLength = 190
// AlertRuleMaxRuleGroupNameLength is the maximum length of the alert rule group name
const AlertRuleMaxRuleGroupNameLength = 190
type UpdateRuleGroupCmd struct {
OrgID int64
NamespaceUID string
RuleGroupConfig apimodels.PostableRuleGroupConfig
}
type UpsertRule struct {
Existing *ngmodels.AlertRule
New ngmodels.AlertRule
}
// Store is the interface for persisting alert rules and instances
type RuleStore interface {
DeleteAlertRuleByUID(ctx context.Context, orgID int64, ruleUID string) error
DeleteNamespaceAlertRules(ctx context.Context, orgID int64, namespaceUID string) ([]string, error)
DeleteRuleGroupAlertRules(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string) ([]string, error)
DeleteAlertInstancesByRuleUID(ctx context.Context, orgID int64, ruleUID string) error
GetAlertRuleByUID(ctx context.Context, query *ngmodels.GetAlertRuleByUIDQuery) error
GetAlertRulesForScheduling(ctx context.Context, query *ngmodels.ListAlertRulesQuery) error
GetOrgAlertRules(ctx context.Context, query *ngmodels.ListAlertRulesQuery) error
GetNamespaceAlertRules(ctx context.Context, query *ngmodels.ListNamespaceAlertRulesQuery) error
GetRuleGroupAlertRules(ctx context.Context, query *ngmodels.ListRuleGroupAlertRulesQuery) error
GetNamespaces(context.Context, int64, *models.SignedInUser) (map[string]*models.Folder, error)
GetNamespaceByTitle(context.Context, string, int64, *models.SignedInUser, bool) (*models.Folder, error)
GetOrgRuleGroups(ctx context.Context, query *ngmodels.ListOrgRuleGroupsQuery) error
UpsertAlertRules(ctx context.Context, rule []UpsertRule) error
UpdateRuleGroup(ctx context.Context, cmd UpdateRuleGroupCmd) error
}
func getAlertRuleByUID(sess *sqlstore.DBSession, alertRuleUID string, orgID int64) (*ngmodels.AlertRule, error) {
// we consider optionally enabling some caching
alertRule := ngmodels.AlertRule{OrgID: orgID, UID: alertRuleUID}
has, err := sess.Get(&alertRule)
if err != nil {
return nil, err
}
if !has {
return nil, ngmodels.ErrAlertRuleNotFound
}
return &alertRule, nil
}
// DeleteAlertRuleByUID is a handler for deleting an alert rule.
func (st DBstore) DeleteAlertRuleByUID(ctx context.Context, orgID int64, ruleUID string) error {
return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
_, err := sess.Exec("DELETE FROM alert_rule WHERE org_id = ? AND uid = ?", orgID, ruleUID)
if err != nil {
return err
}
_, err = sess.Exec("DELETE FROM alert_rule_version WHERE rule_org_id = ? and rule_uid = ?", orgID, ruleUID)
if err != nil {
return err
}
_, err = sess.Exec("DELETE FROM alert_instance WHERE rule_org_id = ? AND rule_uid = ?", orgID, ruleUID)
if err != nil {
return err
}
return nil
})
}
// DeleteNamespaceAlertRules is a handler for deleting namespace alert rules. A list of deleted rule UIDs are returned.
func (st DBstore) DeleteNamespaceAlertRules(ctx context.Context, orgID int64, namespaceUID string) ([]string, error) {
ruleUIDs := []string{}
err := st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
if err := sess.SQL("SELECT uid FROM alert_rule WHERE org_id = ? and namespace_uid = ?", orgID, namespaceUID).Find(&ruleUIDs); err != nil {
return err
}
if _, err := sess.Exec("DELETE FROM alert_rule WHERE org_id = ? and namespace_uid = ?", orgID, namespaceUID); err != nil {
return err
}
if _, err := sess.Exec("DELETE FROM alert_rule WHERE org_id = ? and namespace_uid = ?", orgID, namespaceUID); err != nil {
return err
}
if _, err := sess.Exec("DELETE FROM alert_rule_version WHERE rule_org_id = ? and rule_namespace_uid = ?", orgID, namespaceUID); err != nil {
return err
}
if _, err := sess.Exec(`DELETE FROM alert_instance WHERE rule_org_id = ? AND rule_uid NOT IN (
SELECT uid FROM alert_rule where org_id = ?
)`, orgID, orgID); err != nil {
return err
}
return nil
})
return ruleUIDs, err
}
// DeleteRuleGroupAlertRules is a handler for deleting rule group alert rules. A list of deleted rule UIDs are returned.
func (st DBstore) DeleteRuleGroupAlertRules(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string) ([]string, error) {
ruleUIDs := []string{}
err := st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
if err := sess.SQL("SELECT uid FROM alert_rule WHERE org_id = ? and namespace_uid = ? and rule_group = ?",
orgID, namespaceUID, ruleGroup).Find(&ruleUIDs); err != nil {
return err
}
exist, err := sess.Exist(&ngmodels.AlertRule{OrgID: orgID, NamespaceUID: namespaceUID, RuleGroup: ruleGroup})
if err != nil {
return err
}
if !exist {
return ngmodels.ErrRuleGroupNamespaceNotFound
}
if _, err := sess.Exec("DELETE FROM alert_rule WHERE org_id = ? and namespace_uid = ? and rule_group = ?", orgID, namespaceUID, ruleGroup); err != nil {
return err
}
if _, err := sess.Exec("DELETE FROM alert_rule_version WHERE rule_org_id = ? and rule_namespace_uid = ? and rule_group = ?", orgID, namespaceUID, ruleGroup); err != nil {
return err
}
if _, err := sess.Exec(`DELETE FROM alert_instance WHERE rule_org_id = ? AND rule_uid NOT IN (
SELECT uid FROM alert_rule where org_id = ?
)`, orgID, orgID); err != nil {
return err
}
return nil
})
return ruleUIDs, err
}
// DeleteAlertInstanceByRuleUID is a handler for deleting alert instances by alert rule UID when a rule has been updated
func (st DBstore) DeleteAlertInstancesByRuleUID(ctx context.Context, orgID int64, ruleUID string) error {
return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
_, err := sess.Exec("DELETE FROM alert_instance WHERE rule_org_id = ? AND rule_uid = ?", orgID, ruleUID)
if err != nil {
return err
}
return nil
})
}
// GetAlertRuleByUID is a handler for retrieving an alert rule from that database by its UID and organisation ID.
// It returns ngmodels.ErrAlertRuleNotFound if no alert rule is found for the provided ID.
func (st DBstore) GetAlertRuleByUID(ctx context.Context, query *ngmodels.GetAlertRuleByUIDQuery) error {
return st.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
alertRule, err := getAlertRuleByUID(sess, query.UID, query.OrgID)
if err != nil {
return err
}
query.Result = alertRule
return nil
})
}
// UpsertAlertRules is a handler for creating/updating alert rules.
func (st DBstore) UpsertAlertRules(ctx context.Context, rules []UpsertRule) error {
return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
newRules := make([]ngmodels.AlertRule, 0, len(rules))
ruleVersions := make([]ngmodels.AlertRuleVersion, 0, len(rules))
for _, r := range rules {
if r.Existing == nil && r.New.UID != "" {
// check by UID
existingAlertRule, err := getAlertRuleByUID(sess, r.New.UID, r.New.OrgID)
if err != nil {
if errors.Is(err, ngmodels.ErrAlertRuleNotFound) {
return fmt.Errorf("failed to get alert rule %s: %w", r.New.UID, err)
}
return err
}
r.Existing = existingAlertRule
}
var parentVersion int64
switch r.Existing {
case nil: // new rule
uid, err := GenerateNewAlertRuleUID(sess, r.New.OrgID, r.New.Title)
if err != nil {
return fmt.Errorf("failed to generate UID for alert rule %q: %w", r.New.Title, err)
}
r.New.UID = uid
if r.New.IntervalSeconds == 0 {
r.New.IntervalSeconds = int64(st.DefaultInterval.Seconds())
}
r.New.Version = 1
if r.New.NoDataState == "" {
// set default no data state
r.New.NoDataState = ngmodels.NoData
}
if r.New.ExecErrState == "" {
// set default error state
r.New.ExecErrState = ngmodels.AlertingErrState
}
if err := st.validateAlertRule(r.New); err != nil {
return err
}
if err := (&r.New).PreSave(TimeNow); err != nil {
return err
}
newRules = append(newRules, r.New)
default:
// explicitly set the existing properties if missing
// do not rely on xorm
if r.New.Title == "" {
r.New.Title = r.Existing.Title
}
if r.New.Condition == "" {
r.New.Condition = r.Existing.Condition
}
if len(r.New.Data) == 0 {
r.New.Data = r.Existing.Data
}
r.New.ID = r.Existing.ID
r.New.OrgID = r.Existing.OrgID
r.New.NamespaceUID = r.Existing.NamespaceUID
r.New.RuleGroup = r.Existing.RuleGroup
r.New.Version = r.Existing.Version + 1
if r.New.ExecErrState == "" {
r.New.ExecErrState = r.Existing.ExecErrState
}
if r.New.NoDataState == "" {
r.New.NoDataState = r.Existing.NoDataState
}
if err := st.validateAlertRule(r.New); err != nil {
return err
}
if err := (&r.New).PreSave(TimeNow); err != nil {
return err
}
// no way to update multiple rules at once
if _, err := sess.ID(r.Existing.ID).AllCols().Update(r.New); err != nil {
return fmt.Errorf("failed to update rule %s: %w", r.New.Title, err)
}
parentVersion = r.Existing.Version
}
ruleVersions = append(ruleVersions, ngmodels.AlertRuleVersion{
RuleOrgID: r.New.OrgID,
RuleUID: r.New.UID,
RuleNamespaceUID: r.New.NamespaceUID,
RuleGroup: r.New.RuleGroup,
ParentVersion: parentVersion,
Version: r.New.Version,
Created: r.New.Updated,
Condition: r.New.Condition,
Title: r.New.Title,
Data: r.New.Data,
IntervalSeconds: r.New.IntervalSeconds,
NoDataState: r.New.NoDataState,
ExecErrState: r.New.ExecErrState,
For: r.New.For,
Annotations: r.New.Annotations,
Labels: r.New.Labels,
})
}
if len(newRules) > 0 {
if _, err := sess.Insert(&newRules); err != nil {
return fmt.Errorf("failed to create new rules: %w", err)
}
}
if len(ruleVersions) > 0 {
if _, err := sess.Insert(&ruleVersions); err != nil {
return fmt.Errorf("failed to create new rule versions: %w", err)
}
}
return nil
})
}
// GetOrgAlertRules is a handler for retrieving alert rules of specific organisation.
func (st DBstore) GetOrgAlertRules(ctx context.Context, query *ngmodels.ListAlertRulesQuery) error {
return st.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
alertRules := make([]*ngmodels.AlertRule, 0)
q := "SELECT * FROM alert_rule WHERE org_id = ?"
params := []interface{}{query.OrgID}
if len(query.NamespaceUIDs) > 0 {
placeholders := make([]string, 0, len(query.NamespaceUIDs))
for _, folderUID := range query.NamespaceUIDs {
params = append(params, folderUID)
placeholders = append(placeholders, "?")
}
q = fmt.Sprintf("%s AND namespace_uid IN (%s)", q, strings.Join(placeholders, ","))
}
if query.DashboardUID != "" {
params = append(params, query.DashboardUID)
q = fmt.Sprintf("%s AND dashboard_uid = ?", q)
if query.PanelID != 0 {
params = append(params, query.PanelID)
q = fmt.Sprintf("%s AND panel_id = ?", q)
}
}
q = fmt.Sprintf("%s ORDER BY id ASC", q)
if err := sess.SQL(q, params...).Find(&alertRules); err != nil {
return err
}
query.Result = alertRules
return nil
})
}
// GetNamespaceAlertRules is a handler for retrieving namespace alert rules of specific organisation.
func (st DBstore) GetNamespaceAlertRules(ctx context.Context, query *ngmodels.ListNamespaceAlertRulesQuery) error {
return st.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
alertRules := make([]*ngmodels.AlertRule, 0)
// TODO rewrite using group by namespace_uid, rule_group
q := "SELECT * FROM alert_rule WHERE org_id = ? and namespace_uid = ?"
if err := sess.SQL(q, query.OrgID, query.NamespaceUID).Find(&alertRules); err != nil {
return err
}
query.Result = alertRules
return nil
})
}
// GetRuleGroupAlertRules is a handler for retrieving rule group alert rules of specific organisation.
func (st DBstore) GetRuleGroupAlertRules(ctx context.Context, query *ngmodels.ListRuleGroupAlertRulesQuery) error {
return st.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
q := "SELECT * FROM alert_rule WHERE org_id = ? and namespace_uid = ? and rule_group = ?"
args := []interface{}{query.OrgID, query.NamespaceUID, query.RuleGroup}
if query.DashboardUID != "" {
q = fmt.Sprintf("%s and dashboard_uid = ?", q)
args = append(args, query.DashboardUID)
if query.PanelID != 0 {
q = fmt.Sprintf("%s and panel_id = ?", q)
args = append(args, query.PanelID)
}
}
alertRules := make([]*ngmodels.AlertRule, 0)
if err := sess.SQL(q, args...).Find(&alertRules); err != nil {
return err
}
query.Result = alertRules
return nil
})
}
// GetNamespaces returns the folders that are visible to the user
func (st DBstore) GetNamespaces(ctx context.Context, orgID int64, user *models.SignedInUser) (map[string]*models.Folder, error) {
s := dashboards.NewFolderService(orgID, user, st.SQLStore)
namespaceMap := make(map[string]*models.Folder)
var page int64 = 1
for {
// if limit is negative; it fetches at most 1000
folders, err := s.GetFolders(ctx, -1, page)
if err != nil {
return nil, err
}
if len(folders) == 0 {
break
}
for _, f := range folders {
namespaceMap[f.Uid] = f
}
page += 1
}
return namespaceMap, nil
}
// GetNamespaceByTitle is a handler for retrieving a namespace by its title. Alerting rules follow a Grafana folder-like structure which we call namespaces.
func (st DBstore) GetNamespaceByTitle(ctx context.Context, namespace string, orgID int64, user *models.SignedInUser, withCanSave bool) (*models.Folder, error) {
s := dashboards.NewFolderService(orgID, user, st.SQLStore)
folder, err := s.GetFolderByTitle(ctx, namespace)
if err != nil {
return nil, err
}
if withCanSave {
g := guardian.New(ctx, folder.Id, orgID, user)
if canSave, err := g.CanSave(); err != nil || !canSave {
if err != nil {
st.Logger.Error("checking can save permission has failed", "userId", user.UserId, "username", user.Login, "namespace", namespace, "orgId", orgID, "error", err)
}
return nil, ngmodels.ErrCannotEditNamespace
}
}
return folder, nil
}
// GetAlertRulesForScheduling returns alert rule info (identifier, interval, version state)
// that is useful for it's scheduling.
func (st DBstore) GetAlertRulesForScheduling(ctx context.Context, query *ngmodels.ListAlertRulesQuery) error {
return st.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
alerts := make([]*ngmodels.AlertRule, 0)
q := "SELECT uid, org_id, interval_seconds, version FROM alert_rule"
if len(query.ExcludeOrgs) > 0 {
q = fmt.Sprintf("%s WHERE org_id NOT IN (%s)", q, strings.Join(strings.Split(strings.Trim(fmt.Sprint(query.ExcludeOrgs), "[]"), " "), ","))
}
if err := sess.SQL(q).Find(&alerts); err != nil {
return err
}
query.Result = alerts
return nil
})
}
// GenerateNewAlertRuleUID generates a unique UID for a rule.
// This is set as a variable so that the tests can override it.
// The ruleTitle is only used by the mocked functions.
var GenerateNewAlertRuleUID = func(sess *sqlstore.DBSession, orgID int64, ruleTitle string) (string, error) {
for i := 0; i < 3; i++ {
uid := util.GenerateShortUID()
exists, err := sess.Where("org_id=? AND uid=?", orgID, uid).Get(&ngmodels.AlertRule{})
if err != nil {
return "", err
}
if !exists {
return uid, nil
}
}
return "", ngmodels.ErrAlertRuleFailedGenerateUniqueUID
}
// validateAlertRule validates the alert rule interval and organisation.
func (st DBstore) validateAlertRule(alertRule ngmodels.AlertRule) error {
if len(alertRule.Data) == 0 {
return fmt.Errorf("%w: no queries or expressions are found", ngmodels.ErrAlertRuleFailedValidation)
}
if alertRule.Title == "" {
return fmt.Errorf("%w: title is empty", ngmodels.ErrAlertRuleFailedValidation)
}
if alertRule.IntervalSeconds%int64(st.BaseInterval.Seconds()) != 0 || alertRule.IntervalSeconds <= 0 {
return fmt.Errorf("%w: interval (%v) should be non-zero and divided exactly by scheduler interval: %v", ngmodels.ErrAlertRuleFailedValidation, time.Duration(alertRule.IntervalSeconds)*time.Second, st.BaseInterval)
}
// enfore max name length in SQLite
if len(alertRule.Title) > AlertRuleMaxTitleLength {
return fmt.Errorf("%w: name length should not be greater than %d", ngmodels.ErrAlertRuleFailedValidation, AlertRuleMaxTitleLength)
}
// enfore max rule group name length in SQLite
if len(alertRule.RuleGroup) > AlertRuleMaxRuleGroupNameLength {
return fmt.Errorf("%w: rule group name length should not be greater than %d", ngmodels.ErrAlertRuleFailedValidation, AlertRuleMaxRuleGroupNameLength)
}
if alertRule.OrgID == 0 {
return fmt.Errorf("%w: no organisation is found", ngmodels.ErrAlertRuleFailedValidation)
}
if alertRule.DashboardUID == nil && alertRule.PanelID != nil {
return fmt.Errorf("%w: cannot have Panel ID without a Dashboard UID", ngmodels.ErrAlertRuleFailedValidation)
}
return nil
}
// UpdateRuleGroup creates new rules and updates and/or deletes existing rules
func (st DBstore) UpdateRuleGroup(ctx context.Context, cmd UpdateRuleGroupCmd) error {
return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
ruleGroup := cmd.RuleGroupConfig.Name
q := &ngmodels.ListRuleGroupAlertRulesQuery{
OrgID: cmd.OrgID,
NamespaceUID: cmd.NamespaceUID,
RuleGroup: ruleGroup,
}
if err := st.GetRuleGroupAlertRules(ctx, q); err != nil {
return err
}
existingGroupRules := q.Result
existingGroupRulesUIDs := make(map[string]ngmodels.AlertRule, len(existingGroupRules))
for _, r := range existingGroupRules {
existingGroupRulesUIDs[r.UID] = *r
}
upsertRules := make([]UpsertRule, 0)
for _, r := range cmd.RuleGroupConfig.Rules {
if r.GrafanaManagedAlert == nil {
continue
}
newAlertRule := ngmodels.AlertRule{
OrgID: cmd.OrgID,
Title: r.GrafanaManagedAlert.Title,
Condition: r.GrafanaManagedAlert.Condition,
Data: r.GrafanaManagedAlert.Data,
UID: r.GrafanaManagedAlert.UID,
IntervalSeconds: int64(time.Duration(cmd.RuleGroupConfig.Interval).Seconds()),
NamespaceUID: cmd.NamespaceUID,
RuleGroup: ruleGroup,
NoDataState: ngmodels.NoDataState(r.GrafanaManagedAlert.NoDataState),
ExecErrState: ngmodels.ExecutionErrorState(r.GrafanaManagedAlert.ExecErrState),
}
if r.ApiRuleNode != nil {
newAlertRule.For = time.Duration(r.ApiRuleNode.For)
newAlertRule.Annotations = r.ApiRuleNode.Annotations
newAlertRule.Labels = r.ApiRuleNode.Labels
}
if s := newAlertRule.Annotations[ngmodels.DashboardUIDAnnotation]; s != "" {
newAlertRule.DashboardUID = &s
}
if s := newAlertRule.Annotations[ngmodels.PanelIDAnnotation]; s != "" {
panelID, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return fmt.Errorf("the %s annotation does not contain a valid Panel ID: %w", ngmodels.PanelIDAnnotation, err)
}
newAlertRule.PanelID = &panelID
}
upsertRule := UpsertRule{
New: newAlertRule,
}
if existingGroupRule, ok := existingGroupRulesUIDs[r.GrafanaManagedAlert.UID]; ok {
upsertRule.Existing = &existingGroupRule
// remove the rule from existingGroupRulesUIDs
delete(existingGroupRulesUIDs, r.GrafanaManagedAlert.UID)
}
upsertRules = append(upsertRules, upsertRule)
}
if err := st.UpsertAlertRules(ctx, upsertRules); err != nil {
if st.SQLStore.Dialect.IsUniqueConstraintViolation(err) {
return ngmodels.ErrAlertRuleUniqueConstraintViolation
}
return err
}
// delete instances for rules that will not be removed
for _, rule := range existingGroupRules {
if _, ok := existingGroupRulesUIDs[rule.UID]; !ok {
if err := st.DeleteAlertInstancesByRuleUID(ctx, cmd.OrgID, rule.UID); err != nil {
return err
}
}
}
// delete the remaining rules
for ruleUID := range existingGroupRulesUIDs {
if err := st.DeleteAlertRuleByUID(ctx, cmd.OrgID, ruleUID); err != nil {
return err
}
}
return nil
})
}
func (st DBstore) GetOrgRuleGroups(ctx context.Context, query *ngmodels.ListOrgRuleGroupsQuery) error {
return st.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
var ruleGroups [][]string
q := `
SELECT DISTINCT
rule_group,
namespace_uid,
(
SELECT title
FROM dashboard
WHERE
org_id = alert_rule.org_id AND
uid = alert_rule.namespace_uid
) AS namespace_title
FROM alert_rule
WHERE org_id = ?`
params := []interface{}{query.OrgID}
if len(query.NamespaceUIDs) > 0 {
placeholders := make([]string, 0, len(query.NamespaceUIDs))
for _, folderUID := range query.NamespaceUIDs {
params = append(params, folderUID)
placeholders = append(placeholders, "?")
}
q = fmt.Sprintf(" %s AND namespace_uid IN (%s)", q, strings.Join(placeholders, ","))
}
if query.DashboardUID != "" {
q = fmt.Sprintf("%s and dashboard_uid = ?", q)
params = append(params, query.DashboardUID)
if query.PanelID != 0 {
q = fmt.Sprintf("%s and panel_id = ?", q)
params = append(params, query.PanelID)
}
}
q = fmt.Sprintf(" %s ORDER BY namespace_title", q)
if err := sess.SQL(q, params...).Find(&ruleGroups); err != nil {
return err
}
query.Result = ruleGroups
return nil
})
}