Skip to content

Commit

Permalink
Re-porting #247 to main
Browse files Browse the repository at this point in the history
  • Loading branch information
malcolmholmes committed Jan 4, 2024
1 parent a80f3ac commit 2b076b7
Show file tree
Hide file tree
Showing 12 changed files with 382 additions and 137 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/go-clix/cli v0.2.0
github.com/go-openapi/runtime v0.26.2
github.com/gobwas/glob v0.2.3
github.com/goccy/go-yaml v1.11.2
github.com/google/go-jsonnet v0.20.0
github.com/grafana/grafana-openapi-client-go v0.0.0-20231219151618-11c46332acad
github.com/grafana/synthetic-monitoring-agent v0.16.5
Expand Down Expand Up @@ -89,6 +90,7 @@ require (
golang.org/x/sys v0.15.0 // indirect
golang.org/x/term v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 // indirect
google.golang.org/grpc v1.58.3 // indirect
google.golang.org/protobuf v1.31.0 // indirect
Expand Down
60 changes: 8 additions & 52 deletions go.sum

Large diffs are not rendered by default.

114 changes: 114 additions & 0 deletions pkg/encoding/grizzly.jsonnet
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
local main = import '%s';

local convert(main, apiVersion) = {
local makeResource(kind, name, spec=null, data=null, metadata={}) = {
apiVersion: apiVersion,
kind: kind,
metadata: {
name: std.strReplace(std.strReplace(std.strReplace(name, '.json', ''), '.yaml', ''), '.yml', ''),
} + metadata,
[if spec != null then 'spec']: spec,
[if data != null then 'data']: std.manifestJsonEx(data, ' '),
},

local formatUID(name) =
local is_alpha(x) = std.member("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_", x);
std.join("", std.filter(is_alpha, std.stringChars(name))),

grafana: {
folders:
if ('grafanaDashboardFolder' in main) && main.grafanaDashboardFolder != 'General'
then makeResource(
'DashboardFolder',
formatUID(main.grafanaDashboardFolder),
spec={
title: main.grafanaDashboardFolder,
}),
dashboards:
local uid(k, dashboard) =
if std.objectHasAll(dashboard, "uid")
then dashboard.uid
else k;
local folder =
if 'grafanaDashboardFolder' in main
then formatUID(main.grafanaDashboardFolder)
else 'General';
local fromMap(dashboards, folder) = [
makeResource(
'Dashboard',
uid(k, dashboards[k]),
spec=dashboards[k] + {
uid::'',
},
metadata={ folder: folder }
)
for k in std.objectFields(dashboards)
];
if 'grafanaDashboards' in main
then fromMap(main.grafanaDashboards, folder)
else {},

datasources:
local fromMap(datasources) = [
makeResource(
'Datasource',
k,
spec=datasources[k] + {
name:: ''
},
)
for k in std.objectFields(datasources)
];
if 'grafanaDatasources' in main
then fromMap(main.grafanaDatasources)
else {},
},

prometheus:
local forceNamespace(contents) =
// if rulesGroup isn't namespaced (monitoring-mixins), then put them into default namespace
if std.objectHas(contents, 'groups') then
// no namespace, wrap into default namespace
{ 'grizzly_rules': contents }
else
// already has namespace
contents
;
local fromMap(key) =
if key in main then
local allNamespaced = forceNamespace(main[key]);
[

makeResource(
'PrometheusRuleGroup',
g.name,
spec={
rules: g.rules,
},
metadata={ namespace: ns }
)

for ns in std.objectFields(allNamespaced)
for g in allNamespaced[ns].groups
]
else [];
fromMap('prometheusRules')
+ fromMap('prometheusAlerts'),

syntheticMonitoringChecks:
local fromMap(checks) = [
makeResource(
'SyntheticMonitoringCheck',
checks[k].job,
spec=checks[k] + {
job::'',
},
metadata={type: std.objectFields(checks[k].settings)[0]}
)
for k in std.objectFields(checks)
];
if 'syntheticMonitoring' in main
then fromMap(main.syntheticMonitoring)
else {},
};
convert(main, 'grizzly.grafana.com/v1alpha1') + main
28 changes: 28 additions & 0 deletions pkg/encoding/json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package encoding

import (
"encoding/json"
"os"
"path/filepath"
)

func MarshalToJSON(spec map[string]interface{}, filename string) error {
j, err := json.MarshalIndent(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)
if err != nil {
return err
}
err = os.WriteFile(filename, content, 0644)
if err != nil {
return err
}
return nil
}
57 changes: 57 additions & 0 deletions pkg/encoding/jsonnet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package encoding

import (
_ "embed"
"encoding/json"
"fmt"
"os"

"github.com/google/go-jsonnet"
"github.com/grafana/tanka/pkg/jsonnet/native"
"github.com/grafana/tanka/pkg/kubernetes/manifest"
"github.com/grafana/tanka/pkg/process"
)

//go:embed grizzly.jsonnet
var script string

// ParseJsonnet evaluates a jsonnet file and parses it into an object tree
func ParseJsonnet(jsonnetFile string, jsonnetPaths []string) (map[string]manifest.Manifest, error) {
if _, err := os.Stat(jsonnetFile); os.IsNotExist(err) {
return nil, fmt.Errorf("file does not exist: %s", jsonnetFile)
}

script := fmt.Sprintf(script, jsonnetFile)
vm := jsonnet.MakeVM()
currentWorkingDirectory, err := os.Getwd()
if err != nil {
return nil, err
}

vm.Importer(newExtendedImporter(jsonnetFile, currentWorkingDirectory, jsonnetPaths))
for _, nf := range native.Funcs() {
vm.NativeFunction(nf)
}

result, err := vm.EvaluateAnonymousSnippet(jsonnetFile, script)
if err != nil {
return nil, err
}

var data interface{}
if err := json.Unmarshal([]byte(result), &data); err != nil {
return nil, err
}

extracted, err := process.Extract(data)
if err != nil {
return nil, err
}

// Unwrap *List types
if err := process.Unwrap(extracted); err != nil {
return nil, err
}

return extracted, nil
}
84 changes: 84 additions & 0 deletions pkg/encoding/jsonnetplumbing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package encoding

import (
"path/filepath"

"github.com/google/go-jsonnet"
)

// ExtendedImporter does stuff
type ExtendedImporter struct {
loaders []importLoader // for loading jsonnet from somewhere. First one that returns non-nil is used
processors []importProcessor // for post-processing (e.g. yaml -> json)
}

type importLoader func(importedFrom, importedPath string) (c *jsonnet.Contents, foundAt string, err error)

// importProcessor are executed after the file import and may modify the result
// further
type importProcessor func(contents, foundAt string) (c *jsonnet.Contents, err error)

// newFileLoader returns an importLoader that uses jsonnet.FileImporter to source
// files from the local filesystem
func newFileLoader(fi *jsonnet.FileImporter) importLoader {
return func(importedFrom, importedPath string) (contents *jsonnet.Contents, foundAt string, err error) {
var c jsonnet.Contents
c, foundAt, err = fi.Import(importedFrom, importedPath)
return &c, foundAt, err
}
}

func newExtendedImporter(jsonnetFile, path string, jpath []string) *ExtendedImporter {
absolutePaths := make([]string, len(jpath)*2+1)
absolutePaths = append(absolutePaths, path)
jsonnetDir := filepath.Dir(jsonnetFile)
for _, p := range jpath {
if !filepath.IsAbs(p) {
p = filepath.Join(jsonnetDir, p)
}
absolutePaths = append(absolutePaths, p)
}
for _, p := range jpath {
if !filepath.IsAbs(p) {
p = filepath.Join(path, p)
}
absolutePaths = append(absolutePaths, p)
}
return &ExtendedImporter{
loaders: []importLoader{
newFileLoader(&jsonnet.FileImporter{
JPaths: absolutePaths,
})},
processors: []importProcessor{},
}
}

// Import implements the functionality offered by the ExtendedImporter
func (i *ExtendedImporter) Import(importedFrom, importedPath string) (contents jsonnet.Contents, foundAt string, err error) {
// load using loader
for _, loader := range i.loaders {
c, f, err := loader(importedFrom, importedPath)
if err != nil {
return jsonnet.Contents{}, "", err
}
if c != nil {
contents = *c
foundAt = f
break
}
}

// check if needs postprocessing
for _, processor := range i.processors {
c, err := processor(contents.String(), foundAt)
if err != nil {
return jsonnet.Contents{}, "", err
}
if c != nil {
contents = *c
break
}
}

return contents, foundAt, nil
}
73 changes: 73 additions & 0 deletions pkg/encoding/yaml.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package encoding

import (
"io"
"math"
"os"
"path/filepath"
"strconv"

"github.com/goccy/go-yaml"
)

// NewYAMLDecoder returns a YAML decoder configured to unmarshal data from the given reader.
func NewYAMLDecoder(reader io.Reader) *yaml.Decoder {
return yaml.NewDecoder(reader)
}

// MarshalYAML takes an input and renders as a YAML string.
func MarshalYAML(input any) (string, error) {
y, err := yaml.MarshalWithOptions(
input,
yaml.Indent(4),
yaml.IndentSequence(true),
yaml.UseLiteralStyleIfMultiline(true),
yaml.CustomMarshaler[float64](func(v float64) ([]byte, error) {
// goccy/go-yaml tends to add .0 suffixes to floats, even when they're not required.
// To preserve consistency with go-yaml/yaml, this custom marshaler disables that feature.

if v == math.Inf(0) {
return []byte(".inf"), nil
}
if v == math.Inf(-1) {
return []byte("-.inf"), nil
}
if math.IsNaN(v) {
return []byte(".nan"), nil
}

return []byte(strconv.FormatFloat(v, 'g', -1, 64)), nil
}),
)
if err != nil {
return "", err
}

return string(y), nil
}

// MarshalYAMLFile takes an input and renders it to a file as a YAML string.
func MarshalYAMLFile(input any, filename string) error {
y, err := MarshalYAML(input)
if err != nil {
return err
}

dir := filepath.Dir(filename)
err = os.MkdirAll(dir, 0755)
if err != nil {
return err
}

err = os.WriteFile(filename, []byte(y), 0644)
if err != nil {
return err
}

return nil
}

// UnmarshalYAML takes YAML content as input unmarshals it into the destination.
func UnmarshalYAML(input []byte, destination any) error {
return yaml.Unmarshal(input, destination)
}
Loading

0 comments on commit 2b076b7

Please sign in to comment.