Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cli: convert pop to mixin #29

Merged
merged 4 commits into from
Sep 14, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions cmd/pop/eval.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package main

import (
"github.com/go-clix/cli"
"github.com/pollypkg/polly/pkg/pop"
)

func mixCmd() *cli.Command {
cmd := &cli.Command{
Use: "mix <path>",
Short: "output pop in mixin compatible format",
Args: cli.ArgsExact(1),
}

printer := cmd.Flags().StringP("output", "o", "json", "output format. One of json, yaml")
system := cmd.Flags().StringP("system", "s", "", "choose subsystem. One of alerts, rules, grafana (default all)")

cmd.Run = func(cmd *cli.Command, args []string) error {
p, err := pop.Load(args[0])
if err != nil {
return err
}

mix, err := p.Mixin()
if err != nil {
return err
}

var out interface{} = mix
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could inline choosePrinter and not have to use interface{} anymore, if I'm not mistaken.

switch *system {
case "alerts":
out = map[string]interface{}{"groups": mix.PrometheusAlerts}
case "rules":
out = map[string]interface{}{"groups": mix.PrometheusRules}
case "grafana":
out = mix.GrafanaDashboards
}

return choosePrinter(*printer).Print(out)
}

return cmd
}
26 changes: 26 additions & 0 deletions cmd/pop/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package main

import (
"log"
"os"

"cuelang.org/go/cue/errors"
"github.com/go-clix/cli"
)

func main() {
log.SetFlags(0)

cmd := cli.Command{
Use: "pop",
}

cmd.AddCommand(
mixCmd(),
)

if err := cmd.Execute(); err != nil {
errors.Print(os.Stderr, err, nil)
os.Exit(1)
}
}
49 changes: 49 additions & 0 deletions cmd/pop/out.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package main

import (
"encoding/json"
"fmt"
"log"

"sigs.k8s.io/yaml"
)

type Printer interface {
Print(interface{}) error
}

func choosePrinter(p string) Printer {
switch p {
case "json":
return JSONPrinter{}
case "yaml", "yml":
return YAMLPrinter{}
}

log.Printf("warning: unknown printer '%s'. Falling back to 'json'", p)
return JSONPrinter{}
}

type JSONPrinter struct{}

func (j JSONPrinter) Print(i interface{}) error {
data, err := json.MarshalIndent(i, "", " ")
if err != nil {
return err
}

fmt.Println(string(data))
return nil
}

type YAMLPrinter struct{}

func (y YAMLPrinter) Print(i interface{}) error {
data, err := yaml.Marshal(i)
if err != nil {
return err
}

fmt.Print(string(data))
return nil
}
9 changes: 9 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module github.com/pollypkg/polly

go 1.16

require (
cuelang.org/go v0.4.0
github.com/go-clix/cli v0.2.0
sigs.k8s.io/yaml v1.2.0
)
214 changes: 214 additions & 0 deletions go.sum

Large diffs are not rendered by default.

49 changes: 49 additions & 0 deletions pkg/pop/mix.cue
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import "encoding/json"

// TODO: include full schema?
// TODO: figure out why it stops working when uncommented (cue eval works tho)
// prometheusAlerts: v0: {...}
// prometheusRules: v0: {...}
// grafanaDashboards: v0: {...}

// TODO: handle different versions, deal with migrations, etc, etc
mixin: {
sh0rez marked this conversation as resolved.
Show resolved Hide resolved
// TODO: change to ruleConvert() once syntax sugar is available
_alerts: #ruleConvert&{_, #rules: prometheusAlerts.v0}
_rules: #ruleConvert&{_, #rules: prometheusRules.v0}
_dashboards: #dashboardConvert&{#dashboards: grafanaDashboards.v0}

{
prometheusAlerts: _alerts
prometheusRules: _rules
grafanaDashboards: _dashboards
}
}

#ruleConvert: {
#rules: [string]: {group: string, alert: {...}, ...}

[for n, r in (#flattenAlerts & {arg: #rules}).out {
name: n, rules: r
}]
}

#dashboardConvert: {
#dashboards: [string]: _

for k, v in #dashboards {
"\(k).json": json.Indent(json.Marshal(v), "", " ")
}
}

#flattenAlerts: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not import the one that's already written and exported?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bad argument: I did not manage to get imports working

good argument: whatever the binary needs should be self contained, we should not rely on the users cue.mod to have what we need. that also makes it predictable

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally agreed, but i think that for this case, it's fine to embed everything contained in the same CUE module and thereby treat those imports as safe. By CUE's own rules, those don't trigger package management.

In fact, i'd even argue that not including the entire tree that shares an import namespace/cue.mod actually violates the principle of least surprise inasmuch as it disallows same-module imports.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. Let's address this in a future PR

arg: [string]: { group: string, alert: {...} }
out: [string]: [...]
_inter: [string]: [string]: {...}
for n, v in arg {
_inter: "\(v.group)": "\(n)": v.alert
}
for g, a in _inter {
out: "\(g)": [ for v in a {v}, ...]
}
}
92 changes: 92 additions & 0 deletions pkg/pop/pop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package pop

import (
_ "embed"
"fmt"

"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
"cuelang.org/go/cue/errors"
"cuelang.org/go/cue/load"
)

// Pop represents a polly package
type Pop struct {
v cue.Value
ctx *cue.Context
}

// New constructs a pop from a cue.Value
func New(v cue.Value) *Pop {
return &Pop{v: v, ctx: v.Context()}
}

// Load a polly package from disk
func Load(path string) (*Pop, error) {
inst := load.Instances([]string{path}, nil)
if len(inst) != 1 {
return nil, fmt.Errorf("polly requires exactly one instance. Found %d at '%s'", len(inst), path)
}

ctx := cuecontext.New()
v := ctx.BuildInstance(inst[0])
if err := v.Err(); err != nil {
return nil, err
}

if err := v.Validate(); err != nil {
return nil, err
}

return New(v), nil
}

// Value returns the underlying cue.Value
func (p Pop) Value() cue.Value {
return p.v
}

// special CUE file to convert polly packages to mixin format
//go:embed mix.cue
var mixCue string

// Mixin converts the polly package to mixin compatible format
func (p Pop) Mixin() (*Mixin, error) {
mixer := p.v.Context().CompileString(mixCue,
cue.Filename("<polly/mix.cue>"),
cue.Scope(p.v),
)
if mixer.Err() != nil {
panic(fmt.Errorf("failed loading internal mix.cue! Please raise an issue. Error:\n%s", errors.Details(mixer.Err(), nil)))
}

mixed := mixer.Unify(p.v)
if mixed.Err() != nil {
return nil, mixed.Err()
}

if err := mixed.Validate(cue.Concrete(true)); err != nil {
return nil, err
}

mixin := mixer.LookupPath(cue.ParsePath("mixin"))

m := Mixin{
GrafanaDashboards: make(map[string]string),
PrometheusRules: make([]interface{}, 0),
PrometheusAlerts: make([]interface{}, 0),
}

if err := mixin.Decode(&m); err != nil {
return nil, fmt.Errorf("decoding CUE into struct: %w", err)
}

return &m, nil
}

// Mixin compatible format representation
type Mixin struct {
GrafanaDashboards map[string]string `json:"grafanaDashboards"`
PrometheusRules []interface{} `json:"prometheusRules"`
PrometheusAlerts []interface{} `json:"prometheusAlerts"`
}