diff --git a/cmd/tools/grafana/grafana.go b/cmd/tools/grafana/grafana.go index 388d1a6fe..371270401 100644 --- a/cmd/tools/grafana/grafana.go +++ b/cmd/tools/grafana/grafana.go @@ -63,6 +63,8 @@ type options struct { customizeDir string customAllValue string customCluster string + varDefaults string + defaultDropdownMap map[string][]string } type Folder struct { @@ -437,6 +439,30 @@ func initImportVars() { } opts.dirGrafanaFolderMap[k] = v } + + // Parse default dropdown values + opts.defaultDropdownMap = make(map[string][]string) + if opts.varDefaults != "" { + if !validateVarDefaults(opts.varDefaults) { + fmt.Println("Error: Invalid format for --var-defaults. Expected format is 'variable1=value1,value2;variable2=value3'") + os.Exit(1) + } + pairs := strings.Split(opts.varDefaults, ";") + for _, pair := range pairs { + parts := strings.SplitN(pair, "=", 2) + if len(parts) == 2 { + values := strings.Split(parts[1], ",") + opts.defaultDropdownMap[parts[0]] = values + } + } + } +} + +// validateVarDefaults validates the format of the --var-defaults input string. +// The expected format is 'variable1=value1,value2;variable2=value3'. +func validateVarDefaults(input string) bool { + re := regexp.MustCompile(`^([^=;,]+=[^=;,]+(,[^=;,]+)*)(;[^=;,]+=[^=;,]+(,[^=;,]+)*)*$`) + return re.MatchString(input) } func checkAndCreateServerFolder(folder *Folder) error { @@ -519,6 +545,11 @@ func importFiles(dir string, folder *Folder) { data = changeClusterLabel(data, opts.customCluster) } + // Set default dropdown values if provided + if len(opts.defaultDropdownMap) > 0 { + data = setDefaultDropdownValues(data, opts.defaultDropdownMap) + } + // labelMap is used to ensure we don't modify the query of one of the new labels we're adding labelMap := make(map[string]string) caser := cases.Title(language.Und) @@ -590,6 +621,24 @@ func importFiles(dir string, folder *Folder) { } } +// setDefaultDropdownValues sets the default values for specified dropdown variables in the dashboard JSON data. +// It takes a map of variable names to their default values and updates the JSON data accordingly. +func setDefaultDropdownValues(data []byte, defaultValues map[string][]string) []byte { + for variable, defaultValues := range defaultValues { + variablePath := fmt.Sprintf("templating.list.#(name=%q)", variable) + variableData := gjson.GetBytes(data, variablePath) + if variableData.Exists() { + current := map[string]any{ + "selected": true, + "text": defaultValues, + "value": defaultValues, + } + data, _ = sjson.SetBytes(data, variablePath+".current", current) + } + } + return data +} + // This function will rewrite all panel expressions in the dashboard to use the new cluster label. // Example: // sum(write_data{datacenter=~"$Datacenter",cluster=~"$Cluster",svm=~"$SVM"}) @@ -1293,6 +1342,7 @@ func init() { addCommonFlags(importCmd, exportCmd, customizeCmd) addImportExportFlags(importCmd, exportCmd) addImportCustomizeFlags(importCmd, customizeCmd) + addImportFlags(importCmd) customizeCmd.PersistentFlags().StringVarP(&opts.customizeDir, "output-dir", "o", "", "Write customized dashboards to the local directory. The directory must not exist") @@ -1300,6 +1350,32 @@ func init() { "", "local directory that contains dashboards (searched recursively).") } +func addImportFlags(cmd *cobra.Command) { + cmd.PersistentFlags().StringVar(&opts.varDefaults, "var-defaults", "", ` +Default values for dropdown variables in the format 'variable1=value1,value2;variable2=value3'. + +Examples: + +1. Set a single variable: + To set the default value for the 'Datacenter' variable to 'DC1': + --var-defaults "Datacenter=DC1" + +2. Set multiple values for a single variable: + To set the default values for the 'Datacenter' variable to 'DC1' and 'DC2': + --var-defaults "Datacenter=DC1,DC2" + +3. Set multiple variables in one command: + To set the default values for 'Datacenter' to 'DC1' and 'DC2', and 'Cluster' to 'Cluster1': + --var-defaults "Datacenter=DC1,DC2;Cluster=Cluster1" + +4. Set multiple values for multiple variables: + To set the default values for 'Datacenter' to 'DC1' and 'DC2', and 'Cluster' to 'Cluster1' and 'Cluster2': + --var-defaults "Datacenter=DC1,DC2;Cluster=Cluster1,Cluster2" + +Note: Ensure that variable names and values do not contain the characters '=', ',', or ';' as these are used as delimiters. +`) +} + func addImportCustomizeFlags(commands ...*cobra.Command) { for _, cmd := range commands { cmd.PersistentFlags().StringSliceVar(&opts.labels, "labels", nil, diff --git a/cmd/tools/grafana/grafana_test.go b/cmd/tools/grafana/grafana_test.go index d30a63eb6..087a2628e 100644 --- a/cmd/tools/grafana/grafana_test.go +++ b/cmd/tools/grafana/grafana_test.go @@ -675,3 +675,33 @@ func TestClusterRewrite(t *testing.T) { }) } } + +func TestValidateVarDefaults(t *testing.T) { + tests := []struct { + input string + want bool + }{ + {input: "Datacenter=DC1,DC2;Cluster=Cluster1", want: true}, + {input: "Datacenter=DC1;Cluster=Cluster1,Cluster2", want: true}, + {input: "Datacenter=DC1,DC2;Cluster=Cluster1;Region=US,EU", want: true}, + {input: "Datacenter=DC1,DC2;Cluster=Cluster1;Region=US,EU;SVM=SAN,NAS", want: true}, + {input: "Datacenter=DC1,DC2", want: true}, + {input: "Datacenter=nane,rtp;Cluster=Cluster2,A250-15-28-29", want: true}, + {input: "Datacenter=nane,rtp;Cluster=Cluster2,A250-15-28#29", want: true}, + {input: "Datacenter=DC1,DC2;", want: false}, // trailing semicolon + {input: "=DC1,DC2;Cluster=Cluster1", want: false}, // missing variable name + {input: "Datacenter=;Cluster=Cluster1", want: false}, // missing value + {input: "Datacenter=DC1,DC2;Cluster=", want: false}, // missing value + {input: "Datacenter=DC1,DC2;Cluster", want: false}, // missing equals sign + {input: "Datacenter=DC1,DC2;Cluster=Cluster1;", want: false}, // trailing semicolon + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + got := validateVarDefaults(tt.input) + if got != tt.want { + t.Errorf("validateVarDefaults(%q) = %v, want %v", tt.input, got, tt.want) + } + }) + } +}