Skip to content

Commit

Permalink
feat(*): uids everywhere
Browse files Browse the repository at this point in the history
Converts every place a dashboard has to be identified to use the uid.
This is the only reference that is shared between Grafana and Grizzly.
  • Loading branch information
sh0rez committed Aug 19, 2020
1 parent d24a4d0 commit d58bc1a
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 117 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/grr
/test
1 change: 1 addition & 0 deletions cmd/grr/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
var Version = "dev"

func main() {
log.SetFlags(0)

rootCmd := &cli.Command{
Use: "grr",
Expand Down
12 changes: 6 additions & 6 deletions cmd/grr/workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func showCmd() *cli.Command {
if err != nil {
return err
}
return grizzly.Show(*config, jsonnetFile, targets)
return grizzly.Show(*config, jsonnetFile, *targets)
}
return cmd
}
Expand All @@ -67,7 +67,7 @@ func diffCmd() *cli.Command {
if err != nil {
return err
}
return grizzly.Diff(*config, jsonnetFile, targets)
return grizzly.Diff(*config, jsonnetFile, *targets)
}
return cmd
}
Expand All @@ -85,7 +85,7 @@ func applyCmd() *cli.Command {
if err != nil {
return err
}
return grizzly.Apply(*config, jsonnetFile, targets)
return grizzly.Apply(*config, jsonnetFile, *targets)
}
return cmd
}
Expand All @@ -105,7 +105,7 @@ func watchCmd() *cli.Command {
return err
}

return grizzly.Watch(*config, watchDir, jsonnetFile, targets)
return grizzly.Watch(*config, watchDir, jsonnetFile, *targets)

}
return cmd
Expand Down Expand Up @@ -133,7 +133,7 @@ func previewCmd() *cli.Command {
if err != nil {
return err
}
return grizzly.Preview(*config, jsonnetFile, targets, opts)
return grizzly.Preview(*config, jsonnetFile, *targets, opts)
}
return cmd
}
Expand All @@ -152,7 +152,7 @@ func exportCmd() *cli.Command {
if err != nil {
return err
}
return grizzly.Export(*config, jsonnetFile, dashboardDir, targets)
return grizzly.Export(*config, jsonnetFile, dashboardDir, *targets)
}
return cmd
}
153 changes: 112 additions & 41 deletions pkg/grizzly/grafana.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net/http"
"net/url"
"path"
"strings"
)

