...

Source file src/edge-infra.dev/pkg/lib/gcp/monitoring/dashboardmanager/dashboardmanager.go

Documentation: edge-infra.dev/pkg/lib/gcp/monitoring/dashboardmanager

     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  // Creates a new Dashboards service client
    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  // Reads the JSON dashboard template file(s) from the given path
    64  func ReadDashboardsFromPath(path string) ([]*Dashboard, error) {
    65  	var sourceDashboards []*Dashboard
    66  
    67  	// if !isFolder(path) { // file path is a single file
    68  	if !monutil.IsDirectory(path) && monutil.FileExists(path) && supportedPath(path) { // file path is a single file
    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  	// get list of json dashboard files from path
    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  	// add each dashboard template to dashboards array
    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  // Creates a Dashboards in the specified Project
   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  	// validate the create dashboard request only (dry run)
   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  	// create the dashboard
   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  // Checks Dashboard Labels for any reserved expiration label type
   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  // Returns array of Labels for a Dashboard
   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  // Returns the version label for a Dashboard
   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  // Deletes dashboard by name or by matching display name in the project
   197  // NOTE: If multiple dashboards have the same display name in a project, all will be deleted
   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  // Returns a collection of dashboards for the Project ID specified
   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  // Create a list of dashboard configurations
   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  // Checks if a dashboard exists with the specified display name
   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  // Returns dashboard by name, or any matching dashboards for a project if the name provided is a display name
   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 { // handles dashboard name when not display name
   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  // Deletes all dashboards in a project that contain the specified label
   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  // Lists all dashboard names in a project that match the specified label
   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  // Get Dashboard request by dashboard name
   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  // Update the existing dashboard in the project
   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  // Validate dashboard update request
   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  // Change an existing Dashboard's name or display name
   334  func (c *Client) Rename(newName string, name string, validate bool) error {
   335  	// retrieve the specified dashboard from the project
   336  	dashboards, err := c.GetDashboardByDisplayName(name)
   337  	if err != nil {
   338  		return err
   339  	}
   340  
   341  	// verifies the new dashboard name is not already in use
   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 { // validate dashboard rename request
   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 { // rename the project dashboard
   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 { // more than one dashboard matches the name provided
   362  		return fmt.Errorf("%s matches more than one dashboard result", name)
   363  	}
   364  	return nil
   365  }
   366  
   367  // determines if the dashboard contains version and managed labels
   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  // Get duplicate display name iteration
   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  // search array for string
   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  // returns list of file paths from a collection of dashboards
   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  // search array for int
   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  // Add a label to an existing project dashboard
   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  // Add label to a dashboard object
   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  // Manually Adds labels to dashboard(s) that match the specified Display Name - NOTE: Should only used with non-managed dashboard instances
   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  // Removes labels from a project dashboard
   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  // Removes the label from the dashboard object
   479  func (d *Dashboard) RemoveLabel(label string) {
   480  	delete(d.Labels, label)
   481  }
   482  
   483  // Remove label(s) from the specified project dashboard(s)
   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  // Removes specified label type from a project dashboard instance
   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  	// get labels matching the type specified
   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  	// remove all matching type labels from project dashboard
   516  	if c.RemoveLabelsFromDashboard(typeMatches, name, false) != nil {
   517  		return nil, err
   518  	}
   519  
   520  	return c.getDashboard(name)
   521  }
   522  
   523  // Removes specified label type from a project dashboard instance
   524  func (d *Dashboard) RemoveLabelType(labelType string) {
   525  	// get labels that match type
   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  	// remove matching type labels
   539  	for i := 0; i < len(typeMatches); i++ {
   540  		if d.HasLabel(typeMatches[i]) {
   541  			d.RemoveLabel(typeMatches[i])
   542  		}
   543  	}
   544  
   545  	// return nil
   546  }
   547  
   548  // Checks if a Dashboard has a specific label
   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  // Checks if a Dashboard has a label type (label_...)
   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  // checks the label specified for valid formatting
   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  // Checks if a Dashboard has required project and/or team label type, and owner label type
   580  func (d *Dashboard) HasRequiredLabels() bool {
   581  	matchPT := false
   582  	owner := false
   583  	p, _ := regexp.Compile("product_|team_") // product and/or team label type
   584  	o, _ := regexp.Compile("owner_")         // owner label type
   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  // Returns a list of dashboard names for any dashboards in the project which match the display name provided
   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  // Returns a list of dashboard names in the project with the specified label
   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  // Removes labels from dashboard template
   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) { // remove label from dashboard instance
   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  // Adds labels to dashboard template
   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) { // add label to dashboard instance
   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  // Updates dashboard template display name to specified
   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  // Updates project dashboard from provided configuration
   699  func (c *Client) UpdateDashboard(name string, t *Dashboard, validate bool) (*Dashboard, error) {
   700  	// retrieve the specified dashboard from the project
   701  	d, err := c.GetDashboardByDisplayName(name)
   702  	if err != nil {
   703  		return nil, err
   704  	}
   705  
   706  	// update dashboard fields from dashboard source
   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 { // rename the project dashboard
   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  // Updates project dashboard from the template source provided
   734  func (c *Client) UpdateDashboardFromTemplate(name string, path string, validate bool) (*Dashboard, error) {
   735  	// retrieve the specified dashboard from the project
   736  	d, err := c.GetDashboardByDisplayName(name)
   737  	if err != nil {
   738  		return nil, err
   739  	}
   740  
   741  	// retrieve dashboard from template file
   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 { // rename the project dashboard
   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  // Indents json data to make it readable
   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  // Write dashboard configuration to file
   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  // Checks the dashboard name to determine if it is a display name
   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  // checks if the provided path contains any unsupported characters
   810  func supportedPath(path string) bool {
   811  	r, _ := regexp.Compile(`([\s"])`)
   812  	return !r.MatchString(path)
   813  }
   814  
   815  // Gets a Dashboard object from a JSON configuration file
   816  func readDashboardFile(filePath string) (*Dashboard, error) {
   817  	fileBytes, err := os.ReadFile(filePath)
   818  	if err != nil {
   819  		return nil, err
   820  	}
   821  
   822  	// unmarshal byte data to dashboard object
   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  // Convert the byte data to the Dashboard format
   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  // Evaluates the date specified to determine if it has passed
   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  // Get expiration label
   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  // Checks if the specified display name is in the collection of dashboards
   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  // Check if the dashboard contains fields that shouldn't be in the template
   878  func (d *Dashboard) IsDirty() bool {
   879  	if d.Etag != "" || d.Name != "" {
   880  		return true
   881  	}
   882  	return false
   883  }
   884  
   885  // Returns a list of dashboards in project with matching labels
   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  		// add dashboard names to list
   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  // Gets project dashboards from a list of dashboards
   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  // Checks the error and Continues flag state to determine whether to continue
   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  // Enables verbose loging
   930  func vPrintln(msg string) {
   931  	if Verbose {
   932  		fmt.Println(msg)
   933  	}
   934  }
   935  
   936  // Enables verbose formatted loging
   937  func vPrintf(str string, msg ...interface{}) {
   938  	if Verbose {
   939  		fmt.Printf(str, msg...)
   940  	}
   941  }
   942  
   943  // Returns a list of missing metrics in the error message
   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]) { // first line should contain the status code
   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 { // error line contains missing metric name
   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  // Checks if the return code error is a 400
   968  func is400Code(e string) bool {
   969  	var rCode = regexp.MustCompile(`Error ([\d]+)`)
   970  	if rCode.MatchString(e) { // first line should contain the status code
   971  		status := rCode.FindStringSubmatch(e)
   972  		if status[1] == "400" {
   973  			return true
   974  		}
   975  	}
   976  	return false
   977  }
   978  
   979  // Saves a collection of dashboards to a folder path
   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  // Returns the next available iterator based on a specified filter
  1013  // - Used to add iterators to a rename operation to prevent duplicates
  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  	// return next unused iterator
  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  // Creates a new iterated duplicate file name
  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  		// find the next available iterator for the duplicate name
  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  // Returns dashboard object without reserved labels
  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  // Verifies if a dashboard matches another config
  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