Skip to content

Commit 6d4e158

Browse files
authored
Revert "Revert "PMM-8273 download templates from SaaS (#786)" (#801)" PMM-8710 (#816)
* Revert "Revert "PMM-8273 download templates from SaaS (#786)" (#801)" PMM-8273 was mistakenly merged and later reverted. Since IA template is now available on SaaS we can re-introduce the feature. This reverts commit 3c660c3.
1 parent 9a5d465 commit 6d4e158

10 files changed

Lines changed: 238 additions & 104 deletions

File tree

main.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -649,7 +649,10 @@ func main() {
649649
}
650650

651651
// Integrated alerts services
652-
templatesService := ia.NewTemplatesService(db)
652+
templatesService, err := ia.NewTemplatesService(db)
653+
if err != nil {
654+
l.Fatalf("Could not create templates service: %s", err)
655+
}
653656
rulesService := ia.NewRulesService(db, templatesService, vmalert, alertmanager)
654657
alertsService := ia.NewAlertsService(db, alertmanager, templatesService)
655658

services/checks/checks.go

Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,13 @@ import (
4343
"github.com/percona/pmm-managed/services"
4444
"github.com/percona/pmm-managed/utils/envvars"
4545
"github.com/percona/pmm-managed/utils/saasdial"
46+
"github.com/percona/pmm-managed/utils/signatures"
4647
)
4748

4849
const (
4950
defaultStartDelay = time.Minute
5051

5152
// Environment variables that affect checks service; only for testing.
52-
envPublicKey = "PERCONA_TEST_CHECKS_PUBLIC_KEY"
5353
envCheckFile = "PERCONA_TEST_CHECKS_FILE"
5454
envResendInterval = "PERCONA_TEST_CHECKS_RESEND_INTERVAL"
5555
envDisableStartDelay = "PERCONA_TEST_CHECKS_DISABLE_START_DELAY"
@@ -78,12 +78,6 @@ var (
7878
pmmAgentInvalid = version.MustParse("3.0.0-invalid")
7979
)
8080

81-
var defaultPublicKeys = []string{
82-
"RWTfyQTP3R7VzZggYY7dzuCbuCQWqTiGCqOvWRRAMVEiw0eSxHMVBBE5", // PMM 2.6
83-
"RWRxgu1w3alvJsQf+sHVUYiF6guAdEsBWXDe8jHZuB9dXVE9b5vw7ONM", // PMM 2.12
84-
"RWTHhufOlJ38dWt+DrprOg702YvZgqQJsx1XKfzF+MaB/pe9eCJgKkiF", // PMM 2.17
85-
}
86-
8781
// Service is responsible for interactions with Percona Check service.
8882
type Service struct {
8983
agentsRegistry agentsRegistry
@@ -135,7 +129,6 @@ func New(agentsRegistry agentsRegistry, alertmanagerService alertmanagerService,
135129

136130
l: l,
137131
host: host,
138-
publicKeys: defaultPublicKeys,
139132
startDelay: defaultStartDelay,
140133
resendInterval: defaultResendInterval,
141134
localChecksFile: os.Getenv(envCheckFile),
@@ -155,9 +148,9 @@ func New(agentsRegistry agentsRegistry, alertmanagerService alertmanagerService,
155148
}, []string{"service_type", "check_type"}),
156149
}
157150

158-
if k := os.Getenv(envPublicKey); k != "" {
159-
s.publicKeys = strings.Split(k, ",")
151+
if k := envvars.GetPublicKeys(); k != nil {
160152
l.Warnf("Public keys changed to %q.", k)
153+
s.publicKeys = k
161154
}
162155
if d, _ := strconv.ParseBool(os.Getenv(envDisableStartDelay)); d {
163156
l.Warn("Start delay disabled.")
@@ -1081,7 +1074,7 @@ func (s *Service) downloadChecks(ctx context.Context) ([]check.Check, error) {
10811074
return nil, errors.Wrap(err, "failed to request checks service")
10821075
}
10831076

1084-
if err = s.verifySignatures(resp); err != nil {
1077+
if err = signatures.Verify(s.l, resp.File, resp.Signatures, s.publicKeys); err != nil {
10851078
return nil, err
10861079
}
10871080

@@ -1147,26 +1140,6 @@ func (s *Service) UpdateIntervals(rare, standard, frequent time.Duration) {
11471140
s.l.Infof("Intervals are changed: rare %s, standard %s, frequent %s", rare, standard, frequent)
11481141
}
11491142

1150-
// verifySignatures verifies checks signatures and returns error in case of verification problem.
1151-
func (s *Service) verifySignatures(resp *api.GetAllChecksResponse) error {
1152-
if len(resp.Signatures) == 0 {
1153-
return errors.New("zero signatures received")
1154-
}
1155-
1156-
var err error
1157-
for _, sign := range resp.Signatures {
1158-
for _, key := range s.publicKeys {
1159-
if err = check.Verify([]byte(resp.File), key, sign); err == nil {
1160-
s.l.Debugf("Key %q matches signature %q.", key, sign)
1161-
return nil
1162-
}
1163-
s.l.Debugf("Key %q doesn't match signature %q: %s.", key, sign, err)
1164-
}
1165-
}
1166-
1167-
return errors.New("no verified signatures")
1168-
}
1169-
11701143
// Describe implements prom.Collector.
11711144
func (s *Service) Describe(ch chan<- *prom.Desc) {
11721145
s.mScriptsExecuted.Describe(ch)

services/checks/checks_test.go

Lines changed: 0 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import (
2323
"time"
2424

2525
"github.com/AlekSi/pointer"
26-
api "github.com/percona-platform/saas/gen/check/retrieval"
2726
"github.com/percona-platform/saas/pkg/check"
2827
"github.com/percona/pmm/version"
2928
promtest "github.com/prometheus/client_golang/prometheus/testutil"
@@ -297,63 +296,6 @@ func TestSTTMetrics(t *testing.T) {
297296
assert.NoError(t, promtest.CollectAndCompare(s, expected))
298297
})
299298
}
300-
301-
func TestVerifySignatures(t *testing.T) {
302-
t.Parallel()
303-
304-
t.Run("normal", func(t *testing.T) {
305-
t.Parallel()
306-
307-
s, err := New(nil, nil, nil)
308-
require.NoError(t, err)
309-
s.host = devChecksHost
310-
311-
validKey := "RWSdGihBPffV2c4IysqHAIxc5c5PLfmQStbRPkuLXDr3igJOqFWt7aml"
312-
invalidKey := "RWSdGihBPffV2c4IysqHAIxc5c5PLfmQStbRPkuLXDr3igJO+INVALID"
313-
314-
s.publicKeys = []string{invalidKey, validKey}
315-
316-
validSign := strings.TrimSpace(`
317-
untrusted comment: signature from minisign secret key
318-
RWSdGihBPffV2W/zvmIiTLh8UnocoF3OcwmczGdZ+zM13eRnm2Qq9YxfQ9cLzAp1dA5w7C5a3Cp5D7jlYiydu5hqZhJUxJt/ugg=
319-
trusted comment: some comment
320-
uEF33ScMPYpvHvBKv8+yBkJ9k4+DCfV4nDs6kKYwGhalvkkqwWkyfJffO+KW7a1m3y42WHpOnzBxLJeU/AuzDw==
321-
`)
322-
323-
invalidSign := strings.TrimSpace(`
324-
untrusted comment: signature from minisign secret key
325-
RWSdGihBPffV2W/zvmIiTLh8UnocoF3OcwmczGdZ+zM13eRnm2Qq9YxfQ9cLzAp1dA5w7C5a3Cp5D7jlYiydu5hqZhJ+INVALID=
326-
trusted comment: some comment
327-
uEF33ScMPYpvHvBKv8+yBkJ9k4+DCfV4nDs6kKYwGhalvkkqwWkyfJffO+KW7a1m3y42WHpOnzBxLJ+INVALID==
328-
`)
329-
330-
resp := api.GetAllChecksResponse{
331-
File: "random data",
332-
Signatures: []string{invalidSign, validSign},
333-
}
334-
335-
err = s.verifySignatures(&resp)
336-
assert.NoError(t, err)
337-
})
338-
339-
t.Run("empty signatures", func(t *testing.T) {
340-
t.Parallel()
341-
342-
s, err := New(nil, nil, nil)
343-
require.NoError(t, err)
344-
s.host = devChecksHost
345-
s.publicKeys = []string{"RWSdGihBPffV2c4IysqHAIxc5c5PLfmQStbRPkuLXDr3igJOqFWt7aml"}
346-
347-
resp := api.GetAllChecksResponse{
348-
File: "random data",
349-
Signatures: []string{},
350-
}
351-
352-
err = s.verifySignatures(&resp)
353-
assert.EqualError(t, err, "zero signatures received")
354-
})
355-
}
356-
357299
func TestGetSecurityCheckResults(t *testing.T) {
358300
sqlDB := testdb.Open(t, models.SkipFixtures, nil)
359301
db := reform.NewDB(sqlDB, postgresql.Dialect, nil)

services/management/ia/alerts_service_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,8 @@ func TestListAlerts(t *testing.T) {
271271
}
272272
mockAlert.On("GetAlerts", ctx).Return(mockedAlerts, nil)
273273

274-
tmplSvc := NewTemplatesService(db)
274+
tmplSvc, err := NewTemplatesService(db)
275+
require.NoError(t, err)
275276
tmplSvc.Collect(ctx)
276277
svc := NewAlertsService(db, mockAlert, tmplSvc)
277278

services/management/ia/rules_service_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ func TestCreateAlertRule(t *testing.T) {
7272
channelID := respC.ChannelId
7373

7474
// Load test templates
75-
templates := NewTemplatesService(db)
75+
templates, err := NewTemplatesService(db)
76+
require.NoError(t, err)
7677
templates.userTemplatesPath = testTemplates2
7778
templates.Collect(ctx)
7879

services/management/ia/templates_service.go

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"text/template"
2828
"time"
2929

30+
api "github.com/percona-platform/saas/gen/check/retrieval"
3031
"github.com/percona-platform/saas/pkg/alert"
3132
"github.com/percona-platform/saas/pkg/common"
3233
iav1beta1 "github.com/percona/pmm/api/managementpb/ia"
@@ -40,9 +41,14 @@ import (
4041
"github.com/percona/pmm-managed/data"
4142
"github.com/percona/pmm-managed/models"
4243
"github.com/percona/pmm-managed/utils/dir"
44+
"github.com/percona/pmm-managed/utils/envvars"
45+
"github.com/percona/pmm-managed/utils/saasdial"
46+
"github.com/percona/pmm-managed/utils/signatures"
4347
)
4448

45-
const templatesDir = "/srv/ia/templates"
49+
const (
50+
templatesDir = "/srv/ia/templates"
51+
)
4652

4753
// templateInfo represents alerting rule template information from various sources.
4854
//
@@ -61,25 +67,41 @@ type TemplatesService struct {
6167
l *logrus.Entry
6268
userTemplatesPath string
6369

70+
host string
71+
publicKeys []string
72+
6473
rw sync.RWMutex
6574
templates map[string]templateInfo
6675
}
6776

6877
// NewTemplatesService creates a new TemplatesService.
69-
func NewTemplatesService(db *reform.DB) *TemplatesService {
78+
func NewTemplatesService(db *reform.DB) (*TemplatesService, error) {
7079
l := logrus.WithField("component", "management/ia/templates")
7180

7281
err := dir.CreateDataDir(templatesDir, "pmm", "pmm", dirPerm)
7382
if err != nil {
7483
l.Error(err)
7584
}
7685

77-
return &TemplatesService{
86+
host, err := envvars.GetSAASHost()
87+
if err != nil {
88+
return nil, err
89+
}
90+
91+
s := &TemplatesService{
7892
db: db,
7993
l: l,
8094
userTemplatesPath: templatesDir,
95+
host: host,
8196
templates: make(map[string]templateInfo),
8297
}
98+
99+
if k := envvars.GetPublicKeys(); k != nil {
100+
l.Warnf("Public keys changed to %q.", k)
101+
s.publicKeys = k
102+
}
103+
104+
return s, nil
83105
}
84106

85107
// Enabled returns if service is enabled and can be used.
@@ -110,6 +132,7 @@ func (s *TemplatesService) getTemplates() map[string]templateInfo {
110132

111133
// Collect collects IA rule templates from various sources like:
112134
// builtin templates: read from the generated code in bindata.go.
135+
// SaaS templates: templates downloaded from checks service.
113136
// user file templates: read from yaml files created by the user in `/srv/ia/templates`
114137
// user API templates: in the DB created using the API.
115138
func (s *TemplatesService) Collect(ctx context.Context) {
@@ -131,7 +154,14 @@ func (s *TemplatesService) Collect(ctx context.Context) {
131154
return
132155
}
133156

134-
templates := make([]templateInfo, 0, len(builtInTemplates)+len(userDefinedTemplates)+len(dbTemplates))
157+
saasTemplates, err := s.downloadTemplates(ctx)
158+
if err != nil {
159+
// just log the error and don't return, if the user is not connected to SaaS
160+
// we should still collect and show the Built-In templates.
161+
s.l.Errorf("Failed to download rule templates from SaaS: %s.", err)
162+
}
163+
164+
templates := make([]templateInfo, 0, len(builtInTemplates)+len(userDefinedTemplates)+len(dbTemplates)+len(saasTemplates))
135165

136166
for _, t := range builtInTemplates {
137167
templates = append(templates, templateInfo{
@@ -147,9 +177,14 @@ func (s *TemplatesService) Collect(ctx context.Context) {
147177
})
148178
}
149179

150-
templates = append(templates, dbTemplates...)
180+
for _, t := range saasTemplates {
181+
templates = append(templates, templateInfo{
182+
Template: t,
183+
Source: iav1beta1.TemplateSource_SAAS,
184+
})
185+
}
151186

152-
// TODO download templates from SAAS.
187+
templates = append(templates, dbTemplates...)
153188

154189
// replace previously stored templates with newly collected ones.
155190
s.rw.Lock()
@@ -331,6 +366,49 @@ func (s *TemplatesService) loadTemplatesFromDB() ([]templateInfo, error) {
331366
return res, nil
332367
}
333368

369+
// downloadTemplates downloads IA templates from SaaS.
370+
func (s *TemplatesService) downloadTemplates(ctx context.Context) ([]alert.Template, error) {
371+
s.l.Infof("Downloading templates from %s ...", s.host)
372+
373+
settings, err := models.GetSettings(s.db)
374+
if err != nil {
375+
return nil, err
376+
}
377+
378+
cc, err := saasdial.Dial(ctx, settings.SaaS.SessionID, s.host)
379+
if err != nil {
380+
return nil, errors.Wrap(err, "failed to dial")
381+
}
382+
defer cc.Close() //nolint:errcheck
383+
384+
resp, err := api.NewRetrievalAPIClient(cc).GetAllAlertRuleTemplates(ctx, &api.GetAllAlertRuleTemplatesRequest{})
385+
if err != nil {
386+
// if credentials are invalid then force a logout so that the next check download
387+
// attempt can be successful.
388+
logoutErr := saasdial.LogoutIfInvalidAuth(s.db, s.l, err)
389+
if logoutErr != nil {
390+
s.l.Warnf("Failed to force logout: %v", logoutErr)
391+
}
392+
return nil, errors.Wrap(err, "failed to request checks service")
393+
}
394+
395+
if err = signatures.Verify(s.l, resp.File, resp.Signatures, s.publicKeys); err != nil {
396+
return nil, err
397+
}
398+
399+
// be liberal about files from SaaS for smooth transition to future versions
400+
params := &alert.ParseParams{
401+
DisallowUnknownFields: false,
402+
DisallowInvalidTemplates: false,
403+
}
404+
templates, err := alert.Parse(strings.NewReader(resp.File), params)
405+
if err != nil {
406+
return nil, err
407+
}
408+
409+
return templates, nil
410+
}
411+
334412
// validateUserTemplate validates user-provided template (API or file).
335413
func validateUserTemplate(t *alert.Template) error {
336414
// TODO move to some better place

services/management/ia/templates_service_test.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,17 @@ func TestCollect(t *testing.T) {
4444
t.Run("builtin are valid", func(t *testing.T) {
4545
t.Parallel()
4646

47-
svc := NewTemplatesService(db)
48-
_, err := svc.loadTemplatesFromAssets(ctx)
47+
svc, err := NewTemplatesService(db)
48+
require.NoError(t, err)
49+
_, err = svc.loadTemplatesFromAssets(ctx)
4950
require.NoError(t, err)
5051
})
5152

5253
t.Run("bad template paths", func(t *testing.T) {
5354
t.Parallel()
5455

55-
svc := NewTemplatesService(db)
56+
svc, err := NewTemplatesService(db)
57+
require.NoError(t, err)
5658
svc.userTemplatesPath = testBadTemplates
5759
templates, err := svc.loadTemplatesFromUserFiles(ctx)
5860
assert.NoError(t, err)
@@ -62,7 +64,8 @@ func TestCollect(t *testing.T) {
6264
t.Run("valid template paths", func(t *testing.T) {
6365
t.Parallel()
6466

65-
svc := NewTemplatesService(db)
67+
svc, err := NewTemplatesService(db)
68+
require.NoError(t, err)
6669
svc.userTemplatesPath = testTemplates2
6770
svc.Collect(ctx)
6871

@@ -136,7 +139,8 @@ templates:
136139
summary: MySQL too many connections (instance {{ $labels.instance }})
137140
`
138141

139-
svc := NewTemplatesService(db)
142+
svc, err := NewTemplatesService(db)
143+
require.NoError(t, err)
140144
resp, err := svc.CreateTemplate(ctx, &iav1beta1.CreateTemplateRequest{
141145
Yaml: templateWithMissingParam,
142146
})
@@ -228,7 +232,8 @@ templates:
228232
summary: MySQL too many connections (instance {{ $labels.instance }})
229233
`
230234

231-
svc := NewTemplatesService(db)
235+
svc, err := NewTemplatesService(db)
236+
require.NoError(t, err)
232237
createResp, err := svc.CreateTemplate(ctx, &iav1beta1.CreateTemplateRequest{
233238
Yaml: validTemplate,
234239
})

0 commit comments

Comments
 (0)