@@ -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.
115138func (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).
335413func validateUserTemplate (t * alert.Template ) error {
336414 // TODO move to some better place
0 commit comments