// Folder encapsulates a folder object from the Grafana API
Expand All @@ -21,17 +22,80 @@ type Folder struct {
// Board enscapsulates a dashboard for upload to Grafana API
type Board struct {
Dashboard map[string]interface{} `json:"dashboard"`
FolderID int `json:"folderId"`
Overwrite bool `json:"overwrite"`
UID string
Name string
}

func (b *Board) UnmarshalJSON(data []byte) error {
var m map[string]interface{}
if err := json.Unmarshal(data, &m); err != nil {
return err
}

delete(m, "version")
delete(m, "id")

b.Dashboard = m
return nil
}

func (b Board) UID() string {
if s, ok := b.Dashboard["uid"]; ok {
return s.(string)
}
panic("no uid")
}

// Boards encasulates a set of dashboards ready for upload
type Boards map[string]Board

func (bPtr *Boards) UnmarshalJSON(data []byte) error {
if *bPtr == nil {
*bPtr = make(Boards)
}

var m map[string]Board
if err := json.Unmarshal(data, &m); err != nil {
return err
}

// check uids missing
var missing ErrUidsMissing
for key, board := range m {
if _, ok := board.Dashboard["uid"]; !ok {
missing = append(missing, key)
}
}
if len(missing) > 0 {
return missing
}

for k, v := range m {
(*bPtr)[k] = v
}

// check duplicate uids
// uid -> name
uids := make(map[string]string)
for name, board := range m {
has, exist := uids[board.UID()]
if exist {
return fmt.Errorf("UID '%s' claimed by '%s' is also used by '%s'. UIDs must be unique.", board.UID(), name, has)
}
uids[board.UID()] = name
}

return nil
}

type ErrUidsMissing []string

func (e ErrUidsMissing) Error() string {
return fmt.Sprintf("One or more dashboards have no UID set. UIDs are required for Grizzly to operate properly:\n - %s", strings.Join(e, "\n - "))
}

// GetAPIJSON returns JSON expected by Grafana API
func (b Board) GetAPIJSON() (string, error) {
b.Overwrite = true
j, err := json.MarshalIndent(b, "", " ")
if err != nil {
return "", err
Expand All @@ -48,29 +112,6 @@ func (b Board) GetDashboardJSON() (string, error) {
return string(j), nil
}

func parseDashboards(raw string) (Boards, error) {
var boards Boards
if err := json.Unmarshal([]byte(raw), &boards); err != nil {
return nil, err
}
newBoards := make(Boards)
for key, board := range boards {
board.UID = fmt.Sprintf("%v", board.Dashboard["uid"])
board.Name = key
newBoards[key] = board
}
return newBoards, nil
}

func parseDashboard(raw string) (*Board, error) {
var board Board
if err := json.Unmarshal([]byte(raw), &board); err != nil {
return nil, err
}
board.UID = fmt.Sprintf("%v", board.Dashboard["uid"])
return &board, nil
}

func searchFolder(config Config, name string) (*Folder, error) {
if config.GrafanaURL == "" {
return nil, errors.New("Must set GRAFANA_URL environment variable")
Expand Down Expand Up @@ -106,7 +147,6 @@ func searchFolder(config Config, name string) (*Folder, error) {
}

func getDashboard(config Config, uid string) (*Board, error) {

if config.GrafanaURL == "" {
return nil, errors.New("Must set GRAFANA_URL environment variable")
}
Expand All @@ -116,30 +156,45 @@ func getDashboard(config Config, uid string) (*Board, error) {
return nil, err
}
u.Path = path.Join(u.Path, "api/dashboards/uid", uid)

resp, err := http.Get(u.String())
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode == 404 {

switch resp.StatusCode {
case http.StatusNotFound:
return nil, ErrNotFound
default:
if resp.StatusCode >= 400 {
return nil, errors.New(resp.Status)
}
}
if resp.StatusCode >= 400 {
return nil, errors.New(resp.Status)
}
board, err := parseDashboard(string(body))

data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return board, nil

var board Board
if err := json.Unmarshal(data, &board); err != nil {
return nil, APIErr{err, data}
}

return &board, nil
}

func postDashboard(config Config, board Board) error {
type APIErr struct {
err error
body []byte
}

func (e APIErr) Error() string {
return fmt.Sprintf("Failed to parse response: %s.\n\nResponse:\n%s", e.err, string(e.body))
}

func postDashboard(config Config, board Board) error {
if config.GrafanaURL == "" {
return errors.New("Must set GRAFANA_URL environment variable")
}
Expand All @@ -153,13 +208,29 @@ func postDashboard(config Config, board Board) error {
if err != nil {
return err
}
resp, err := http.Post(u.String(), "application/json", bytes.NewBuffer([]byte(boardJSON)))

resp, err := http.Post(u.String(), "application/json", bytes.NewBufferString(boardJSON))
if err != nil {
return err
}
if resp.StatusCode != 200 {
return fmt.Errorf("Non-200 response from Grafana: %s", resp.Status)

switch resp.StatusCode {
case http.StatusOK:
break
case http.StatusPreconditionFailed:
d := json.NewDecoder(resp.Body)
var r struct {
Message string `json:"message"`
}
if err := d.Decode(&r); err != nil {
return fmt.Errorf("Failed to decode actual error (412 Precondition failed): %s", err)
}
fmt.Println(boardJSON)
return fmt.Errorf("Error while applying '%s' to Grafana: %s", board.UID(), r.Message)
default:
return fmt.Errorf("Non-200 response from Grafana while applying '%s': %s", resp.Status, board.UID())
}

return nil
}

Expand Down
Loading

0 comments on commit d58bc1a

Please sign in to comment.