1 package dashboardmanager
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "fmt"
8 "os"
9 "path/filepath"
10 "regexp"
11 "strconv"
12 "strings"
13 "time"
14
15 "github.com/nsf/jsondiff"
16 monitoring "google.golang.org/api/monitoring/v1"
17 "google.golang.org/api/option"
18
19 "edge-infra.dev/pkg/lib/gcp/monitoring/monutil"
20 )
21
22 var err error
23
24 var (
25 Verbose = false
26 Continues = false
27 Sprintf = fmt.Sprintf
28 )
29
30 type Dashboard struct {
31 *monitoring.Dashboard
32 TemplatePath string
33 }
34 type Client struct {
35 s *monitoring.ProjectsDashboardsService
36 ctx context.Context
37 ProjectID string
38 }
39 type DashTemplate struct {
40 DisplayName string `json:"displayName,omitempty"`
41 Labels map[string]string `json:"labels,omitempty"`
42 ColumnLayout *monitoring.ColumnLayout `json:"columnLayout,omitempty"`
43 RowLayout *monitoring.RowLayout `json:"rowLayout,omitempty"`
44 MosaicLayout *monitoring.MosaicLayout `json:"mosaicLayout,omitempty"`
45 GridLayout *monitoring.GridLayout `json:"gridLayout,omitempty"`
46 DashboardFilters []*monitoring.DashboardFilter `json:"dashboardFilters,omitempty"`
47 }
48
49
50 func New(ctx context.Context, projectID string, opts ...option.ClientOption) (*Client, error) {
51 svc, err := monitoring.NewService(ctx, opts...)
52 if err != nil {
53 return nil, fmt.Errorf("dashboards.New: failed to create dashboards service. error: %w", err)
54 }
55
56 return &Client{
57 s: monitoring.NewProjectsDashboardsService(svc),
58 ctx: ctx,
59 ProjectID: projectID,
60 }, nil
61 }
62
63
64 func ReadDashboardsFromPath(path string) ([]*Dashboard, error) {
65 var sourceDashboards []*Dashboard
66
67
68 if !monutil.IsDirectory(path) && monutil.FileExists(path) && supportedPath(path) {
69 d, err := readDashboardFile(path)
70 if err != nil {
71 return nil, err
72 }
73 sourceDashboards = append(sourceDashboards, d)
74 return sourceDashboards, nil
75 } else if !supportedPath(path) {
76 return nil, fmt.Errorf("ReadDashboardsFromPath: %s folder or file contains unsupported whitespace character (use underscores or dashes)", path)
77 }
78
79
80 files, err := monutil.ListFiles(path, ".json")
81 if evalError(err, "ReadDashboardsFromPath: unable to load dashboard templates from path: %s\n", path) {
82 return nil, err
83 }
84
85
86 for i := 0; i < len(files); i++ {
87 if !supportedPath(files[i]) && !Continues {
88 return nil, fmt.Errorf("ReadDashboardsFromPath: %s contains whitespace in the parent folder or filename which is not currently supported", files[i])
89 } else if !supportedPath(files[i]) {
90 vPrintf("ReadDashboardsFromPath: skipping %s file due to unsupported whitespace in file or parent folder\n", files[i])
91 continue
92 }
93
94 d, err := readDashboardFile(files[i])
95 if evalError(err, "ReadDashboardsFromPath: %s template file error\n", files[i]) {
96 return nil, err
97 } else if err == nil {
98 sourceDashboards = append(sourceDashboards, d)
99 }
100 }
101
102 if len(sourceDashboards) == 0 && !Continues {
103 return nil, fmt.Errorf("ReadDashboardsFromPath: no dashboard templates were found in the path `%s`", path)
104 }
105 return sourceDashboards, nil
106 }
107
108 func (c *Client) createDashboard(d *monitoring.Dashboard) error {
109 response, err := c.s.Create(Sprintf("projects/%s", c.ProjectID), d).Do()
110 if err != nil {
111 return err
112 }
113 vPrintf("%s dashboard has been created successfully\n", response.DisplayName)
114 vPrintf("- name: %s\n", response.Name)
115 vPrintf("- etag: %s\n", response.Etag)
116 return nil
117 }
118
119 func (c *Client) createDashboardValidate(d *monitoring.Dashboard) error {
120 request := c.s.Create(Sprintf("projects/%s", c.ProjectID), d).ValidateOnly(true)
121 response, err := request.Do()
122 if err != nil {
123 if metrics := getMetricErrors(err); len(metrics) > 0 {
124 msg := "('" + strings.Join(metrics[:], "', '") + "')"
125 return fmt.Errorf("%s template is missing the following metrics: %s", d.DisplayName, msg)
126 }
127 return err
128 }
129
130 vPrintf("%s dashboard template is valid\n", response.DisplayName)
131 return nil
132 }
133
134
135 func (c *Client) CreateDashboard(d *Dashboard, validate bool) error {
136 var dashboard *monitoring.Dashboard = &monitoring.Dashboard{
137 DisplayName: d.DisplayName,
138 Labels: d.Labels,
139 ColumnLayout: d.ColumnLayout,
140 GridLayout: d.GridLayout,
141 RowLayout: d.RowLayout,
142 MosaicLayout: d.MosaicLayout,
143 DashboardFilters: d.DashboardFilters,
144 }
145
146
147 if validate {
148 if err := c.createDashboardValidate(dashboard); err != nil {
149 return fmt.Errorf("dashboard validation failed: %w", err)
150 }
151 return nil
152 }
153
154
155 if err := c.createDashboard(dashboard); err != nil {
156 return fmt.Errorf("failed to create dashboard: %w", err)
157 }
158 return nil
159 }
160
161
162 func (d *Dashboard) HasAnyExpirationLabel() bool {
163 r, _ := regexp.Compile("expiration_|orphan-expiration_")
164 labels := d.GetLabels()
165 for i := 0; i < len(labels); i++ {
166 if r.MatchString(labels[i]) {
167 return true
168 }
169 }
170 return false
171 }
172
173
174 func (d *Dashboard) GetLabels() []string {
175 var labels []string
176 for l := range d.Labels {
177 labels = append(labels, l)
178 }
179 return labels
180 }
181
182
183 func (d *Dashboard) GetVersion() string {
184 r, _ := regexp.Compile("v_(.*)")
185 labels := d.GetLabels()
186 for i := 0; i < len(labels); i++ {
187 if r.MatchString(labels[i]) {
188 vString := r.FindStringSubmatch(labels[i])
189 ver := "v" + strings.ReplaceAll(vString[1], "_", ".")
190 return ver
191 }
192 }
193 return ""
194 }
195
196
197
198 func (c *Client) DeleteDashboard(displayName string) error {
199 var names []string
200
201 if isDashboardName(displayName) {
202 names, err = c.lookupDashboardName(displayName, c.ProjectID)
203 if err != nil {
204 return err
205 }
206 } else {
207 names = append(names, displayName)
208 }
209
210 for i := 0; i < len(names); i++ {
211 _, err := c.s.Delete(names[i]).Do()
212 if err != nil {
213 return err
214 }
215 vPrintf("%s was successfully deleted\n", names[i])
216 }
217 return nil
218 }
219
220
221 func (c *Client) GetAllDashboards() ([]*Dashboard, error) {
222 result, err := c.s.List(Sprintf("projects/%s", c.ProjectID)).Do()
223 if err != nil {
224 return nil, err
225 }
226 return newList(result.Dashboards), nil
227 }
228
229
230 func newList(d []*monitoring.Dashboard) []*Dashboard {
231 var dashboards []*Dashboard
232 for i := 0; i < len(d); i++ {
233 dashboards = append(dashboards, &Dashboard{d[i], ""})
234 }
235 return dashboards
236 }
237
238
239 func (c *Client) DisplayNameExists(displayName string) bool {
240 names, err := c.lookupDashboardName(displayName, c.ProjectID)
241 if err != nil || len(names) == 0 {
242 return false
243 }
244 return true
245 }
246
247
248 func (c *Client) GetDashboardByDisplayName(displayName string) ([]*Dashboard, error) {
249 var names []string
250 var projectDashboards []*Dashboard
251
252 if isDashboardName(displayName) {
253 names, err = c.lookupDashboardName(displayName, c.ProjectID)
254 if err != nil {
255 return nil, err
256 }
257 } else {
258 names = append(names, displayName)
259 }
260
261 for i := 0; i < len(names); i++ {
262 d, err := c.getDashboard(names[i])
263 if err != nil {
264 return nil, err
265 }
266
267 projectDashboards = append(projectDashboards, d)
268 }
269 return projectDashboards, nil
270 }
271
272
273 func (c *Client) DeleteDashboardsByLabel(label string) error {
274 dashboardNames, err := c.lookupDashboardsByLabel(label)
275 if err != nil {
276 return err
277 }
278
279 for i := 0; i < len(dashboardNames); i++ {
280 err = c.DeleteDashboard(dashboardNames[i])
281 if err != nil {
282 return err
283 }
284 vPrintf("%s dashboard deleted\n", dashboardNames[i])
285 }
286 return nil
287 }
288
289
290 func (c *Client) ListDashboardsByLabel(label string) ([]string, error) {
291 dNames, err := c.lookupDashboardsByLabel(label)
292 if err != nil {
293 return nil, err
294 }
295
296 var list []string
297 for i := 0; i < len(dNames); i++ {
298 if !inStrArray(dNames[i], list) {
299 list = append(list, dNames[i])
300 }
301 }
302 return list, nil
303 }
304
305
306 func (c *Client) getDashboard(name string) (*Dashboard, error) {
307 d, err := c.s.Get(name).Do()
308 if err != nil {
309 return nil, err
310 }
311 return &Dashboard{d, ""}, err
312 }
313
314
315 func (c *Client) patchDashboard(name string, d *Dashboard) (*Dashboard, error) {
316 response, err := c.s.Patch(name, d.Dashboard).Do()
317 if err != nil {
318 return nil, err
319 }
320 return &Dashboard{response, ""}, nil
321 }
322
323
324 func (c *Client) patchDashboardValidate(name string, d *Dashboard) (*Dashboard, error) {
325 request := c.s.Patch(name, d.Dashboard).ValidateOnly(true)
326 response, err := request.Do()
327 if err != nil {
328 return nil, err
329 }
330 return &Dashboard{response, ""}, nil
331 }
332
333
334 func (c *Client) Rename(newName string, name string, validate bool) error {
335
336 dashboards, err := c.GetDashboardByDisplayName(name)
337 if err != nil {
338 return err
339 }
340
341
342 dashboardNew, _ := c.GetDashboardByDisplayName(newName)
343 if len(dashboardNew) > 0 {
344 return fmt.Errorf("%s dashboard name already exists in the project", newName)
345 }
346
347 if len(dashboards) == 1 && validate {
348 dashboards[0].DisplayName = newName
349 _, err := c.patchDashboardValidate(dashboards[0].Name, dashboards[0])
350 if err != nil {
351 return err
352 }
353 vPrintf("%s dashboard rename request to %s is valid\n", name, newName)
354 } else if len(dashboards) == 1 && !validate {
355 dashboards[0].DisplayName = newName
356 _, err := c.patchDashboard(dashboards[0].Name, dashboards[0])
357 if err != nil {
358 return err
359 }
360 vPrintf("%s dashboard has been renamed to %s\n", name, newName)
361 } else if len(dashboards) > 1 {
362 return fmt.Errorf("%s matches more than one dashboard result", name)
363 }
364 return nil
365 }
366
367
368 func (d *Dashboard) IsUnmanaged() bool {
369 labels := d.GetLabels()
370 if d.HasLabelType("v_") && inStrArray("managed", labels) {
371 return false
372 }
373
374 return true
375 }
376
377
378 func (c *Client) GetDNameIter(displayName string) int {
379 dashboards, _ := c.GetAllDashboards()
380 var dNames []string
381 for i := 0; i < len(dashboards); i++ {
382 if dashboards[i].IsUnmanaged() {
383 dNames = append(dNames, dashboards[i].DisplayName)
384 }
385 }
386
387 return getNextIter(dNames, displayName+` \(Duplicate Name ([\d]+)\)`)
388 }
389
390
391 func inStrArray(s string, array []string) bool {
392 for i := 0; i < len(array); i++ {
393 if array[i] == s {
394 return true
395 }
396 }
397 return false
398 }
399
400
401 func DashboardsFileList(d []*Dashboard) []string {
402 var list []string
403 for _, f := range d {
404 list = append(list, f.TemplatePath)
405 }
406 return list
407 }
408
409
410 func inIntArray(n int, array []int) bool {
411 for i := 0; i < len(array); i++ {
412 if array[i] == n {
413 return true
414 }
415 }
416 return false
417 }
418
419
420 func (c *Client) addLabels(labels []string, d *Dashboard, validate bool) (*Dashboard, error) {
421 for i := 0; i < len(labels); i++ {
422 label := strings.TrimSpace(labels[i])
423 d.AddLabel(label)
424 }
425
426 if !validate {
427 return c.patchDashboard(d.Name, d)
428 }
429 return c.patchDashboardValidate(d.Name, d)
430 }
431
432
433 func (d *Dashboard) AddLabel(label string) {
434 if _, found := d.Labels[label]; found {
435 vPrintf("%s dashboard already has %s label\n", d.DisplayName, label)
436 return
437 } else if d.Labels == nil {
438 varLabel := map[string]string{label: ""}
439 d.Labels = varLabel
440 } else {
441 d.Labels[label] = ""
442 }
443 }
444
445
446 func (c *Client) AddLabelsToDashboard(labels []string, displayName string, validate bool) error {
447 dashboards, err := c.GetDashboardByDisplayName(displayName)
448 if err != nil {
449 return err
450 }
451
452 for i := 0; i < len(dashboards); i++ {
453 _, err := c.addLabels(labels, dashboards[i], validate)
454 if err != nil {
455 return err
456 }
457 }
458 return nil
459 }
460
461
462 func (c *Client) removeLabels(labels []string, d *Dashboard, validate bool) (*Dashboard, error) {
463 for i := 0; i < len(labels); i++ {
464 label := strings.TrimSpace(labels[i])
465 if _, found := d.Labels[label]; !found {
466 vPrintf("%s dashboard does not have %s label", d.DisplayName, label)
467 } else {
468 d.RemoveLabel(label)
469 }
470 }
471
472 if !validate {
473 return c.patchDashboard(d.Name, d)
474 }
475 return c.patchDashboardValidate(d.Name, d)
476 }
477
478
479 func (d *Dashboard) RemoveLabel(label string) {
480 delete(d.Labels, label)
481 }
482
483
484 func (c *Client) RemoveLabelsFromDashboard(labels []string, dName string, validate bool) error {
485 dashboards, err := c.GetDashboardByDisplayName(dName)
486 if err != nil {
487 return err
488 }
489 for i := 0; i < len(dashboards); i++ {
490 _, err := c.removeLabels(labels, dashboards[i], validate)
491 if err != nil {
492 return err
493 }
494 }
495 return nil
496 }
497
498
499 func (c *Client) RemoveLabelTypeFromDashboard(labelType string, name string) (*Dashboard, error) {
500 dashboard, err := c.getDashboard(name)
501 if err != nil {
502 return nil, err
503 }
504
505
506 r, _ := regexp.Compile(labelType)
507 labels := dashboard.GetLabels()
508 var typeMatches []string
509 for i := 0; i < len(labels); i++ {
510 if r.MatchString(labels[i]) {
511 typeMatches = append(typeMatches, labels[i])
512 }
513 }
514
515
516 if c.RemoveLabelsFromDashboard(typeMatches, name, false) != nil {
517 return nil, err
518 }
519
520 return c.getDashboard(name)
521 }
522
523
524 func (d *Dashboard) RemoveLabelType(labelType string) {
525
526 r, _ := regexp.Compile(labelType)
527 labels := d.GetLabels()
528 var typeMatches []string
529 for i := 0; i < len(labels); i++ {
530 if r.MatchString(labels[i]) {
531 typeMatches = append(typeMatches, labels[i])
532 }
533 }
534 if len(typeMatches) == 0 {
535 vPrintf("RemoveLabelType: %s dashboard has no labels matching type %s", d.DisplayName, labelType)
536 }
537
538
539 for i := 0; i < len(typeMatches); i++ {
540 if d.HasLabel(typeMatches[i]) {
541 d.RemoveLabel(typeMatches[i])
542 }
543 }
544
545
546 }
547
548
549 func (d *Dashboard) HasLabel(label string) bool {
550 if _, found := d.Dashboard.Labels[label]; found {
551 return true
552 }
553 return false
554 }
555
556
557 func (d *Dashboard) HasLabelType(labelType string) bool {
558 labels := d.GetLabels()
559 r, _ := regexp.Compile(labelType)
560 for i := 0; i < len(labels); i++ {
561 if r.MatchString(labels[i]) {
562 return true
563 }
564 }
565 return false
566 }
567
568
569 func (d *Dashboard) ValidLabels() bool {
570 r, _ := regexp.Compile(`^[a-z][a-z\d_-]+$`)
571 for k, v := range d.Labels {
572 if !r.MatchString(k) || v != "" {
573 return false
574 }
575 }
576 return true
577 }
578
579
580 func (d *Dashboard) HasRequiredLabels() bool {
581 matchPT := false
582 owner := false
583 p, _ := regexp.Compile("product_|team_")
584 o, _ := regexp.Compile("owner_")
585 labels := d.GetLabels()
586 for i := 0; i < len(labels); i++ {
587 if p.MatchString(labels[i]) {
588 matchPT = true
589 }
590 if o.MatchString(labels[i]) {
591 owner = true
592 }
593 }
594
595 if matchPT && owner {
596 return true
597 }
598 if !matchPT {
599 vPrintf("Missing required label: %s dashboard must have at least one `product_<product-name>` or 'team_<team-name>' label\n", d.DisplayName)
600 }
601 if !owner {
602 vPrintf("Missing required label: %s dashboard must have an 'owner_<owner>' label\n", d.DisplayName)
603 }
604 return false
605 }
606
607
608 func (c *Client) lookupDashboardName(dName string, projectID string) ([]string, error) {
609 dashboards, err := c.GetAllDashboards()
610 if err != nil {
611 return nil, err
612 }
613
614 var names []string
615 for i := 0; i < len(dashboards); i++ {
616 if dashboards[i].DisplayName == dName {
617 names = append(names, dashboards[i].Name)
618 }
619 }
620 if len(names) == 0 {
621 err := fmt.Errorf("no match found for dashboard display name '%s' in project '%s'", dName, projectID)
622 return nil, err
623 }
624 return names, nil
625 }
626
627
628 func (c *Client) lookupDashboardsByLabel(label string) ([]string, error) {
629 dashboards, err := c.GetAllDashboards()
630 if err != nil {
631 return nil, err
632 }
633
634 var names []string
635 for i := 0; i < len(dashboards); i++ {
636 if dashboards[i].HasLabel(label) {
637 names = append(names, dashboards[i].Name)
638 }
639 }
640 if len(names) == 0 {
641 err := fmt.Errorf("no match found for dashboard '%s' label in project '%s'", label, c.ProjectID)
642 return nil, err
643 }
644 return names, nil
645 }
646
647
648 func (d *Dashboard) RemoveTemplateLabels(labels []string) (*Dashboard, error) {
649 for i := 0; i < len(labels); i++ {
650 label := strings.TrimSpace(labels[i])
651 if d.HasLabel(label) {
652 d.RemoveLabel(label)
653 vPrintf("Removed %s label from %s dashboard template\n", label, d.DisplayName)
654 } else {
655 vPrintf("%s (%s) dashboard does not have the %s label\n", d.DisplayName, d.Name, label)
656 }
657 }
658
659 err := d.SaveToTemplate()
660 if err != nil {
661 return nil, err
662 }
663 return d, nil
664 }
665
666
667 func (d *Dashboard) AddTemplateLabels(labels []string) (*Dashboard, error) {
668 var modified = false
669 for i := 0; i < len(labels); i++ {
670 label := strings.TrimSpace(labels[i])
671 if !d.HasLabel(label) {
672 d.AddLabel(label)
673 vPrintf("Added %s label to %s dashboard template\n", label, d.DisplayName)
674 modified = true
675 }
676 }
677 if modified {
678 err = d.SaveToTemplate()
679 if err != nil {
680 return nil, err
681 }
682 }
683 return d, nil
684 }
685
686
687 func (d *Dashboard) UpdateTemplateDisplayName(displayName string) (*Dashboard, error) {
688 d.DisplayName = strings.TrimSpace(displayName)
689 err := d.SaveToTemplate()
690 if err != nil {
691 return nil, err
692 }
693 vPrintf("Updated dashboard template display name to %s\n", d.DisplayName)
694
695 return d, nil
696 }
697
698
699 func (c *Client) UpdateDashboard(name string, t *Dashboard, validate bool) (*Dashboard, error) {
700
701 d, err := c.GetDashboardByDisplayName(name)
702 if err != nil {
703 return nil, err
704 }
705
706
707 d[0].DisplayName = t.DisplayName
708 d[0].Labels = t.Labels
709 d[0].ColumnLayout = t.ColumnLayout
710 d[0].GridLayout = t.GridLayout
711 d[0].MosaicLayout = t.MosaicLayout
712 d[0].RowLayout = t.RowLayout
713 d[0].DashboardFilters = t.DashboardFilters
714
715 if validate {
716 result, err := c.patchDashboardValidate(d[0].Name, d[0])
717 if err != nil {
718 return nil, err
719 }
720 vPrintf("%s dashboard update request is valid\n", d[0].Name)
721 return &Dashboard{result.Dashboard, t.TemplatePath}, nil
722 } else if !validate {
723 result, err := c.patchDashboard(d[0].Name, d[0])
724 if err != nil {
725 return nil, err
726 }
727 vPrintf("%s dashboard instance %s updated\n", d[0].DisplayName, d[0].Name)
728 return &Dashboard{result.Dashboard, t.TemplatePath}, nil
729 }
730 return d[0], nil
731 }
732
733
734 func (c *Client) UpdateDashboardFromTemplate(name string, path string, validate bool) (*Dashboard, error) {
735
736 d, err := c.GetDashboardByDisplayName(name)
737 if err != nil {
738 return nil, err
739 }
740
741
742 template, err := readDashboardFile(path)
743 if err != nil {
744 return nil, err
745 }
746 d[0].DisplayName = template.DisplayName
747 d[0].Labels = template.Labels
748 d[0].ColumnLayout = template.ColumnLayout
749 d[0].GridLayout = template.GridLayout
750 d[0].MosaicLayout = template.MosaicLayout
751 d[0].RowLayout = template.RowLayout
752 d[0].DashboardFilters = template.DashboardFilters
753
754 if validate {
755 result, err := c.patchDashboardValidate(d[0].Name, d[0])
756 if err != nil {
757 return nil, err
758 }
759 vPrintf("%s dashboard update from %s template is valid\n", d[0].Name, template.TemplatePath)
760 return &Dashboard{result.Dashboard, path}, nil
761 } else if !validate {
762 result, err := c.patchDashboard(d[0].Name, d[0])
763 if err != nil {
764 return nil, err
765 }
766 vPrintf("%s dashboard updated from %s template\n", d[0].Name, template.TemplatePath)
767 return &Dashboard{result.Dashboard, path}, nil
768 }
769 return d[0], nil
770 }
771
772
773 func prettyprint(b []byte) ([]byte, error) {
774 var out bytes.Buffer
775 err := json.Indent(&out, b, "", " ")
776 return out.Bytes(), err
777 }
778
779
780 func (d *Dashboard) SaveToTemplate() error {
781 template := monitoring.Dashboard{
782 DisplayName: d.DisplayName,
783 Labels: d.Labels,
784 ColumnLayout: d.ColumnLayout,
785 GridLayout: d.GridLayout,
786 MosaicLayout: d.MosaicLayout,
787 RowLayout: d.RowLayout,
788 DashboardFilters: d.DashboardFilters,
789 }
790 byteData, err := template.MarshalJSON()
791 data, _ := prettyprint(byteData)
792 if err != nil {
793 return err
794 }
795 err = os.WriteFile(d.TemplatePath, data, 0)
796 if err != nil {
797 return err
798 }
799 vPrintf("Saved changes to %s template file\n", filepath.Base(d.TemplatePath))
800 return nil
801 }
802
803
804 func isDashboardName(dName string) bool {
805 isName, _ := regexp.MatchString(`^projects/\d{12,15}/dashboards/[0-9a-z\-]+`, dName)
806 return !isName
807 }
808
809
810 func supportedPath(path string) bool {
811 r, _ := regexp.Compile(`([\s"])`)
812 return !r.MatchString(path)
813 }
814
815
816 func readDashboardFile(filePath string) (*Dashboard, error) {
817 fileBytes, err := os.ReadFile(filePath)
818 if err != nil {
819 return nil, err
820 }
821
822
823 d, err := unmarshalJSON(fileBytes)
824 if err != nil {
825 return nil, err
826 } else if d == nil {
827 return nil, fmt.Errorf("%s is not a valid dashboard JSON template format", filePath)
828 }
829
830 return &Dashboard{d, filePath}, nil
831 }
832
833
834 func unmarshalJSON(b []byte) (*monitoring.Dashboard, error) {
835 var d *monitoring.Dashboard
836 if err := json.Unmarshal(b, &d); err != nil {
837 return nil, err
838 }
839 return d, nil
840 }
841
842
843 func isExpiredDate(date string) bool {
844 currTime := time.Now()
845 loc := currTime.Location()
846 evalDate, _ := time.ParseInLocation("2006-01-02", date, loc)
847 diff := currTime.Sub(evalDate)
848 return diff.Minutes() > 0
849 }
850
851
852 func (d *Dashboard) Expired() bool {
853 r, _ := regexp.Compile("expiration_|orphan-expiration_")
854 rd, _ := regexp.Compile(".*expiration_(.*)")
855 labels := d.GetLabels()
856 for i := 0; i < len(labels); i++ {
857 if r.MatchString(labels[i]) {
858 date := rd.FindStringSubmatch(labels[i])
859 if isExpiredDate(date[1]) {
860 return true
861 }
862 }
863 }
864 return false
865 }
866
867
868 func InList(displayName string, d []*Dashboard) bool {
869 for i := 0; i < len(d); i++ {
870 if d[i].DisplayName == displayName {
871 return true
872 }
873 }
874 return false
875 }
876
877
878 func (d *Dashboard) IsDirty() bool {
879 if d.Etag != "" || d.Name != "" {
880 return true
881 }
882 return false
883 }
884
885
886 func (c *Client) GetDashboardsByLabels(labels []string) ([]*Dashboard, error) {
887 var dList []string
888 for i := 0; i < len(labels); i++ {
889 ld, err := c.ListDashboardsByLabel(labels[i])
890 if err != nil {
891 return nil, err
892 }
893
894
895 for n := 0; n < len(ld); n++ {
896 if !inStrArray(ld[n], dList) {
897 dList = append(dList, ld[n])
898 }
899 }
900 }
901
902 return c.getDashboards(dList)
903 }
904
905
906 func (c *Client) getDashboards(dList []string) ([]*Dashboard, error) {
907 var dashboards []*Dashboard
908 for i := 0; i < len(dList); i++ {
909 d, err := c.getDashboard(dList[i])
910 if err != nil {
911 return nil, err
912 }
913 dashboards = append(dashboards, d)
914 }
915 return dashboards, nil
916 }
917
918
919 func evalError(err error, str string, msg ...interface{}) bool {
920 if err != nil && !Continues {
921 return true
922 } else if err != nil && Continues {
923 vPrintf(str, msg...)
924 return false
925 }
926 return false
927 }
928
929
930 func vPrintln(msg string) {
931 if Verbose {
932 fmt.Println(msg)
933 }
934 }
935
936
937 func vPrintf(str string, msg ...interface{}) {
938 if Verbose {
939 fmt.Printf(str, msg...)
940 }
941 }
942
943
944 func getMetricErrors(e error) []string {
945 var rMetrics = regexp.MustCompile(`Could not find a metric named '.*\/(.*)'.`)
946 var metrics []string
947
948 lines := strings.Split(e.Error(), "\n")
949 if !is400Code(lines[0]) {
950 vPrintln("could not determine error status code")
951 return nil
952 }
953
954 for i := 0; i < len(lines); i++ {
955 match := rMetrics.FindStringSubmatch(lines[i])
956 if len(match) == 2 {
957 metrics = append(metrics, match[1])
958 }
959 }
960 if len(metrics) == 0 {
961 vPrintln("error is unrelated to missing metrics")
962 }
963
964 return metrics
965 }
966
967
968 func is400Code(e string) bool {
969 var rCode = regexp.MustCompile(`Error ([\d]+)`)
970 if rCode.MatchString(e) {
971 status := rCode.FindStringSubmatch(e)
972 if status[1] == "400" {
973 return true
974 }
975 }
976 return false
977 }
978
979
980 func CreateDashboardTemplates(dashboards []*Dashboard, folderPath string, prefix string, overwrite bool) error {
981 var dNames []string
982
983 for i := 0; i < len(dashboards); i++ {
984 name := prefix + monutil.FilterString(`(?:[\w\-_\[\]\(\)]+)`, dashboards[i].DisplayName) + ".json"
985 dNames = append(dNames, name)
986 }
987 dNames = reconcileFileNames(dNames)
988
989 if len(dashboards) != len(dNames) {
990 return fmt.Errorf("dashboard count does not equal the dashboard name count: unable to create dashboard templates")
991 }
992
993 t := time.Now()
994 for i, f := range dNames {
995 if monutil.FileExists(folderPath+"/"+f) && !overwrite {
996 f = strings.TrimSuffix(f, ".json") + "_" + t.Format("2006-01-02-1504") + ".json"
997 }
998 dashboards[i].TemplatePath = folderPath + "/" + f
999 _, err = os.Create(dashboards[i].TemplatePath)
1000 if err != nil {
1001 return err
1002 }
1003
1004 if err = dashboards[i].SaveToTemplate(); err != nil {
1005 return err
1006 }
1007 }
1008
1009 return nil
1010 }
1011
1012
1013
1014 func getNextIter(list []string, filter string) int {
1015 var iter []int
1016 for _, l := range list {
1017 r, _ := regexp.Compile(l + filter)
1018 match := r.FindStringSubmatch(l)
1019 if len(match) != 2 {
1020 continue
1021 }
1022 mi, _ := strconv.Atoi(match[1])
1023 iter = append(iter, mi)
1024 }
1025
1026
1027 var newIter = 1
1028 for i := 0; i < len(iter); i++ {
1029 if inIntArray(newIter, iter) {
1030 newIter++
1031 }
1032 }
1033
1034 return newIter
1035 }
1036
1037
1038 func reconcileFileNames(names []string) []string {
1039 var fileNames []string
1040 for _, n := range names {
1041 if !inStrArray(n, fileNames) {
1042 fileNames = append(fileNames, n)
1043 continue
1044 }
1045
1046
1047 iter := getNextIter(fileNames, n+`_(Duplicate_Name_([\d]+)).json`)
1048 fileNames = append(fileNames, n+"_(Duplicate_Name_"+strconv.Itoa(iter)+").json")
1049 }
1050
1051 return fileNames
1052 }
1053
1054
1055 func (d *Dashboard) DropReservedLabels() {
1056 if d.HasLabel("managed") {
1057 d.RemoveLabel("managed")
1058 }
1059
1060 if d.HasLabelType("v_") {
1061 verLabel := strings.ReplaceAll(strings.ReplaceAll(d.GetVersion(), ".", "_"), "v", "v_")
1062 d.RemoveLabel(verLabel)
1063 }
1064
1065 if d.HasAnyExpirationLabel() {
1066 if d.HasLabelType("orphan-expiration_") {
1067 d.RemoveLabelType("orphan-expiration_")
1068 } else {
1069 d.RemoveLabelType("expiration_")
1070 }
1071 }
1072
1073 if d.HasLabel("duplicate_renamed") {
1074 d.RemoveLabel("duplicate_renamed")
1075 }
1076 }
1077
1078
1079 func (d *Dashboard) Matches(c *Dashboard) bool {
1080 d.DropReservedLabels()
1081 source := monitoring.Dashboard{
1082 DisplayName: d.DisplayName,
1083 Labels: d.Labels,
1084 ColumnLayout: d.ColumnLayout,
1085 GridLayout: d.GridLayout,
1086 MosaicLayout: d.MosaicLayout,
1087 RowLayout: d.RowLayout,
1088 DashboardFilters: d.DashboardFilters,
1089 }
1090
1091 sourceJSON, err := source.MarshalJSON()
1092 if err != nil {
1093 vPrintf("%s source marshaling error", d.DisplayName)
1094 return false
1095 }
1096
1097 c.DropReservedLabels()
1098 comp := monitoring.Dashboard{
1099 DisplayName: c.DisplayName,
1100 Labels: c.Labels,
1101 ColumnLayout: c.ColumnLayout,
1102 GridLayout: c.GridLayout,
1103 MosaicLayout: c.MosaicLayout,
1104 RowLayout: c.RowLayout,
1105 DashboardFilters: c.DashboardFilters,
1106 }
1107 compJSON, err := comp.MarshalJSON()
1108 if err != nil {
1109 vPrintf("%s comparison marshaling error", c.DisplayName)
1110 return false
1111 }
1112
1113 diff, _ := jsondiff.Compare(sourceJSON, compJSON, &jsondiff.Options{})
1114 if diff.String() != "FullMatch" {
1115 vPrintf("%s dashboard configuration did not match: \n", d.DisplayName)
1116 return false
1117 }
1118
1119 return true
1120 }
1121
View as plain text