diff --git a/go/cmd/rulesctl/cmd/add.go b/go/cmd/rulesctl/cmd/add.go new file mode 100644 index 00000000000..76e80999360 --- /dev/null +++ b/go/cmd/rulesctl/cmd/add.go @@ -0,0 +1,150 @@ +package cmd + +import ( + "log" + "os" + "strings" + + "github.com/spf13/cobra" + + "vitess.io/vitess/go/cmd/rulesctl/common" + "vitess.io/vitess/go/vt/vttablet/tabletserver/planbuilder" + vtrules "vitess.io/vitess/go/vt/vttablet/tabletserver/rules" +) + +var ( + addOptDryrun bool + addOptName string + addOptDescription string + addOptAction string + addOptPlans []string + addOptTables []string + addOptQueryRE string + // TODO: other stuff, bind vars etc +) + +func runAdd(cmd *cobra.Command, args []string) { + rulePlans := mkPlanSlice() + ruleAction := mkAction() + + rule := vtrules.NewQueryRule(addOptDescription, addOptName, ruleAction) + for _, pt := range rulePlans { + rule.AddPlanCond(pt) + } + + for _, t := range addOptTables { + rule.AddTableCond(t) + } + + if err := rule.SetQueryCond(addOptQueryRE); err != nil { + log.Fatalf("Query condition invalid '%v': %v", addOptQueryRE, err) + } + + var rules *vtrules.Rules + _, err := os.Stat(configFile) + if os.IsNotExist(err) { + rules = vtrules.New() + } else { + rules = common.GetRules(configFile) + } + existingRule := rules.Find(rule.Name) + if existingRule != nil { + log.Fatalf("Rule by name %q already exists", rule.Name) + } + rules.Add(rule) + + if addOptDryrun { + common.MustPrintJSON(rules) + } else { + common.MustWriteJSON(rules, configFile) + } +} + +func mkPlanSlice() []planbuilder.PlanType { + if len(addOptPlans) == 0 { + return nil + } + + plans := []planbuilder.PlanType{} + badPlans := []string{} + + for _, p := range addOptPlans { + if pbn, ok := planbuilder.PlanByNameIC(p); ok { + plans = append(plans, pbn) + } else { + badPlans = append(badPlans, p) + } + } + + if len(badPlans) != 0 { + log.Fatalf("Unknown PlanType(s) %q", badPlans) + } + + return plans +} + +func mkAction() vtrules.Action { + switch strings.ToLower(addOptAction) { + case "fail": + return vtrules.QRFail + case "fail_retry": + return vtrules.QRFailRetry + case "continue": + return vtrules.QRContinue + default: + log.Fatalf("Unknown action '%v'", addOptAction) + } + + panic("Nope") +} + +func Add() *cobra.Command { + addCmd := &cobra.Command{ + Use: "add-rule", + Short: "Adds a rule to the config file", + Args: cobra.NoArgs, + Run: runAdd, + } + + addCmd.Flags().BoolVarP( + &addOptDryrun, + "dry-run", "d", + false, + "Instead of writing the config file back print the result to stdout") + addCmd.Flags().StringVarP( + &addOptName, + "name", "n", + "", + "The name of the rule to add (required)") + addCmd.Flags().StringVarP( + &addOptDescription, + "description", "e", + "", + "The purpose/description of the rule being added") + addCmd.Flags().StringVarP( + &addOptAction, + "action", "a", + "", + "What action should be taken when this rule is matched {continue, fail, fail-retry} (required)") + addCmd.Flags().StringSliceVarP( + &addOptPlans, + "plan", "p", + nil, + "Which query plan types does this rule match; see \"explain query-plans\" for details; may be specified multiple times") + addCmd.Flags().StringSliceVarP( + &addOptTables, + "table", "t", + nil, + "Queries will only match if running against these tables; may be specified multiple times") + addCmd.Flags().StringVarP( + &addOptQueryRE, + "query", "q", + "", + "A regexp that will be applied to a query in order to determine if it matches") + + for _, f := range []string{"name", "action"} { + addCmd.MarkFlagRequired(f) + } + + return addCmd +} diff --git a/go/cmd/rulesctl/cmd/explain.go b/go/cmd/rulesctl/cmd/explain.go new file mode 100644 index 00000000000..db0b3d8eabf --- /dev/null +++ b/go/cmd/rulesctl/cmd/explain.go @@ -0,0 +1,46 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + + "vitess.io/vitess/go/vt/vttablet/tabletserver/planbuilder" +) + +func Explain() *cobra.Command { + explain := &cobra.Command{ + Use: "explain [concept]", + Short: "Explains a concept, valid options are: query-plans", + Args: cobra.ExactArgs(1), + Run: runExplain, + } + return explain +} + +func runExplain(cmd *cobra.Command, args []string) { + lookup := map[string]func(){ + "query-plans": helpQueryPlans, + } + + if fn, ok := lookup[args[0]]; ok { + fn() + } else { + fmt.Printf("I don't know anything about %q, sorry!", args[0]) + } +} + +func helpQueryPlans() { + fmt.Printf(`Query Plans! + +A query plan is the type of work the Tablet is about to do. When used in a rule +it can be used to limit the class of queries that a rule can impact. In other +words it will allow you to say "this rule only fails inserts" or "this rule only +fails selects." + +The list of valid plan types that can be used follows: +`) + for i := 0; i < int(planbuilder.NumPlans); i++ { + fmt.Printf(" - %v\n", planbuilder.PlanType(i).String()) + } +} diff --git a/go/cmd/rulesctl/cmd/list.go b/go/cmd/rulesctl/cmd/list.go new file mode 100644 index 00000000000..30a7456c477 --- /dev/null +++ b/go/cmd/rulesctl/cmd/list.go @@ -0,0 +1,55 @@ +package cmd + +import ( + "github.com/spf13/cobra" + + "vitess.io/vitess/go/cmd/rulesctl/common" +) + +func List() *cobra.Command { + var listOptName string + var listOptNamesOnly bool + listCmd := &cobra.Command{ + Use: "list", + Short: "Display the rules in the config file", + Args: cobra.NoArgs, + } + + listCmd.Flags().StringVarP( + &listOptName, + "name", "n", + "", + "Display a named rule (optional)") + listCmd.Flags().BoolVar( + &listOptNamesOnly, + "names-only", + false, + "Lists only the names of the rules in the config file") + + listCmd.Run = func(cmd *cobra.Command, args []string) { + rules := common.GetRules(configFile) + + var out interface{} + if listOptName == "" { + if listOptNamesOnly { + out = []string{} + for _, r := range rules.CopyUnderlying() { + out = append(out.([]string), r.Name) + } + } else { + out = rules + } + } else { + out = rules.Find(listOptName) + if listOptNamesOnly && out != nil { + out = listOptName + } else if listOptNamesOnly { + out = "" + } + } + + common.MustPrintJSON(out) + } + + return listCmd +} diff --git a/go/cmd/rulesctl/cmd/main.go b/go/cmd/rulesctl/cmd/main.go new file mode 100644 index 00000000000..d80bd7609ba --- /dev/null +++ b/go/cmd/rulesctl/cmd/main.go @@ -0,0 +1,38 @@ +package cmd + +import ( + "flag" + "os" + + "github.com/spf13/cobra" +) + +var configFile string + +func Main() *cobra.Command { + rootCmd := &cobra.Command{ + Use: "rulesctl", + Args: cobra.NoArgs, + PreRun: func(cmd *cobra.Command, args []string) { + tmp := os.Args + os.Args = os.Args[0:1] + flag.Parse() + os.Args = tmp + }, + Run: func(cmd *cobra.Command, _ []string) { cmd.Help() }, + } + + rootCmd.PersistentFlags().StringVarP( + &configFile, + "config-file", "f", + "rules.json", + "the config file we will be using to store query rules") + rootCmd.MarkPersistentFlagFilename("config-file") + + rootCmd.AddCommand(List()) + rootCmd.AddCommand(Remove()) + rootCmd.AddCommand(Add()) + rootCmd.AddCommand(Explain()) + + return rootCmd +} diff --git a/go/cmd/rulesctl/cmd/remove.go b/go/cmd/rulesctl/cmd/remove.go new file mode 100644 index 00000000000..0ca512ee651 --- /dev/null +++ b/go/cmd/rulesctl/cmd/remove.go @@ -0,0 +1,48 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + + "vitess.io/vitess/go/cmd/rulesctl/common" +) + +func Remove() *cobra.Command { + var removeOptName string + var removeOptDryRun bool + + removeCmd := &cobra.Command{ + Use: "remove-rule", + Short: "Removes a named rule from the config file", + Args: cobra.NoArgs, + } + + removeCmd.Flags().StringVarP( + &removeOptName, + "name", "n", + "", + "The named rule to remove (required)") + removeCmd.Flags().BoolVarP( + &removeOptDryRun, + "dry-run", "d", + false, + "Instead of writing the config file back print the result to stdout") + removeCmd.MarkFlagRequired("name") + + removeCmd.Run = func(cmd *cobra.Command, args []string) { + rules := common.GetRules(configFile) + if deleted := rules.Delete(removeOptName); deleted == nil { + fmt.Printf("No rule found: '%v'", removeOptName) + return + } + + if removeOptDryRun { + common.MustPrintJSON(rules) + } else { + common.MustWriteJSON(rules, configFile) + } + } + + return removeCmd +} diff --git a/go/cmd/rulesctl/common/common.go b/go/cmd/rulesctl/common/common.go new file mode 100644 index 00000000000..318512b99d5 --- /dev/null +++ b/go/cmd/rulesctl/common/common.go @@ -0,0 +1,39 @@ +package common + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + + vtfcr "vitess.io/vitess/go/vt/vttablet/customrule/filecustomrule" + "vitess.io/vitess/go/vt/vttablet/tabletserver/rules" +) + +func GetRules(path string) *rules.Rules { + rules, err := vtfcr.ParseRules(path) + if err != nil { + log.Fatalf("Failure attempting to parse rules: %v", err) + } + return rules +} + +func MustPrintJSON(obj interface{}) { + enc, err := json.MarshalIndent(obj, "", " ") + if err != nil { + log.Fatalf("Unable to marshal object: %v", err) + } + fmt.Printf("%v\n", string(enc)) +} + +func MustWriteJSON(obj interface{}, path string) { + enc, err := json.MarshalIndent(obj, "", " ") + if err != nil { + log.Fatalf("Unable to marshal object: %v", err) + } + + err = ioutil.WriteFile(path, enc, 0400) + if err != nil { + log.Fatalf("Unable to save new JSON: %v", err) + } +} diff --git a/go/cmd/rulesctl/main.go b/go/cmd/rulesctl/main.go new file mode 100644 index 00000000000..d9fc5bee404 --- /dev/null +++ b/go/cmd/rulesctl/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "log" + + "vitess.io/vitess/go/cmd/rulesctl/cmd" +) + +func main() { + rootCmd := cmd.Main() + if err := rootCmd.Execute(); err != nil { + log.Printf("%v", err) + } +} diff --git a/go/vt/vttablet/customrule/filecustomrule/filecustomrule.go b/go/vt/vttablet/customrule/filecustomrule/filecustomrule.go index caa35a9a998..4ad7b64da45 100644 --- a/go/vt/vttablet/customrule/filecustomrule/filecustomrule.go +++ b/go/vt/vttablet/customrule/filecustomrule/filecustomrule.go @@ -53,6 +53,25 @@ func NewFileCustomRule() (fcr *FileCustomRule) { return fcr } +// ParseRules will construct a Rules object based on a file path. In the case +// of error it returns nil and that error. A log will be printed to capture the +// stage at which parsing failed. +func ParseRules(path string) (*rules.Rules, error) { + data, err := ioutil.ReadFile(path) + if err != nil { + log.Warningf("Error reading file %v: %v", path, err) + // Don't update any internal cache, just return error + return nil, err + } + qrs := rules.New() + err = qrs.UnmarshalJSON(data) + if err != nil { + log.Warningf("Error unmarshaling query rules %v", err) + return nil, err + } + return qrs, nil +} + // Open try to build query rules from local file and push the rules to vttablet func (fcr *FileCustomRule) Open(qsc tabletserver.Controller, rulePath string) error { fcr.path = rulePath @@ -60,16 +79,9 @@ func (fcr *FileCustomRule) Open(qsc tabletserver.Controller, rulePath string) er // Don't go further if path is empty return nil } - data, err := ioutil.ReadFile(fcr.path) - if err != nil { - log.Warningf("Error reading file %v: %v", fcr.path, err) - // Don't update any internal cache, just return error - return err - } - qrs := rules.New() - err = qrs.UnmarshalJSON(data) + + qrs, err := ParseRules(rulePath) if err != nil { - log.Warningf("Error unmarshaling query rules %v", err) return err } fcr.currentRuleSetTimestamp = time.Now().Unix() diff --git a/go/vt/vttablet/tabletserver/planbuilder/plan.go b/go/vt/vttablet/tabletserver/planbuilder/plan.go index d0b9381fcbe..b8ae054cd81 100644 --- a/go/vt/vttablet/tabletserver/planbuilder/plan.go +++ b/go/vt/vttablet/tabletserver/planbuilder/plan.go @@ -18,6 +18,7 @@ package planbuilder import ( "encoding/json" + "strings" "vitess.io/vitess/go/sqltypes" "vitess.io/vitess/go/vt/sqlparser" @@ -119,6 +120,16 @@ func PlanByName(s string) (pt PlanType, ok bool) { return NumPlans, false } +// PlanByNameIC finds a plan type by its string name without case sensitivity +func PlanByNameIC(s string) (pt PlanType, ok bool) { + for i, v := range planName { + if strings.EqualFold(v, s) { + return PlanType(i), true + } + } + return NumPlans, false +} + // IsSelect returns true if PlanType is about a select query. func (pt PlanType) IsSelect() bool { return pt == PlanSelect || pt == PlanSelectImpossible diff --git a/go/vt/vttablet/tabletserver/rules/rules.go b/go/vt/vttablet/tabletserver/rules/rules.go index 8dd25ef7450..3cca04cee31 100644 --- a/go/vt/vttablet/tabletserver/rules/rules.go +++ b/go/vt/vttablet/tabletserver/rules/rules.go @@ -72,6 +72,16 @@ func (qrs *Rules) Copy() (newqrs *Rules) { return newqrs } +// CopyUnderlying makes a copy of the underlying rule array and returns it to +// the caller. +func (qrs *Rules) CopyUnderlying() []*Rule { + cpy := make([]*Rule, 0, len(qrs.rules)) + for _, r := range qrs.rules { + cpy = append(cpy, r.Copy()) + } + return cpy +} + // Append merges the rules from another Rules into the receiver func (qrs *Rules) Append(otherqrs *Rules) { qrs.rules = append(qrs.rules, otherqrs.rules...)