-
Notifications
You must be signed in to change notification settings - Fork 73
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #839 from newrelic/feat/generate-dashboard-hcl
feat(utils): add a command to generate dashboard HCL
- Loading branch information
Showing
4 changed files
with
328 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
package utils | ||
|
||
import ( | ||
"fmt" | ||
"io/ioutil" | ||
"os" | ||
"regexp" | ||
|
||
"github.com/newrelic/newrelic-cli/internal/utils/terraform" | ||
|
||
log "github.com/sirupsen/logrus" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
var ( | ||
label string | ||
file string | ||
outFile string | ||
shiftWidth int | ||
snakeCaseRE = regexp.MustCompile("^[a-z]+(_[a-z]+)*$") | ||
) | ||
|
||
var cmdTerraform = &cobra.Command{ | ||
Use: "terraform", | ||
Short: "Tools for working with Terraform", | ||
Long: `Tools for working with Terraform | ||
The terraform commands can be used for generating Terraform HCL for simple observability | ||
as code use cases. | ||
`, | ||
Example: `cat terraform.json | newrelic utils terraform dashboard --label my_dashboard_resource`, | ||
} | ||
|
||
var cmdTerraformDashboard = &cobra.Command{ | ||
Use: "dashboard", | ||
Short: "Generate HCL for the newrelic_one_dashboard resource", | ||
Long: `Generate HCL for the newrelic_one_dashboard resource | ||
This command generates HCL configuration for newrelic_one_dashboard resources from | ||
exported JSON documents. For more detail on exporting dashboards to JSON, see | ||
https://docs.newrelic.com/docs/query-your-data/explore-query-data/dashboards/manage-your-dashboard/#dash-json | ||
Input can be sourced from STDIN per the provided example, or from a file using the --file option. | ||
Output will be sent to STDOUT by default but can be redirected to a file with the --out option. | ||
`, | ||
Example: `cat terraform.json | newrelic utils terraform dashboard --label my_dashboard_resource`, | ||
Args: func(cmd *cobra.Command, args []string) error { | ||
if ok := snakeCaseRE.MatchString(label); !ok { | ||
return fmt.Errorf("resource label must be formatted with snake case: %s", label) | ||
} | ||
|
||
if file != "" { | ||
if _, err := os.Stat(file); os.IsNotExist(err) { | ||
return fmt.Errorf("file not found: %s", file) | ||
} | ||
} | ||
|
||
return nil | ||
}, | ||
Run: func(cmd *cobra.Command, args []string) { | ||
|
||
var input []byte | ||
var err error | ||
if file != "" { | ||
input, err = ioutil.ReadFile(file) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
} else { | ||
input, err = ioutil.ReadAll(os.Stdin) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
} | ||
|
||
hcl, err := terraform.GenerateDashboardHCL(label, shiftWidth, input) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
|
||
if outFile != "" { | ||
if err := ioutil.WriteFile(outFile, []byte(hcl), 0644); err != nil { | ||
log.Fatal(err) | ||
} | ||
|
||
log.Info("success") | ||
} else { | ||
fmt.Print(hcl) | ||
} | ||
}, | ||
} | ||
|
||
func init() { | ||
Command.AddCommand(cmdTerraform) | ||
|
||
cmdTerraform.AddCommand(cmdTerraformDashboard) | ||
cmdTerraformDashboard.Flags().StringVarP(&label, "label", "l", "", "the resource label to use when generating resource HCL") | ||
if err := cmdTerraformDashboard.MarkFlagRequired("label"); err != nil { | ||
log.Error(err) | ||
} | ||
|
||
cmdTerraformDashboard.Flags().StringVarP(&file, "file", "f", "", "a file that contains exported dashboard JSON") | ||
cmdTerraformDashboard.Flags().StringVarP(&outFile, "out", "o", "", "the file to send the generated HCL to") | ||
cmdTerraformDashboard.Flags().IntVarP(&shiftWidth, "shiftWidth", "w", 2, "the indentation shift with of the output") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
// +build unit | ||
|
||
package utils | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
|
||
"github.com/newrelic/newrelic-cli/internal/testcobra" | ||
) | ||
|
||
func TestTerraform(t *testing.T) { | ||
assert.Equal(t, "terraform", cmdTerraform.Name()) | ||
|
||
testcobra.CheckCobraMetadata(t, cmdSemver) | ||
} | ||
|
||
func TestTerraformDashboard(t *testing.T) { | ||
assert.Equal(t, "dashboard", cmdTerraformDashboard.Name()) | ||
|
||
testcobra.CheckCobraMetadata(t, cmdTerraformDashboard) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
package terraform | ||
|
||
import ( | ||
"encoding/json" | ||
"strings" | ||
|
||
log "github.com/sirupsen/logrus" | ||
|
||
"github.com/newrelic/newrelic-client-go/pkg/dashboards" | ||
) | ||
|
||
var ( | ||
dashboardResourceName = "newrelic_one_dashboard" | ||
widgetTypes = map[string]string{ | ||
"viz.area": "widget_area", | ||
"viz.bar": "widget_bar", | ||
"viz.billboard": "widget_billboard", | ||
"viz.bullet": "widget_bullet", | ||
"viz.funnel": "widget_funnel", | ||
"viz.heatmap": "widget_heatmap", | ||
"viz.histogram": "widget_histogram", | ||
"viz.json": "widget_json", | ||
"viz.line": "widget_line", | ||
"viz.markdown": "widget_markdown", | ||
"viz.pie": "widget_pie", | ||
"viz.table": "widget_table", | ||
} | ||
) | ||
|
||
type DashboardWidgetRawConfiguration struct { | ||
DataFormatters []string `json:"dataFormatters"` | ||
NRQLQueries []DashboardWidgetNRQLQuery `json:"nrqlQueries"` | ||
LinkedEntityGUIDs []string `json:"linkedEntityGuids"` | ||
Text string `json:"text"` | ||
Facet DashboardWidgetFacet `json:"facet"` | ||
Legend DashboardWidgetLegend `json:"legend"` | ||
YAxisLeft DashboardWidgetYAxisLeft `json:"yAxisLeft"` | ||
} | ||
|
||
type DashboardWidgetFacet struct { | ||
ShowOtherSeries bool `json:"showOtherSeries"` | ||
} | ||
|
||
type DashboardWidgetNRQLQuery struct { | ||
AccountID int `json:"accountId"` | ||
Query string `json:"query"` | ||
} | ||
|
||
type DashboardWidgetLegend struct { | ||
Enabled bool `json:"enabled"` | ||
} | ||
|
||
type DashboardWidgetYAxisLeft struct { | ||
Zero bool `json:"zero"` | ||
} | ||
|
||
func GenerateDashboardHCL(resourceLabel string, shiftWidth int, input []byte) (string, error) { | ||
var d dashboards.DashboardInput | ||
if err := json.Unmarshal(input, &d); err != nil { | ||
log.Fatal(err) | ||
} | ||
|
||
h := NewHCLGen(shiftWidth) | ||
h.WriteBlock("resource", []string{dashboardResourceName, resourceLabel}, func() { | ||
h.WriteStringAttribute("name", d.Name) | ||
h.WriteStringAttributeIfNotEmpty("description", d.Description) | ||
h.WriteStringAttributeIfNotEmpty("permissions", strings.ToLower(string(d.Permissions))) | ||
|
||
for _, p := range d.Pages { | ||
h.WriteBlock("page", []string{}, func() { | ||
h.WriteStringAttribute("name", p.Name) | ||
h.WriteStringAttributeIfNotEmpty("description", p.Description) | ||
|
||
for _, w := range p.Widgets { | ||
requireValidVisualizationID(w.Visualization.ID) | ||
|
||
h.WriteBlock(widgetTypes[w.Visualization.ID], []string{}, func() { | ||
h.WriteStringAttribute("title", w.Title) | ||
h.WriteIntAttribute("row", w.Layout.Row) | ||
h.WriteIntAttribute("column", w.Layout.Column) | ||
h.WriteIntAttribute("height", w.Layout.Height) | ||
h.WriteIntAttribute("width", w.Layout.Width) | ||
|
||
config := unmarshalDashboardWidgetRawConfiguration(w.Title, widgetTypes[w.Visualization.ID], w.RawConfiguration) | ||
|
||
h.WriteStringSliceAttributeIfNotEmpty("linked_entity_guids", config.LinkedEntityGUIDs) | ||
h.WriteMultilineStringAttributeIfNotEmpty("text", config.Text) | ||
|
||
for _, q := range config.NRQLQueries { | ||
h.WriteBlock("nrql_query", []string{}, func() { | ||
h.WriteIntAttributeIfNotZero("account_id", q.AccountID) | ||
h.WriteMultilineStringAttribute("query", q.Query) | ||
}) | ||
} | ||
}) | ||
} | ||
}) | ||
} | ||
}) | ||
|
||
return h.String(), nil | ||
} | ||
|
||
func unmarshalDashboardWidgetRawConfiguration(title string, widgetType string, b []byte) *DashboardWidgetRawConfiguration { | ||
var c DashboardWidgetRawConfiguration | ||
err := json.Unmarshal(b, &c) | ||
if err != nil { | ||
log.Fatalf("failed unmarshaling rawConfiguration for widget \"%s\" of type \"%s\"", title, widgetType) | ||
panic(err) | ||
} | ||
|
||
return &c | ||
} | ||
|
||
func requireValidVisualizationID(id string) { | ||
if widgetTypes[id] == "" { | ||
log.Fatalf("unrecognized widget type \"%s\"", id) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
package terraform | ||
|
||
import ( | ||
"fmt" | ||
"strings" | ||
) | ||
|
||
type HCLGen struct { | ||
strings.Builder | ||
shiftWidth int | ||
i string | ||
} | ||
|
||
func NewHCLGen(shiftWidth int) *HCLGen { | ||
return &HCLGen{ | ||
Builder: strings.Builder{}, | ||
shiftWidth: shiftWidth, | ||
} | ||
} | ||
|
||
func (h *HCLGen) WriteMultilineStringAttribute(label string, value string) { | ||
h.WriteString(fmt.Sprintf("%s%s = <<EOT\n%s\nEOT\n", h.i, label, value)) | ||
} | ||
|
||
func (h *HCLGen) WriteMultilineStringAttributeIfNotEmpty(label string, value string) { | ||
if value != "" { | ||
h.WriteMultilineStringAttribute(label, value) | ||
} | ||
} | ||
|
||
func (h *HCLGen) WriteStringAttribute(label string, value string) { | ||
h.WriteString(fmt.Sprintf("%s%s = \"%s\"\n", h.i, label, strings.ReplaceAll(value, "\"", "\\\""))) | ||
} | ||
|
||
func (h *HCLGen) WriteStringAttributeIfNotEmpty(label string, value string) { | ||
if value != "" { | ||
h.WriteStringAttribute(label, value) | ||
} | ||
} | ||
|
||
func (h *HCLGen) WriteStringSliceAttribute(label string, value []string) { | ||
h.WriteString(fmt.Sprintf("%s%s = [\"%s\"]", h.i, label, strings.Join(value, "\",\""))) | ||
} | ||
|
||
func (h *HCLGen) WriteStringSliceAttributeIfNotEmpty(label string, value []string) { | ||
if len(value) > 0 { | ||
h.WriteStringSliceAttribute(label, value) | ||
} | ||
} | ||
|
||
func (h *HCLGen) WriteIntAttribute(label string, value int) { | ||
h.WriteString(fmt.Sprintf("%s%s = %d\n", h.i, label, value)) | ||
} | ||
|
||
func (h *HCLGen) WriteIntAttributeIfNotZero(label string, value int) { | ||
if value != 0 { | ||
h.WriteIntAttribute(label, value) | ||
} | ||
} | ||
|
||
func (h *HCLGen) WriteBlock(name string, labels []string, f func()) { | ||
h.WriteString(fmt.Sprintf("\n%s%s ", h.i, name)) | ||
for _, l := range labels { | ||
h.WriteString(fmt.Sprintf("\"%s\" ", l)) | ||
} | ||
h.WriteString("{\n") | ||
|
||
h.indent() | ||
f() | ||
h.unindent() | ||
|
||
h.WriteString(fmt.Sprintf("%s}\n", h.i)) | ||
} | ||
|
||
func (h *HCLGen) indent() { | ||
h.i += strings.Repeat(" ", h.shiftWidth) | ||
} | ||
|
||
func (h *HCLGen) unindent() { | ||
h.i = h.i[0 : len(h.i)-h.shiftWidth] | ||
} |