Skip to content

Commit

Permalink
Merge pull request #839 from newrelic/feat/generate-dashboard-hcl
Browse files Browse the repository at this point in the history
feat(utils): add a command to generate dashboard HCL
  • Loading branch information
ctrombley authored May 7, 2021
2 parents fc745b9 + 8a6a37e commit dfc7e92
Show file tree
Hide file tree
Showing 4 changed files with 328 additions and 0 deletions.
105 changes: 105 additions & 0 deletions internal/utils/command_terraform.go
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")
}
23 changes: 23 additions & 0 deletions internal/utils/command_terraform_test.go
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)
}
119 changes: 119 additions & 0 deletions internal/utils/terraform/dashboard.go
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)
}
}
81 changes: 81 additions & 0 deletions internal/utils/terraform/hcl_gen.go
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]
}

0 comments on commit dfc7e92

Please sign in to comment.