github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/bench/bench.go (about) 1 /* 2 Copyright (C) 2022-2023 ApeCloud Co., Ltd 3 4 This file is part of KubeBlocks project 5 6 This program is free software: you can redistribute it and/or modify 7 it under the terms of the GNU Affero General Public License as published by 8 the Free Software Foundation, either version 3 of the License, or 9 (at your option) any later version. 10 11 This program is distributed in the hope that it will be useful 12 but WITHOUT ANY WARRANTY; without even the implied warranty of 13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 GNU Affero General Public License for more details. 15 16 You should have received a copy of the GNU Affero General Public License 17 along with this program. If not, see <http://www.gnu.org/licenses/>. 18 */ 19 20 package bench 21 22 import ( 23 "context" 24 "encoding/json" 25 "fmt" 26 "sort" 27 "strings" 28 29 "github.com/spf13/cobra" 30 corev1 "k8s.io/api/core/v1" 31 apierrors "k8s.io/apimachinery/pkg/api/errors" 32 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 34 "k8s.io/apimachinery/pkg/runtime/schema" 35 "k8s.io/cli-runtime/pkg/genericiooptions" 36 "k8s.io/cli-runtime/pkg/resource" 37 "k8s.io/client-go/dynamic" 38 clientset "k8s.io/client-go/kubernetes" 39 cmdutil "k8s.io/kubectl/pkg/cmd/util" 40 "k8s.io/kubectl/pkg/util/templates" 41 42 "github.com/1aal/kubeblocks/pkg/cli/cluster" 43 "github.com/1aal/kubeblocks/pkg/cli/list" 44 "github.com/1aal/kubeblocks/pkg/cli/printer" 45 "github.com/1aal/kubeblocks/pkg/cli/types" 46 "github.com/1aal/kubeblocks/pkg/cli/util" 47 ) 48 49 var ( 50 benchListExample = templates.Examples(` 51 # List all benchmarks 52 kbcli bench list 53 `) 54 55 benchDeleteExample = templates.Examples(` 56 # Delete benchmark 57 kbcli bench delete mybench 58 `) 59 60 benchDescribeExample = templates.Examples(` 61 # Describe benchmark 62 kbcli bench describe mybench 63 `) 64 ) 65 66 var benchGVRList = []schema.GroupVersionResource{ 67 types.PgBenchGVR(), 68 types.SysbenchGVR(), 69 types.YcsbGVR(), 70 types.TpccGVR(), 71 types.TpchGVR(), 72 } 73 74 type BenchBaseOptions struct { 75 // define the target database 76 Driver string 77 Database string 78 Host string 79 Port int 80 User string 81 Password string 82 ClusterName string 83 84 // define the config of pod that run benchmark 85 name string 86 namespace string 87 Step string // specify the benchmark step, exec all, cleanup, prepare or run 88 TolerationsRaw []string 89 Tolerations []corev1.Toleration 90 ExtraArgs []string // extra arguments for benchmark 91 92 factory cmdutil.Factory 93 client clientset.Interface 94 dynamic dynamic.Interface 95 *cluster.ClusterObjects 96 genericiooptions.IOStreams 97 } 98 99 func (o *BenchBaseOptions) BaseComplete() error { 100 tolerations, err := util.BuildTolerations(o.TolerationsRaw) 101 if err != nil { 102 return err 103 } 104 tolerationsJSON, err := json.Marshal(tolerations) 105 if err != nil { 106 return err 107 } 108 if err := json.Unmarshal(tolerationsJSON, &o.Tolerations); err != nil { 109 return err 110 } 111 112 return nil 113 } 114 115 // BaseValidate validates the base options 116 // In some cases, for example, in redis, the database is not required, the username is not required 117 // and password can be empty for many databases, 118 // so we don't validate them here 119 func (o *BenchBaseOptions) BaseValidate() error { 120 if o.Driver == "" { 121 return fmt.Errorf("driver is required") 122 } 123 124 if o.Host == "" { 125 return fmt.Errorf("host is required") 126 } 127 128 if o.Port == 0 { 129 return fmt.Errorf("port is required") 130 } 131 132 if err := validateBenchmarkExist(o.factory, o.IOStreams, o.name); err != nil { 133 return err 134 } 135 136 return nil 137 } 138 139 func (o *BenchBaseOptions) AddFlags(cmd *cobra.Command) { 140 cmd.Flags().StringVar(&o.Driver, "driver", "", "the driver of database") 141 cmd.Flags().StringVar(&o.Database, "database", "", "database name") 142 cmd.Flags().StringVar(&o.Host, "host", "", "the host of database") 143 cmd.Flags().StringVar(&o.User, "user", "", "the user of database") 144 cmd.Flags().StringVar(&o.Password, "password", "", "the password of database") 145 cmd.Flags().IntVar(&o.Port, "port", 0, "the port of database") 146 cmd.Flags().StringVar(&o.ClusterName, "cluster", "", "the cluster of database") 147 cmd.Flags().StringSliceVar(&o.TolerationsRaw, "tolerations", nil, `Tolerations for benchmark, such as '"dev=true:NoSchedule,large=true:NoSchedule"'`) 148 cmd.Flags().StringSliceVar(&o.ExtraArgs, "extra-args", nil, "extra arguments for benchmark") 149 150 util.RegisterClusterCompletionFunc(cmd, o.factory) 151 } 152 153 // NewBenchCmd creates the bench command 154 func NewBenchCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 155 cmd := &cobra.Command{ 156 Use: "bench", 157 Short: "Run a benchmark.", 158 } 159 160 // add subcommands 161 cmd.AddCommand( 162 NewSysBenchCmd(f, streams), 163 NewPgBenchCmd(f, streams), 164 NewYcsbCmd(f, streams), 165 NewTpccCmd(f, streams), 166 NewTpchCmd(f, streams), 167 newListCmd(f, streams), 168 newDeleteCmd(f, streams), 169 newDescribeCmd(f, streams), 170 ) 171 172 return cmd 173 } 174 175 type benchListOption struct { 176 Factory cmdutil.Factory 177 LabelSelector string 178 Format string 179 AllNamespaces bool 180 181 genericiooptions.IOStreams 182 } 183 184 type benchDeleteOption struct { 185 factory cmdutil.Factory 186 client clientset.Interface 187 dynamic dynamic.Interface 188 namespace string 189 190 genericiooptions.IOStreams 191 } 192 193 type benchDescribeOption struct { 194 factory cmdutil.Factory 195 client clientset.Interface 196 dynamic dynamic.Interface 197 namespace string 198 benchs []string 199 200 genericiooptions.IOStreams 201 } 202 203 func newListCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 204 o := &benchListOption{ 205 Factory: f, 206 IOStreams: streams, 207 } 208 cmd := &cobra.Command{ 209 Use: "list", 210 Short: "List all benchmarks.", 211 Aliases: []string{"ls"}, 212 Args: cobra.NoArgs, 213 Example: benchListExample, 214 Run: func(cmd *cobra.Command, args []string) { 215 cmdutil.CheckErr(o.run()) 216 }, 217 } 218 219 cmd.Flags().StringVarP(&o.LabelSelector, "selector", "l", o.LabelSelector, "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints.") 220 cmd.Flags().BoolVarP(&o.AllNamespaces, "all-namespaces", "A", o.AllNamespaces, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.") 221 222 return cmd 223 } 224 225 func newDeleteCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 226 o := &benchDeleteOption{ 227 factory: f, 228 IOStreams: streams, 229 } 230 cmd := &cobra.Command{ 231 Use: "delete", 232 Short: "Delete a benchmark.", 233 Aliases: []string{"del"}, 234 Example: benchDeleteExample, 235 ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 236 return registerBenchmarkCompletionFunc(cmd, f, args, toComplete) 237 }, 238 Run: func(cmd *cobra.Command, args []string) { 239 cmdutil.CheckErr(o.complete()) 240 cmdutil.CheckErr(o.run(args)) 241 }, 242 } 243 244 return cmd 245 } 246 247 func newDescribeCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 248 o := &benchDescribeOption{ 249 factory: f, 250 IOStreams: streams, 251 } 252 cmd := &cobra.Command{ 253 Use: "describe", 254 Short: "Describe a benchmark.", 255 Aliases: []string{"desc"}, 256 Example: benchDescribeExample, 257 ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 258 return registerBenchmarkCompletionFunc(cmd, f, args, toComplete) 259 }, 260 Run: func(cmd *cobra.Command, args []string) { 261 cmdutil.CheckErr(o.complete(args)) 262 cmdutil.CheckErr(o.run()) 263 }, 264 } 265 266 return cmd 267 } 268 269 func (o *benchListOption) run() error { 270 defer func() { 271 if err := recover(); err != nil { 272 fmt.Fprintf(o.Out, "it seems that kubebench is not running, please run `kbcli addon enable kubebench` to install it or check the kubebench pod status.\n") 273 } 274 }() 275 276 var infos []*resource.Info 277 for _, gvr := range benchGVRList { 278 bench := list.NewListOptions(o.Factory, o.IOStreams, gvr) 279 280 bench.Print = false 281 bench.LabelSelector = o.LabelSelector 282 bench.AllNamespaces = o.AllNamespaces 283 result, err := bench.Run() 284 if err != nil { 285 if strings.Contains(err.Error(), "the server doesn't have a resource type") { 286 fmt.Fprintf(o.Out, "kubebench is not installed, please run `kbcli addon enable kubebench` to install it.\n") 287 return nil 288 } 289 return err 290 } 291 292 benchInfos, err := result.Infos() 293 if err != nil { 294 return err 295 } 296 infos = append(infos, benchInfos...) 297 } 298 299 if len(infos) == 0 { 300 fmt.Fprintf(o.Out, "No benchmarks found.\n") 301 return nil 302 } 303 304 printRows := func(tbl *printer.TablePrinter) error { 305 // sort bench with kind, then .status.phase, finally .metadata.name 306 sort.SliceStable(infos, func(i, j int) bool { 307 iKind := infos[i].Object.(*unstructured.Unstructured).GetKind() 308 jKind := infos[j].Object.(*unstructured.Unstructured).GetKind() 309 iPhase := infos[i].Object.(*unstructured.Unstructured).Object["status"].(map[string]interface{})["phase"] 310 jPhase := infos[j].Object.(*unstructured.Unstructured).Object["status"].(map[string]interface{})["phase"] 311 iName := infos[i].Object.(*unstructured.Unstructured).GetName() 312 jName := infos[j].Object.(*unstructured.Unstructured).GetName() 313 314 if iKind != jKind { 315 return iKind < jKind 316 } 317 if iPhase != jPhase { 318 return iPhase.(string) < jPhase.(string) 319 } 320 return iName < jName 321 }) 322 323 for _, info := range infos { 324 obj := info.Object.(*unstructured.Unstructured) 325 tbl.AddRow( 326 obj.GetName(), 327 obj.GetNamespace(), 328 obj.GetKind(), 329 obj.Object["status"].(map[string]interface{})["phase"], 330 obj.Object["status"].(map[string]interface{})["completions"], 331 ) 332 } 333 return nil 334 } 335 336 if err := printer.PrintTable(o.Out, nil, printRows, "NAME", "NAMESPACE", "KIND", "STATUS", "COMPLETIONS"); err != nil { 337 return err 338 } 339 return nil 340 } 341 342 func (o *benchDeleteOption) complete() error { 343 var err error 344 345 o.namespace, _, err = o.factory.ToRawKubeConfigLoader().Namespace() 346 if err != nil { 347 return err 348 } 349 350 if o.dynamic, err = o.factory.DynamicClient(); err != nil { 351 return err 352 } 353 354 if o.client, err = o.factory.KubernetesClientSet(); err != nil { 355 return err 356 } 357 358 return nil 359 } 360 361 func (o *benchDeleteOption) run(args []string) error { 362 delete := func(benchName string) error { 363 var found bool 364 365 for _, gvr := range benchGVRList { 366 if err := o.dynamic.Resource(gvr).Namespace(o.namespace).Delete(context.TODO(), benchName, metav1.DeleteOptions{}); err != nil { 367 if apierrors.IsNotFound(err) { 368 continue 369 } 370 return err 371 } 372 373 found = true 374 break 375 } 376 377 if !found { 378 return fmt.Errorf("benchmark %s not found", benchName) 379 } 380 381 return nil 382 } 383 384 for _, benchName := range args { 385 if err := delete(benchName); err != nil { 386 return err 387 } 388 } 389 return nil 390 } 391 392 func (o *benchDescribeOption) complete(args []string) error { 393 var err error 394 395 o.benchs = args 396 397 o.namespace, _, err = o.factory.ToRawKubeConfigLoader().Namespace() 398 if err != nil { 399 return err 400 } 401 402 if o.dynamic, err = o.factory.DynamicClient(); err != nil { 403 return err 404 } 405 406 if o.client, err = o.factory.KubernetesClientSet(); err != nil { 407 return err 408 } 409 410 return nil 411 } 412 413 func (o *benchDescribeOption) run() error { 414 describe := func(benchName string) error { 415 var found bool 416 417 for _, gvr := range benchGVRList { 418 obj, err := o.dynamic.Resource(gvr).Namespace(o.namespace).Get(context.Background(), benchName, metav1.GetOptions{}) 419 if err != nil { 420 if apierrors.IsNotFound(err) { 421 continue 422 } 423 return err 424 } 425 426 found = true 427 428 if err := printer.PrettyPrintObj(obj); err != nil { 429 return err 430 } 431 432 break 433 } 434 435 if !found { 436 return fmt.Errorf("benchmark %s not found", benchName) 437 } 438 439 return nil 440 } 441 442 for _, benchName := range o.benchs { 443 if err := describe(benchName); err != nil { 444 return err 445 } 446 } 447 return nil 448 } 449 450 func registerBenchmarkCompletionFunc(cmd *cobra.Command, f cmdutil.Factory, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 451 var benchs []string 452 for _, gvr := range benchGVRList { 453 comp, _ := util.ResourceNameCompletionFunc(f, gvr)(cmd, args, toComplete) 454 benchs = append(benchs, comp...) 455 } 456 457 return benchs, cobra.ShellCompDirectiveNoFileComp 458 } 459 460 func validateBenchmarkExist(factory cmdutil.Factory, streams genericiooptions.IOStreams, name string) error { 461 var infos []*resource.Info 462 for _, gvr := range benchGVRList { 463 bench := list.NewListOptions(factory, streams, gvr) 464 465 bench.Print = false 466 result, err := bench.Run() 467 if err != nil { 468 if strings.Contains(err.Error(), "the server doesn't have a resource type") { 469 return fmt.Errorf("kubebench is not installed, please run `kbcli addon enable kubebench` to install it") 470 } 471 return err 472 } 473 474 benchInfos, err := result.Infos() 475 if err != nil { 476 return err 477 } 478 infos = append(infos, benchInfos...) 479 } 480 481 for _, info := range infos { 482 if info.Name == name { 483 return fmt.Errorf("benchmark %s already exists", name) 484 } 485 } 486 return nil 487 } 488 489 // parseStepAndName parses the step and name from the given arguments and name prefix. 490 // If no arguments are provided, it sets the step to "all" and generates a random name with the given prefix. 491 // If the first argument is "all", "cleanup", "prepare", or "run", it sets the step to the argument value. 492 // If a second argument is provided, it sets the name to the argument value. 493 // If the first argument is not a valid step value, it sets the name to the first argument value. 494 // Returns the step and name as strings. 495 func parseStepAndName(args []string, namePrefix string) (step, name string) { 496 step = "all" 497 name = fmt.Sprintf("%s-%s", namePrefix, util.RandRFC1123String(6)) 498 499 if len(args) > 0 { 500 switch args[0] { 501 case "all", "cleanup", "prepare", "run": 502 step = args[0] 503 if len(args) > 1 { 504 name = args[1] 505 } 506 default: 507 name = args[0] 508 } 509 } 510 return 511 }