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

Implements CLI tool for rule management #7712

Merged
merged 5 commits into from
Mar 29, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
150 changes: 150 additions & 0 deletions go/cmd/rulesctl/cmd/add.go
Original file line number Diff line number Diff line change
@@ -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
}
46 changes: 46 additions & 0 deletions go/cmd/rulesctl/cmd/explain.go
Original file line number Diff line number Diff line change
@@ -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())
}
}
55 changes: 55 additions & 0 deletions go/cmd/rulesctl/cmd/list.go
Original file line number Diff line number Diff line change
@@ -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
}
38 changes: 38 additions & 0 deletions go/cmd/rulesctl/cmd/main.go
Original file line number Diff line number Diff line change
@@ -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
}
48 changes: 48 additions & 0 deletions go/cmd/rulesctl/cmd/remove.go
Original file line number Diff line number Diff line change
@@ -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
}
39 changes: 39 additions & 0 deletions go/cmd/rulesctl/common/common.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
14 changes: 14 additions & 0 deletions go/cmd/rulesctl/main.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading