diff --git a/go.mod b/go.mod index 5b3ef4c3..5d32ce18 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/gdamore/tcell v1.4.0 github.com/go-clix/cli v0.2.0 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/synthetic-monitoring-agent v0.16.5 github.com/grafana/synthetic-monitoring-api-go-client v0.7.0 @@ -17,7 +18,6 @@ require ( github.com/stretchr/testify v1.8.4 golang.org/x/crypto v0.11.0 gopkg.in/fsnotify.v1 v1.4.7 - gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -53,9 +53,11 @@ require ( golang.org/x/sys v0.10.0 // indirect golang.org/x/term v0.10.0 // indirect golang.org/x/text v0.11.0 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/genproto v0.0.0-20230524185152-1884fd1fac28 // indirect google.golang.org/grpc v1.56.2 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/go.sum b/go.sum index a742da27..e122a8db 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,13 @@ github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU= github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0= github.com/go-clix/cli v0.2.0 h1:rqpcyS/cvshOhXkwii0V+7nWetDVC8cp4pKI7JiCIS8= github.com/go-clix/cli v0.2.0/go.mod h1:yWI9abpv187r47lDjz8Z9TWev93aUTWaW2seSb5JmPQ= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/goccy/go-yaml v1.11.2 h1:joq77SxuyIs9zzxEjgyLBugMQ9NEgTWxXfz2wVqwAaQ= +github.com/goccy/go-yaml v1.11.2/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -58,6 +63,7 @@ github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH github.com/karrick/godirwalk v1.17.0 h1:b4kY7nqDdioR/6qnbHQyDvmA17u5G1cZ6J+CZXwSWoI= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= @@ -160,6 +166,7 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto v0.0.0-20230524185152-1884fd1fac28 h1:+55/MuGJORMxCrkAgo2595fMAnN/4rweCuwibbqrvpc= google.golang.org/genproto v0.0.0-20230524185152-1884fd1fac28/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= diff --git a/pkg/grizzly/grizzly.jsonnet b/pkg/encoding/grizzly.jsonnet similarity index 100% rename from pkg/grizzly/grizzly.jsonnet rename to pkg/encoding/grizzly.jsonnet diff --git a/pkg/encoding/jsonnet.go b/pkg/encoding/jsonnet.go new file mode 100644 index 00000000..85993097 --- /dev/null +++ b/pkg/encoding/jsonnet.go @@ -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.EvaluateSnippet(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 +} diff --git a/pkg/grizzly/jsonnet.go b/pkg/encoding/jsonnetplumbing.go similarity index 99% rename from pkg/grizzly/jsonnet.go rename to pkg/encoding/jsonnetplumbing.go index ce124571..9a1be3a4 100644 --- a/pkg/grizzly/jsonnet.go +++ b/pkg/encoding/jsonnetplumbing.go @@ -1,4 +1,4 @@ -package grizzly +package encoding import ( "path/filepath" diff --git a/pkg/encoding/yaml.go b/pkg/encoding/yaml.go new file mode 100644 index 00000000..01b067a9 --- /dev/null +++ b/pkg/encoding/yaml.go @@ -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) +} diff --git a/pkg/grafana/rules.go b/pkg/grafana/rules.go index a0a62a82..9072f65e 100644 --- a/pkg/grafana/rules.go +++ b/pkg/grafana/rules.go @@ -7,8 +7,8 @@ import ( "os/exec" "strings" + "github.com/grafana/grizzly/pkg/encoding" "github.com/grafana/grizzly/pkg/grizzly" - "gopkg.in/yaml.v3" ) const ( @@ -42,7 +42,7 @@ func getRemoteRuleGroup(uid string) (*grizzly.Resource, error) { return nil, err } groupings := map[string][]PrometheusRuleGroup{} - err = yaml.Unmarshal(out, &groupings) + err = encoding.UnmarshalYAML(out, &groupings) if err != nil { return nil, err } @@ -71,7 +71,7 @@ func getRemoteRuleGroupList() ([]string, error) { return nil, err } groupings := map[string][]PrometheusRuleGroup{} - err = yaml.Unmarshal(out, &groupings) + err = encoding.UnmarshalYAML(out, &groupings) if err != nil { return nil, err } @@ -118,11 +118,11 @@ func writeRuleGroup(resource grizzly.Resource) error { Namespace: resource.GetMetadata("namespace"), Groups: []PrometheusRuleGroup{newGroup}, } - out, err := yaml.Marshal(grouping) + out, err := encoding.MarshalYAML(grouping) if err != nil { return err } - os.WriteFile(tmpfile.Name(), out, 0644) + os.WriteFile(tmpfile.Name(), []byte(out), 0644) output, err := cortexTool("rules", "load", tmpfile.Name()) if err != nil { log.Println("OUTPUT", output) diff --git a/pkg/grafana/rules_test.go b/pkg/grafana/rules_test.go index 84be5d9f..b3358c37 100644 --- a/pkg/grafana/rules_test.go +++ b/pkg/grafana/rules_test.go @@ -5,9 +5,9 @@ import ( "os" "testing" + "github.com/grafana/grizzly/pkg/encoding" "github.com/grafana/grizzly/pkg/grizzly" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" ) var errCortextoolClient = errors.New("error coming from cortextool client") @@ -74,7 +74,7 @@ func TestRules(t *testing.T) { spec := make(map[string]interface{}) file, err := os.ReadFile("testdata/rules.yaml") require.NoError(t, err) - err = yaml.Unmarshal(file, &spec) + err = encoding.UnmarshalYAML(file, &spec) require.NoError(t, err) resource := grizzly.NewResource("apiV", "kind", "name", spec) @@ -89,7 +89,7 @@ func TestRules(t *testing.T) { spec := make(map[string]interface{}) file, err := os.ReadFile("testdata/rules.yaml") require.NoError(t, err) - err = yaml.Unmarshal(file, &spec) + err = encoding.UnmarshalYAML(file, &spec) require.NoError(t, err) resource := grizzly.NewResource("apiV", "kind", "name", spec) diff --git a/pkg/grizzly/parsing.go b/pkg/grizzly/parsing.go index 8a8f23da..c3ffdbab 100644 --- a/pkg/grizzly/parsing.go +++ b/pkg/grizzly/parsing.go @@ -2,8 +2,6 @@ package grizzly import ( "bufio" - _ "embed" - "encoding/json" "fmt" "io" "os" @@ -11,12 +9,9 @@ import ( "sort" "strconv" - "github.com/google/go-jsonnet" - "github.com/grafana/tanka/pkg/jsonnet/native" + "github.com/grafana/grizzly/pkg/encoding" "github.com/grafana/tanka/pkg/kubernetes/manifest" - "github.com/grafana/tanka/pkg/process" log "github.com/sirupsen/logrus" - "gopkg.in/yaml.v3" ) func Parse(resourcePath string, opts Opts) (Resources, error) { @@ -68,7 +63,7 @@ func ParseYAML(yamlFile string, opts Opts) (Resources, error) { return nil, err } reader := bufio.NewReader(f) - decoder := yaml.NewDecoder(reader) + decoder := encoding.NewYAMLDecoder(reader) manifests := map[string]manifest.Manifest{} var m manifest.Manifest var resources Resources @@ -100,44 +95,12 @@ func ParseYAML(yamlFile string, opts Opts) (Resources, error) { return resources, nil } -//go:embed grizzly.jsonnet -var script string - // ParseJsonnet evaluates a jsonnet file and parses it into an object tree func ParseJsonnet(jsonnetFile string, opts Opts) (Resources, 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() + extracted, err := encoding.ParseJsonnet(jsonnetFile, opts.JsonnetPaths) if err != nil { return nil, err } - vm.Importer(newExtendedImporter(jsonnetFile, currentWorkingDirectory, opts.JsonnetPaths)) - for _, nf := range native.Funcs() { - vm.NativeFunction(nf) - } - - result, err := vm.EvaluateSnippet(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 - } resources := Resources{} for _, m := range extracted { @@ -159,21 +122,3 @@ func ParseJsonnet(jsonnetFile string, opts Opts) (Resources, error) { sort.Sort(resources) return resources, nil } - -// MarshalYAML takes a resource and renders it to a source file as a YAML string -func MarshalYAML(resource Resource, filename string) error { - y, err := resource.YAML() - 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 -} diff --git a/pkg/grizzly/providers.go b/pkg/grizzly/providers.go index 9106ac61..b92a735a 100644 --- a/pkg/grizzly/providers.go +++ b/pkg/grizzly/providers.go @@ -5,8 +5,8 @@ import ( "fmt" "github.com/gobwas/glob" + "github.com/grafana/grizzly/pkg/encoding" "github.com/grafana/tanka/pkg/kubernetes/manifest" - "gopkg.in/yaml.v3" ) // Resource represents a single Resource destined for a single endpoint @@ -124,11 +124,7 @@ func (r *Resource) SpecAsJSON() (string, error) { // YAML Gets the string representation for this resource func (r *Resource) YAML() (string, error) { - y, err := yaml.Marshal(*r) - if err != nil { - return "", err - } - return string(y), nil + return encoding.MarshalYAML(*r) } // MatchesTarget identifies whether a resource is in a target list diff --git a/pkg/grizzly/workflow.go b/pkg/grizzly/workflow.go index ad1658a1..6165e159 100644 --- a/pkg/grizzly/workflow.go +++ b/pkg/grizzly/workflow.go @@ -10,6 +10,7 @@ import ( "strings" "text/tabwriter" + "github.com/grafana/grizzly/pkg/encoding" "github.com/grafana/grizzly/pkg/grizzly/notifier" "github.com/grafana/grizzly/pkg/term" "github.com/pmezard/go-difflib/difflib" @@ -139,7 +140,7 @@ func Pull(resourcePath string, opts Opts) error { } path := filepath.Join(resourcePath, handler.ResourceFilePath(*resource, "yaml")) - err = MarshalYAML(*resource, path) + err = encoding.MarshalYAMLFile(*resource, path) if err != nil { return err }