-
Notifications
You must be signed in to change notification settings - Fork 8
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
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
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 | ||
} |
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) | ||
} | ||
} |
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 | ||
} |
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 | ||
) |
Large diffs are not rendered by default.
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: { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why not import the one that's already written and exported? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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/ There was a problem hiding this comment. Choose a reason for hiding this commentThe 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}, ...] | ||
} | ||
} |
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"` | ||
} |
There was a problem hiding this comment.
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.