1package ualert
2
3import (
4	"bytes"
5	"errors"
6	"fmt"
7	"io"
8	"math/rand"
9	"os"
10	"path/filepath"
11	"strconv"
12	"time"
13
14	"github.com/gofrs/uuid"
15	"github.com/matttproud/golang_protobuf_extensions/pbutil"
16	pb "github.com/prometheus/alertmanager/silence/silencepb"
17	"github.com/prometheus/common/model"
18
19	"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
20)
21
22const (
23	// Should be the same as 'NoDataAlertName' in pkg/services/schedule/compat.go.
24	NoDataAlertName = "DatasourceNoData"
25
26	ErrorAlertName = "DatasourceError"
27)
28
29func (m *migration) addSilence(da dashAlert, rule *alertRule) error {
30	if da.State != "paused" {
31		return nil
32	}
33
34	uid, err := uuid.NewV4()
35	if err != nil {
36		return errors.New("failed to create uuid for silence")
37	}
38
39	n, v := getLabelForRouteMatching(rule.UID)
40	s := &pb.MeshSilence{
41		Silence: &pb.Silence{
42			Id: uid.String(),
43			Matchers: []*pb.Matcher{
44				{
45					Type:    pb.Matcher_EQUAL,
46					Name:    n,
47					Pattern: v,
48				},
49			},
50			StartsAt:  time.Now(),
51			EndsAt:    time.Now().Add(365 * 20 * time.Hour), // 1 year.
52			CreatedBy: "Grafana Migration",
53			Comment:   "Created during auto migration to unified alerting",
54		},
55		ExpiresAt: time.Now().Add(365 * 20 * time.Hour), // 1 year.
56	}
57
58	_, ok := m.silences[da.OrgId]
59	if !ok {
60		m.silences[da.OrgId] = make([]*pb.MeshSilence, 0)
61	}
62	m.silences[da.OrgId] = append(m.silences[da.OrgId], s)
63	return nil
64}
65
66func (m *migration) addErrorSilence(da dashAlert, rule *alertRule) error {
67	if da.ParsedSettings.ExecutionErrorState != "keep_state" {
68		return nil
69	}
70
71	uid, err := uuid.NewV4()
72	if err != nil {
73		return errors.New("failed to create uuid for silence")
74	}
75
76	s := &pb.MeshSilence{
77		Silence: &pb.Silence{
78			Id: uid.String(),
79			Matchers: []*pb.Matcher{
80				{
81					Type:    pb.Matcher_EQUAL,
82					Name:    model.AlertNameLabel,
83					Pattern: ErrorAlertName,
84				},
85				{
86					Type:    pb.Matcher_EQUAL,
87					Name:    "rule_uid",
88					Pattern: rule.UID,
89				},
90			},
91			StartsAt:  time.Now(),
92			EndsAt:    time.Now().AddDate(1, 0, 0), // 1 year
93			CreatedBy: "Grafana Migration",
94			Comment:   fmt.Sprintf("Created during migration to unified alerting to silence Error state for alert rule ID '%s' and Title '%s' because the option 'Keep Last State' was selected for Error state", rule.UID, rule.Title),
95		},
96		ExpiresAt: time.Now().AddDate(1, 0, 0), // 1 year
97	}
98	if _, ok := m.silences[da.OrgId]; !ok {
99		m.silences[da.OrgId] = make([]*pb.MeshSilence, 0)
100	}
101	m.silences[da.OrgId] = append(m.silences[da.OrgId], s)
102	return nil
103}
104
105func (m *migration) addNoDataSilence(da dashAlert, rule *alertRule) error {
106	if da.ParsedSettings.NoDataState != "keep_state" {
107		return nil
108	}
109
110	uid, err := uuid.NewV4()
111	if err != nil {
112		return errors.New("failed to create uuid for silence")
113	}
114
115	s := &pb.MeshSilence{
116		Silence: &pb.Silence{
117			Id: uid.String(),
118			Matchers: []*pb.Matcher{
119				{
120					Type:    pb.Matcher_EQUAL,
121					Name:    model.AlertNameLabel,
122					Pattern: NoDataAlertName,
123				},
124				{
125					Type:    pb.Matcher_EQUAL,
126					Name:    "rule_uid",
127					Pattern: rule.UID,
128				},
129			},
130			StartsAt:  time.Now(),
131			EndsAt:    time.Now().AddDate(1, 0, 0), // 1 year.
132			CreatedBy: "Grafana Migration",
133			Comment:   fmt.Sprintf("Created during migration to unified alerting to silence NoData state for alert rule ID '%s' and Title '%s' because the option 'Keep Last State' was selected for NoData state", rule.UID, rule.Title),
134		},
135		ExpiresAt: time.Now().AddDate(1, 0, 0), // 1 year.
136	}
137	_, ok := m.silences[da.OrgId]
138	if !ok {
139		m.silences[da.OrgId] = make([]*pb.MeshSilence, 0)
140	}
141	m.silences[da.OrgId] = append(m.silences[da.OrgId], s)
142	return nil
143}
144
145func (m *migration) writeSilencesFile(orgID int64) error {
146	var buf bytes.Buffer
147	orgSilences, ok := m.silences[orgID]
148	if !ok {
149		return nil
150	}
151
152	for _, e := range orgSilences {
153		if _, err := pbutil.WriteDelimited(&buf, e); err != nil {
154			return err
155		}
156	}
157
158	f, err := openReplace(silencesFileNameForOrg(m.mg, orgID))
159	if err != nil {
160		return err
161	}
162
163	if _, err := io.Copy(f, bytes.NewReader(buf.Bytes())); err != nil {
164		return err
165	}
166
167	return f.Close()
168}
169
170func getSilenceFileNamesForAllOrgs(mg *migrator.Migrator) ([]string, error) {
171	return filepath.Glob(filepath.Join(mg.Cfg.DataPath, "alerting", "*", "silences"))
172}
173
174func silencesFileNameForOrg(mg *migrator.Migrator, orgID int64) string {
175	return filepath.Join(mg.Cfg.DataPath, "alerting", strconv.Itoa(int(orgID)), "silences")
176}
177
178// replaceFile wraps a file that is moved to another filename on closing.
179type replaceFile struct {
180	*os.File
181	filename string
182}
183
184func (f *replaceFile) Close() error {
185	if err := f.File.Sync(); err != nil {
186		return err
187	}
188	if err := f.File.Close(); err != nil {
189		return err
190	}
191	return os.Rename(f.File.Name(), f.filename)
192}
193
194// openReplace opens a new temporary file that is moved to filename on closing.
195func openReplace(filename string) (*replaceFile, error) {
196	tmpFilename := fmt.Sprintf("%s.%x", filename, uint64(rand.Int63()))
197
198	if err := os.MkdirAll(filepath.Dir(tmpFilename), os.ModePerm); err != nil {
199		return nil, err
200	}
201
202	f, err := os.Create(tmpFilename)
203	if err != nil {
204		return nil, err
205	}
206
207	rf := &replaceFile{
208		File:     f,
209		filename: filename,
210	}
211	return rf, nil
212}
213