Skip to content

Commit

Permalink
Add support for pulling and pushing dashboard json (#255)
Browse files Browse the repository at this point in the history
* Add support for pulling and pushing json

* Apply review feedback

* Pass -s flag instead of a separate command to save json spec of the dashboard

* Cleanup parsing json

* Apply review feedback
  • Loading branch information
undef1nd authored Oct 17, 2023
1 parent 39a906d commit a4962f6
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 5 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
*~
*.orig
*.swp
.idea
39 changes: 39 additions & 0 deletions cmd/grr/workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"fmt"
"os"
"strings"
"text/tabwriter"

"github.com/go-clix/cli"
Expand All @@ -11,6 +12,8 @@ import (
log "github.com/sirupsen/logrus"
)

const generalFolderUID = "general"

func getCmd() *cli.Command {
cmd := &cli.Command{
Use: "get <resource-type>.<resource-uid>",
Expand Down Expand Up @@ -67,10 +70,17 @@ func pullCmd() *cli.Command {
var opts grizzly.Opts

cmd.Run = func(cmd *cli.Command, args []string) error {
if err := checkDashboardTarget(opts); err != nil {
return err
}

return grizzly.Pull(args[0], opts)
}

cmd.Flags().BoolVarP(&opts.JSONSpec, "only-spec", "s", false, "this flag is only used for dashboards to output the spec")
return initialiseCmd(cmd, &opts)
}

func showCmd() *cli.Command {
cmd := &cli.Command{
Use: "show <resource-path>",
Expand Down Expand Up @@ -116,15 +126,43 @@ func applyCmd() *cli.Command {
var opts grizzly.Opts

cmd.Run = func(cmd *cli.Command, args []string) error {
if err := checkDashboardTarget(opts); err != nil {
return err
}

resources, err := grizzly.Parse(args[0], opts)
if err != nil {
return err
}
return grizzly.Apply(resources)
}

cmd.Flags().StringVarP(&opts.FolderUID, "folder", "f", generalFolderUID, "folder to push dashboards to")
cmd.Flags().BoolVarP(&opts.JSONSpec, "only-spec", "s", false, "this flag is only used for dashboards to output the spec")
return initialiseCmd(cmd, &opts)
}

// targetsOfKind checks if the specified targets are of certain kind
func targetsOfKind(kind string, opts grizzly.Opts) bool {
for _, t := range opts.Targets {
if !(strings.Contains(t, "/") && strings.Split(t, "/")[0] == kind) {
return false
}
}

return true
}

// checkDashboardTarget ensures that the specified targets are of dashboards kind
func checkDashboardTarget(opts grizzly.Opts) error {
ok := targetsOfKind("Dashboard", opts)
if opts.JSONSpec && !ok {
return fmt.Errorf("-s flag is only supported for dashboards")
}

return nil
}

type jsonnetWatchParser struct {
resourcePath string
opts grizzly.Opts
Expand Down Expand Up @@ -222,6 +260,7 @@ func providersCmd() *cli.Command {
}
return w.Flush()
}

return initialiseLogging(cmd, &opts)
}

Expand Down
4 changes: 4 additions & 0 deletions pkg/grizzly/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ type Opts struct {
Directory bool // Deprecated: now is gathered with os.Stat(<resource-path>)
JsonnetPaths []string
Targets []string

// Used for supporting commands that output dashboard JSON
FolderUID string
JSONSpec bool
}

// PreviewOpts contains options to configure a preview
Expand Down
99 changes: 96 additions & 3 deletions pkg/grizzly/parsing.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,97 @@ func FindResourceFiles(resourcePath string) ([]string, error) {
}

func ParseFile(opts Opts, resourceFile string) (Resources, error) {
if opts.JSONSpec && filepath.Ext(resourceFile) != ".json" {
return nil, fmt.Errorf("when -s flag is passed, command expects only json files as resources")
}

switch filepath.Ext(resourceFile) {
case ".json":
return ParseJSON(resourceFile, opts)
case ".yaml", ".yml":
return ParseYAML(resourceFile, opts)
case ".jsonnet", ".libsonnet", ".json":
case ".jsonnet", ".libsonnet":
return ParseJsonnet(resourceFile, opts)
default:
return nil, fmt.Errorf("%s must be yaml, json or jsonnet", resourceFile)
}
}

func manifestFile(resourceFile string) (bool, error) {
if filepath.Ext(resourceFile) != ".json" {
return false, nil
}

m := map[string]interface{}{}

f, err := os.Open(resourceFile)
if err != nil {
return false, err
}

err = json.NewDecoder(f).Decode(&m)
if err != nil {
return false, err
}

if _, ok := m["spec"]; ok {
return true, nil
}

return false, nil
}

// ParseJSON evaluates a JSON file and parses it into resources
func ParseJSON(resourceFile string, opts Opts) (Resources, error) {
if opts.JSONSpec {
return ParseDashboardJSON(resourceFile, opts)
}

isManifest, err := manifestFile(resourceFile)
if err != nil {
return Resources{}, err
}

// TODO: refactor, no need to read the file twice
if !isManifest {
return ParseDashboardJSON(resourceFile, opts)
}

return ParseJsonnet(resourceFile, opts)
}

// ParseDashboardJSON parses a JSON file with a single dashboard object into a Resources (to align with ParseFile interface)
func ParseDashboardJSON(jsonFile string, opts Opts) (Resources, error) {
if filepath.Ext(jsonFile) != ".json" {
return nil, fmt.Errorf("when -s flag is passed, command expects only json files as resources")
}

f, err := os.Open(jsonFile)
if err != nil {
return nil, err
}

var spec map[string]interface{}
err = json.NewDecoder(f).Decode(&spec)
if err != nil {
return Resources{}, err
}

handler := Registry.Handlers["Dashboard"]

resource := Resource{
"apiVersion": handler.APIVersion(),
"kind": handler.Kind(),
"metadata": map[string]interface{}{
"folder": opts.FolderUID,
"name": spec["uid"],
},
"spec": spec,
}

return Resources{resource}, nil
}

// ParseYAML evaluates a YAML file and parses it into resources
func ParseYAML(yamlFile string, opts Opts) (Resources, error) {
f, err := os.Open(yamlFile)
Expand Down Expand Up @@ -174,12 +255,24 @@ func MarshalYAML(resource Resource, filename string) error {
if err != nil {
return err
}
return writeFile(filename, []byte(y))
}

func MarshalSpecToJSON(resource Resource, filename string) error {
j, err := json.MarshalIndent(resource.Spec(), "", " ")
if err != nil {
return err
}
return writeFile(filename, j)
}

func writeFile(filename string, content []byte) error {
dir := filepath.Dir(filename)
err = os.MkdirAll(dir, 0755)
err := os.MkdirAll(dir, 0755)
if err != nil {
return err
}
err = os.WriteFile(filename, []byte(y), 0644)
err = os.WriteFile(filename, content, 0644)
if err != nil {
return err
}
Expand Down
11 changes: 9 additions & 2 deletions pkg/grizzly/workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ func ListRemote(opts Opts) error {

// Pull pulls remote resources and stores them in the local file system.
// The given resourcePath must be a directory, where all resources will be stored.
// If opts.JSONSpec is true, which is only applicable for dashboards, saves the spec as a JSON file.
func Pull(resourcePath string, opts Opts) error {
isFile, err := isFile(resourcePath)
if err != nil {
Expand Down Expand Up @@ -144,8 +145,14 @@ func Pull(resourcePath string, opts Opts) error {
return err
}

path := filepath.Join(resourcePath, handler.ResourceFilePath(*resource, "yaml"))
err = MarshalYAML(*resource, path)
if opts.JSONSpec {
path := filepath.Join(resourcePath, handler.ResourceFilePath(*resource, "json"))
err = MarshalSpecToJSON(*resource, path)
} else {
path := filepath.Join(resourcePath, handler.ResourceFilePath(*resource, "yaml"))
err = MarshalYAML(*resource, path)
}

if err != nil {
return err
}
Expand Down

0 comments on commit a4962f6

Please sign in to comment.