github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/addon/addon.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 addon 21 22 import ( 23 "context" 24 "encoding/json" 25 "fmt" 26 "math" 27 "path" 28 "sort" 29 "strconv" 30 "strings" 31 32 "github.com/jedib0t/go-pretty/v6/table" 33 "github.com/spf13/cobra" 34 "github.com/spf13/pflag" 35 corev1 "k8s.io/api/core/v1" 36 "k8s.io/apimachinery/pkg/api/resource" 37 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 38 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 39 "k8s.io/apimachinery/pkg/runtime" 40 "k8s.io/cli-runtime/pkg/genericiooptions" 41 discoverycli "k8s.io/client-go/discovery" 42 "k8s.io/client-go/dynamic" 43 cmdutil "k8s.io/kubectl/pkg/cmd/util" 44 "k8s.io/kubectl/pkg/util/templates" 45 "k8s.io/utils/strings/slices" 46 47 extensionsv1alpha1 "github.com/1aal/kubeblocks/apis/extensions/v1alpha1" 48 "github.com/1aal/kubeblocks/pkg/cli/cmd/plugin" 49 "github.com/1aal/kubeblocks/pkg/cli/list" 50 "github.com/1aal/kubeblocks/pkg/cli/patch" 51 "github.com/1aal/kubeblocks/pkg/cli/printer" 52 "github.com/1aal/kubeblocks/pkg/cli/types" 53 "github.com/1aal/kubeblocks/pkg/cli/util" 54 "github.com/1aal/kubeblocks/pkg/constant" 55 viper "github.com/1aal/kubeblocks/pkg/viperx" 56 ) 57 58 type addonEnableFlags struct { 59 MemorySets []string 60 CPUSets []string 61 StorageSets []string 62 ReplicaCountSets []string 63 StorageClassSets []string 64 TolerationsSet []string 65 SetValues []string 66 Force bool 67 } 68 69 func (r *addonEnableFlags) useDefault() bool { 70 return len(r.MemorySets) == 0 && 71 len(r.CPUSets) == 0 && 72 len(r.StorageSets) == 0 && 73 len(r.ReplicaCountSets) == 0 && 74 len(r.StorageClassSets) == 0 && 75 len(r.TolerationsSet) == 0 76 } 77 78 type addonCmdOpts struct { 79 genericiooptions.IOStreams 80 81 Factory cmdutil.Factory 82 dynamic dynamic.Interface 83 84 addon extensionsv1alpha1.Addon 85 86 *patch.Options 87 addonEnableFlags *addonEnableFlags 88 89 complete func(self *addonCmdOpts, cmd *cobra.Command, args []string) error 90 } 91 92 // NewAddonCmd for addon functions 93 func NewAddonCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 94 cmd := &cobra.Command{ 95 Use: "addon COMMAND", 96 Short: "Addon command.", 97 } 98 cmd.AddCommand( 99 newListCmd(f, streams), 100 newDescribeCmd(f, streams), 101 newEnableCmd(f, streams), 102 newDisableCmd(f, streams), 103 ) 104 return cmd 105 } 106 107 func newListCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 108 o := list.NewListOptions(f, streams, types.AddonGVR()) 109 cmd := &cobra.Command{ 110 Use: "list", 111 Short: "List addons.", 112 Aliases: []string{"ls"}, 113 ValidArgsFunction: util.ResourceNameCompletionFunc(f, o.GVR), 114 Run: func(cmd *cobra.Command, args []string) { 115 o.Names = args 116 util.CheckErr(addonListRun(o)) 117 }, 118 } 119 o.AddFlags(cmd, true) 120 return cmd 121 } 122 123 func newDescribeCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 124 o := &addonCmdOpts{ 125 Options: patch.NewOptions(f, streams, types.AddonGVR()), 126 Factory: f, 127 IOStreams: streams, 128 complete: addonDescribeHandler, 129 } 130 cmd := &cobra.Command{ 131 Use: "describe ADDON_NAME", 132 Short: "Describe an addon specification.", 133 Args: cobra.ExactArgs(1), 134 Aliases: []string{"desc"}, 135 ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.AddonGVR()), 136 Run: func(cmd *cobra.Command, args []string) { 137 util.CheckErr(o.init(args)) 138 util.CheckErr(o.fetchAddonObj()) 139 util.CheckErr(o.complete(o, cmd, args)) 140 }, 141 } 142 return cmd 143 } 144 145 func newEnableCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 146 o := &addonCmdOpts{ 147 Options: patch.NewOptions(f, streams, types.AddonGVR()), 148 Factory: f, 149 IOStreams: streams, 150 addonEnableFlags: &addonEnableFlags{}, 151 complete: addonEnableDisableHandler, 152 } 153 154 o.Options.OutputOperation = func(didPatch bool) string { 155 if didPatch { 156 return "enabled" 157 } 158 return "enabled (no change)" 159 } 160 161 // # kbcli addon enable flags: 162 // # [--memory [extraName:]<request>/<limit> (can specify multiple if has extra items)] 163 // # [--cpu [extraName:]<request>/<limit> (can specify multiple if has extra items)] 164 // # [--storage [extraName:]<request> (can specify multiple if has extra items)] 165 // # [--replicas [extraName:]<number> (can specify multiple if has extra items)] 166 // # [--storage-class [extraName:]<storage class name> (can specify multiple if has extra items)] 167 // # [--tolerations [extraName:]<toleration JSON list items> (can specify multiple if has extra items)] 168 // # [--dry-run] # TODO 169 170 cmd := &cobra.Command{ 171 Use: "enable ADDON_NAME", 172 Short: "Enable an addon.", 173 Args: cobra.ExactArgs(1), 174 ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.AddonGVR()), 175 Example: templates.Examples(` 176 # Enabled "prometheus" addon 177 kbcli addon enable prometheus 178 179 # Enabled "prometheus" addon with custom resources settings 180 kbcli addon enable prometheus --memory 512Mi/4Gi --storage 8Gi --replicas 2 181 182 # Enabled "prometheus" addon and its extra alertmanager component with custom resources settings 183 kbcli addon enable prometheus --memory 512Mi/4Gi --storage 8Gi --replicas 2 \ 184 --memory alertmanager:16Mi/256Mi --storage alertmanager:1Gi --replicas alertmanager:2 185 186 # Enabled "prometheus" addon with tolerations 187 kbcli addon enable prometheus \ 188 --tolerations '[{"key":"taintkey","operator":"Equal","effect":"NoSchedule","value":"true"}]' \ 189 --tolerations 'alertmanager:[{"key":"taintkey","operator":"Equal","effect":"NoSchedule","value":"true"}]' 190 191 # Enabled "prometheus" addon with helm like custom settings 192 kbcli addon enable prometheus --set prometheus.alertmanager.image.tag=v0.24.0 193 194 # Force enabled "csi-s3" addon 195 kbcli addon enable csi-s3 --force 196 `), 197 Run: func(cmd *cobra.Command, args []string) { 198 util.CheckErr(o.init(args)) 199 util.CheckErr(o.fetchAddonObj()) 200 util.CheckErr(o.validate()) 201 util.CheckErr(o.complete(o, cmd, args)) 202 util.CheckErr(o.Run(cmd)) 203 }, 204 } 205 cmd.Flags().StringArrayVar(&o.addonEnableFlags.MemorySets, "memory", []string{}, 206 "Sets addon memory resource values (--memory [extraName:]<request>/<limit>) (can specify multiple if has extra items))") 207 cmd.Flags().StringArrayVar(&o.addonEnableFlags.CPUSets, "cpu", []string{}, 208 "Sets addon CPU resource values (--cpu [extraName:]<request>/<limit>) (can specify multiple if has extra items))") 209 cmd.Flags().StringArrayVar(&o.addonEnableFlags.StorageSets, "storage", []string{}, 210 `Sets addon storage size (--storage [extraName:]<request>) (can specify multiple if has extra items)). 211 Additional notes: 212 1. Specify '0' value will remove storage values settings and explicitly disable 'persistentVolumeEnabled' attribute. 213 2. For Helm type Addon, that resizing storage will fail if modified value is a storage request size 214 that belongs to StatefulSet's volume claim template, to resolve 'Failed' Addon status possible action is disable and 215 re-enable the addon (More info on how-to resize a PVC: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources). 216 `) 217 cmd.Flags().StringArrayVar(&o.addonEnableFlags.ReplicaCountSets, "replicas", []string{}, 218 "Sets addon component replica count (--replicas [extraName:]<number>) (can specify multiple if has extra items))") 219 cmd.Flags().StringArrayVar(&o.addonEnableFlags.StorageClassSets, "storage-class", []string{}, 220 "Sets addon storage class name (--storage-class [extraName:]<storage class name>) (can specify multiple if has extra items))") 221 cmd.Flags().StringArrayVar(&o.addonEnableFlags.TolerationsSet, "tolerations", []string{}, 222 "Sets addon pod tolerations (--tolerations [extraName:]<toleration JSON list items>) (can specify multiple if has extra items))") 223 cmd.Flags().StringArrayVar(&o.addonEnableFlags.SetValues, "set", []string{}, 224 "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2), it's only being processed if addon's type is helm.") 225 cmd.Flags().BoolVar(&o.addonEnableFlags.Force, "force", false, "ignoring the installable restrictions and forcefully enabling.") 226 227 o.Options.AddFlags(cmd) 228 return cmd 229 } 230 231 func newDisableCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 232 o := &addonCmdOpts{ 233 Options: patch.NewOptions(f, streams, types.AddonGVR()), 234 Factory: f, 235 IOStreams: streams, 236 complete: addonEnableDisableHandler, 237 } 238 239 o.Options.OutputOperation = func(didPatch bool) string { 240 if didPatch { 241 return "disabled" 242 } 243 return "disabled (no change)" 244 } 245 246 cmd := &cobra.Command{ 247 Use: "disable ADDON_NAME", 248 Short: "Disable an addon.", 249 Args: cobra.ExactArgs(1), 250 ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.AddonGVR()), 251 Run: func(cmd *cobra.Command, args []string) { 252 util.CheckErr(o.init(args)) 253 util.CheckErr(o.fetchAddonObj()) 254 util.CheckErr(o.complete(o, cmd, args)) 255 util.CheckErr(o.Run(cmd)) 256 }, 257 } 258 o.Options.AddFlags(cmd) 259 return cmd 260 } 261 262 func (o *addonCmdOpts) init(args []string) error { 263 o.Names = args 264 if o.dynamic == nil { 265 var err error 266 if o.dynamic, err = o.Factory.DynamicClient(); err != nil { 267 return err 268 } 269 } 270 271 // setup _KUBE_SERVER_INFO 272 if viper.Get(constant.CfgKeyServerInfo) == nil { 273 cfg, _ := o.Factory.ToRESTConfig() 274 cli, err := discoverycli.NewDiscoveryClientForConfig(cfg) 275 if err != nil { 276 return err 277 } 278 ver, err := cli.ServerVersion() 279 if err != nil { 280 return err 281 } 282 viper.SetDefault(constant.CfgKeyServerInfo, *ver) 283 } 284 285 return nil 286 } 287 288 func (o *addonCmdOpts) fetchAddonObj() error { 289 ctx := context.TODO() 290 obj, err := o.dynamic.Resource(o.GVR).Get(ctx, o.Names[0], metav1.GetOptions{}) 291 if err != nil { 292 return err 293 } 294 if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &o.addon); err != nil { 295 return err 296 } 297 return nil 298 } 299 300 func (o *addonCmdOpts) validate() error { 301 if o.addonEnableFlags.Force { 302 return nil 303 } 304 if o.addon.Spec.Installable == nil { 305 return nil 306 } 307 for _, s := range o.addon.Spec.Installable.Selectors { 308 if !s.MatchesFromConfig() { 309 return fmt.Errorf("addon %s INSTALLABLE-SELECTOR has no matching requirement", o.Names) 310 } 311 } 312 313 if err := o.installAndUpgradePlugins(); err != nil { 314 fmt.Fprintf(o.Out, "failed to install/upgrade plugins: %v\n", err) 315 } 316 317 return nil 318 } 319 320 func addonDescribeHandler(o *addonCmdOpts, cmd *cobra.Command, args []string) error { 321 printRow := func(tbl *printer.TablePrinter, name string, item *extensionsv1alpha1.AddonInstallSpecItem) { 322 pvEnabled := "" 323 replicas := "" 324 325 if item.PVEnabled != nil { 326 pvEnabled = fmt.Sprintf("%v", *item.PVEnabled) 327 } 328 if item.Replicas != nil { 329 replicas = fmt.Sprintf("%d", *item.Replicas) 330 } 331 332 printQuantity := func(q resource.Quantity, ok bool) string { 333 if ok { 334 return q.String() 335 } 336 return "" 337 } 338 339 q, ok := item.Resources.Requests[corev1.ResourceStorage] 340 storageVal := printQuantity(q, ok) 341 342 q, ok = item.Resources.Requests[corev1.ResourceCPU] 343 cpuVal := printQuantity(q, ok) 344 q, ok = item.Resources.Limits[corev1.ResourceCPU] 345 cpuVal = fmt.Sprintf("%s/%s", cpuVal, printQuantity(q, ok)) 346 347 q, ok = item.Resources.Requests[corev1.ResourceMemory] 348 memVal := printQuantity(q, ok) 349 q, ok = item.Resources.Limits[corev1.ResourceMemory] 350 memVal = fmt.Sprintf("%s/%s", memVal, printQuantity(q, ok)) 351 352 tbl.AddRow(name, 353 replicas, 354 storageVal, 355 cpuVal, 356 memVal, 357 item.StorageClass, 358 item.Tolerations, 359 pvEnabled, 360 ) 361 } 362 printInstalled := func(tbl *printer.TablePrinter) error { 363 installSpec := o.addon.Spec.InstallSpec 364 printRow(tbl, "main", &installSpec.AddonInstallSpecItem) 365 for _, e := range installSpec.ExtraItems { 366 printRow(tbl, e.Name, &e.AddonInstallSpecItem) 367 } 368 return nil 369 } 370 371 var labels []string 372 for k, v := range o.addon.Labels { 373 if strings.Contains(k, constant.APIGroup) { 374 labels = append(labels, fmt.Sprintf("%s=%s", k, v)) 375 } 376 } 377 printer.PrintPairStringToLine("Name", o.addon.Name, 0) 378 printer.PrintPairStringToLine("Description", o.addon.Spec.Description, 0) 379 printer.PrintPairStringToLine("Labels", strings.Join(labels, ","), 0) 380 printer.PrintPairStringToLine("Type", string(o.addon.Spec.Type), 0) 381 if len(o.addon.GetExtraNames()) > 0 { 382 printer.PrintPairStringToLine("Extras", strings.Join(o.addon.GetExtraNames(), ","), 0) 383 } 384 printer.PrintPairStringToLine("Status", string(o.addon.Status.Phase), 0) 385 var autoInstall bool 386 if o.addon.Spec.Installable != nil { 387 autoInstall = o.addon.Spec.Installable.AutoInstall 388 } 389 printer.PrintPairStringToLine("Auto-install", strconv.FormatBool(autoInstall), 0) 390 if len(o.addon.Spec.Installable.GetSelectorsStrings()) > 0 { 391 printer.PrintPairStringToLine("Auto-install selector", strings.Join(o.addon.Spec.Installable.GetSelectorsStrings(), ","), 0) 392 } 393 394 switch o.addon.Status.Phase { 395 case extensionsv1alpha1.AddonEnabled: 396 printer.PrintTitle("Installed Info") 397 printer.PrintLineWithTabSeparator() 398 if err := printer.PrintTable(o.Out, nil, printInstalled, 399 "NAME", "REPLICAS", "STORAGE", "CPU (REQ/LIMIT)", "MEMORY (REQ/LIMIT)", "STORAGE-CLASS", 400 "TOLERATIONS", "PV-ENABLED"); err != nil { 401 return err 402 } 403 default: 404 printer.PrintLineWithTabSeparator() 405 for _, di := range o.addon.Spec.GetSortedDefaultInstallValues() { 406 printInstallable := func(tbl *printer.TablePrinter) error { 407 if len(di.Selectors) == 0 { 408 printer.PrintLineWithTabSeparator( 409 printer.NewPair("Default install selector", "NONE"), 410 ) 411 } else { 412 printer.PrintLineWithTabSeparator( 413 printer.NewPair("Default install selector", strings.Join(di.GetSelectorsStrings(), ",")), 414 ) 415 } 416 installSpec := di.AddonInstallSpec 417 printRow(tbl, "main", &installSpec.AddonInstallSpecItem) 418 for _, e := range installSpec.ExtraItems { 419 printRow(tbl, e.Name, &e.AddonInstallSpecItem) 420 } 421 return nil 422 } 423 if err := printer.PrintTable(o.Out, nil, printInstallable, 424 "NAME", "REPLICAS", "STORAGE", "CPU (REQ/LIMIT)", "MEMORY (REQ/LIMIT)", "STORAGE-CLASS", 425 "TOLERATIONS", "PV-ENABLED"); err != nil { 426 return err 427 } 428 printer.PrintLineWithTabSeparator() 429 } 430 } 431 432 // print failed message 433 if o.addon.Status.Phase == extensionsv1alpha1.AddonFailed { 434 var tbl *printer.TablePrinter 435 printHeader := true 436 for _, c := range o.addon.Status.Conditions { 437 if c.Status == metav1.ConditionTrue { 438 continue 439 } 440 if printHeader { 441 fmt.Fprintln(o.Out, "Failed Message") 442 tbl = printer.NewTablePrinter(o.Out) 443 tbl.Tbl.SetColumnConfigs([]table.ColumnConfig{ 444 {Number: 3, WidthMax: 120}, 445 }) 446 tbl.SetHeader("TIME", "REASON", "MESSAGE") 447 printHeader = false 448 } 449 tbl.AddRow(util.TimeFormat(&c.LastTransitionTime), c.Reason, c.Message) 450 } 451 tbl.Print() 452 } 453 454 return nil 455 } 456 457 func addonEnableDisableHandler(o *addonCmdOpts, cmd *cobra.Command, args []string) error { 458 // record the flags that been set by user 459 var flags []*pflag.Flag 460 cmd.Flags().Visit(func(flag *pflag.Flag) { 461 flags = append(flags, flag) 462 }) 463 return o.buildPatch(flags) 464 } 465 466 func (o *addonCmdOpts) buildEnablePatch(flags []*pflag.Flag, spec, install map[string]interface{}) (err error) { 467 extraNames := o.addon.GetExtraNames() 468 installSpec := extensionsv1alpha1.AddonInstallSpec{ 469 Enabled: true, 470 AddonInstallSpecItem: extensionsv1alpha1.NewAddonInstallSpecItem(), 471 } 472 // only using named return value in defer function 473 defer func() { 474 if err != nil { 475 return 476 } 477 var b []byte 478 b, err = json.Marshal(&installSpec) 479 if err != nil { 480 return 481 } 482 if err = json.Unmarshal(b, &install); err != nil { 483 return 484 } 485 }() 486 487 if o.addonEnableFlags.useDefault() { 488 return nil 489 } 490 491 // extractInstallSpecExtraItem extracts extensionsv1alpha1.AddonInstallExtraItem 492 // for the matching arg name, if not found, appends extensionsv1alpha1.AddonInstallExtraItem 493 // item to installSpec.ExtraItems and returns its pointer. 494 extractInstallSpecExtraItem := func(name string) (*extensionsv1alpha1.AddonInstallExtraItem, error) { 495 var pItem *extensionsv1alpha1.AddonInstallExtraItem 496 for i, eItem := range installSpec.ExtraItems { 497 if eItem.Name == name { 498 pItem = &installSpec.ExtraItems[i] 499 break 500 } 501 } 502 if pItem == nil { 503 if !slices.Contains(extraNames, name) { 504 return nil, fmt.Errorf("invalid extra item name [%s]", name) 505 } 506 installSpec.ExtraItems = append(installSpec.ExtraItems, extensionsv1alpha1.AddonInstallExtraItem{ 507 Name: name, 508 AddonInstallSpecItem: extensionsv1alpha1.NewAddonInstallSpecItem(), 509 }) 510 pItem = &installSpec.ExtraItems[len(installSpec.ExtraItems)-1] 511 } 512 return pItem, nil 513 } 514 515 _tuplesProcessor := func(t []string, s, flag string, 516 valueTransformer func(s, flag string) (interface{}, error), 517 valueAssigner func(*extensionsv1alpha1.AddonInstallSpecItem, interface{}), 518 ) error { 519 l := len(t) 520 var name string 521 var result interface{} 522 switch l { 523 case 2: 524 name = t[0] 525 fallthrough 526 case 1: 527 if valueTransformer != nil { 528 result, err = valueTransformer(t[l-1], flag) 529 if err != nil { 530 return err 531 } 532 } else { 533 result = t[l-1] 534 } 535 default: 536 return fmt.Errorf("wrong flag value --%s=%s", flag, s) 537 } 538 name = strings.TrimSpace(name) 539 if name == "" { 540 valueAssigner(&installSpec.AddonInstallSpecItem, result) 541 } else { 542 pItem, err := extractInstallSpecExtraItem(name) 543 if err != nil { 544 return err 545 } 546 valueAssigner(&pItem.AddonInstallSpecItem, result) 547 } 548 return nil 549 } 550 551 twoTuplesProcessor := func(s, flag string, 552 valueTransformer func(s, flag string) (interface{}, error), 553 valueAssigner func(*extensionsv1alpha1.AddonInstallSpecItem, interface{}), 554 ) error { 555 t := strings.SplitN(s, ":", 2) 556 return _tuplesProcessor(t, s, flag, valueTransformer, valueAssigner) 557 } 558 559 twoTuplesJSONProcessor := func(s, flag string, 560 valueTransformer func(s, flag string) (interface{}, error), 561 valueAssigner func(*extensionsv1alpha1.AddonInstallSpecItem, interface{}), 562 ) error { 563 var jsonArray []map[string]interface{} 564 var t []string 565 566 err := json.Unmarshal([]byte(s), &jsonArray) 567 if err != nil { 568 // not a valid JSON array treat it a 2 tuples 569 t = strings.SplitN(s, ":", 2) 570 } else { 571 t = []string{s} 572 } 573 return _tuplesProcessor(t, s, flag, valueTransformer, valueAssigner) 574 } 575 576 reqLimitResTransformer := func(s, flag string) (interface{}, error) { 577 t := strings.SplitN(s, "/", 2) 578 if len(t) != 2 { 579 return nil, fmt.Errorf("wrong flag value --%s=%s", flag, s) 580 } 581 reqLim := [2]resource.Quantity{} 582 processTuple := func(i int) error { 583 if t[i] == "" { 584 return nil 585 } 586 q, err := resource.ParseQuantity(t[i]) 587 if err != nil { 588 return err 589 } 590 reqLim[i] = q 591 return nil 592 } 593 for i := range t { 594 if err := processTuple(i); err != nil { 595 return nil, fmt.Errorf("wrong flag value --%s=%s, with error %v", flag, s, err) 596 } 597 } 598 return reqLim, nil 599 } 600 601 f := o.addonEnableFlags 602 for _, v := range f.ReplicaCountSets { 603 if err := twoTuplesProcessor(v, "replicas", func(s, flag string) (interface{}, error) { 604 v, err := strconv.Atoi(s) 605 if err != nil { 606 return nil, fmt.Errorf("wrong flag value --%s=%s, with error %v", flag, s, err) 607 } 608 if v < 0 { 609 return nil, fmt.Errorf("wrong flag value --%s=%s replica count value", flag, s) 610 } 611 if v > math.MaxInt32 { 612 return nil, fmt.Errorf("wrong flag value --%s=%s replica count exceed max. value (%d) ", flag, s, math.MaxInt32) 613 } 614 r := int32(v) 615 return &r, nil 616 }, func(item *extensionsv1alpha1.AddonInstallSpecItem, i interface{}) { 617 item.Replicas = i.(*int32) 618 }); err != nil { 619 return err 620 } 621 } 622 623 for _, v := range f.StorageClassSets { 624 if err := twoTuplesProcessor(v, "storage-class", nil, func(item *extensionsv1alpha1.AddonInstallSpecItem, i interface{}) { 625 item.StorageClass = i.(string) 626 }); err != nil { 627 return err 628 } 629 } 630 631 for _, v := range f.TolerationsSet { 632 if err := twoTuplesJSONProcessor(v, "tolerations", nil, func(item *extensionsv1alpha1.AddonInstallSpecItem, i interface{}) { 633 item.Tolerations = i.(string) 634 }); err != nil { 635 return err 636 } 637 } 638 639 for _, v := range f.StorageSets { 640 if err := twoTuplesProcessor(v, "storage", func(s, flag string) (interface{}, error) { 641 q, err := resource.ParseQuantity(s) 642 if err != nil { 643 return nil, fmt.Errorf("wrong flag value --%s=%s, with error %v", flag, s, err) 644 } 645 return q, nil 646 }, func(item *extensionsv1alpha1.AddonInstallSpecItem, i interface{}) { 647 q := i.(resource.Quantity) 648 // for 0 storage size, remove storage request value and explicitly disable `persistentVolumeEnabled` 649 if v, _ := q.AsInt64(); v == 0 { 650 delete(item.Resources.Requests, corev1.ResourceStorage) 651 b := false 652 item.PVEnabled = &b 653 return 654 } 655 item.Resources.Requests[corev1.ResourceStorage] = q 656 // explicitly enable `persistentVolumeEnabled` if with provided storage size setting 657 b := true 658 item.PVEnabled = &b 659 }); err != nil { 660 return err 661 } 662 } 663 664 for _, v := range f.CPUSets { 665 if err := twoTuplesProcessor(v, "cpu", reqLimitResTransformer, func(item *extensionsv1alpha1.AddonInstallSpecItem, i interface{}) { 666 reqLim := i.([2]resource.Quantity) 667 item.Resources.Requests[corev1.ResourceCPU] = reqLim[0] 668 item.Resources.Limits[corev1.ResourceCPU] = reqLim[1] 669 }); err != nil { 670 return err 671 } 672 } 673 674 for _, v := range f.MemorySets { 675 if err := twoTuplesProcessor(v, "memory", reqLimitResTransformer, func(item *extensionsv1alpha1.AddonInstallSpecItem, i interface{}) { 676 reqLim := i.([2]resource.Quantity) 677 item.Resources.Requests[corev1.ResourceMemory] = reqLim[0] 678 item.Resources.Limits[corev1.ResourceMemory] = reqLim[1] 679 }); err != nil { 680 return err 681 } 682 } 683 684 return nil 685 } 686 687 func (o *addonCmdOpts) buildHelmPatch(result map[string]interface{}) error { 688 var helmSpec extensionsv1alpha1.HelmTypeInstallSpec 689 if o.addon.Spec.Helm == nil { 690 helmSpec = extensionsv1alpha1.HelmTypeInstallSpec{ 691 InstallValues: extensionsv1alpha1.HelmInstallValues{ 692 SetValues: o.addonEnableFlags.SetValues, 693 }, 694 } 695 } else { 696 helmSpec = *o.addon.Spec.Helm 697 helmSpec.InstallValues.SetValues = o.addonEnableFlags.SetValues 698 } 699 b, err := json.Marshal(&helmSpec) 700 if err != nil { 701 return err 702 } 703 if err = json.Unmarshal(b, &result); err != nil { 704 return err 705 } 706 return nil 707 } 708 709 func (o *addonCmdOpts) buildPatch(flags []*pflag.Flag) error { 710 var err error 711 spec := map[string]interface{}{} 712 status := map[string]interface{}{} 713 install := map[string]interface{}{} 714 helm := map[string]interface{}{} 715 716 if o.addonEnableFlags != nil { 717 if o.addon.Status.Phase == extensionsv1alpha1.AddonFailed { 718 status["phase"] = nil 719 } 720 if err = o.buildEnablePatch(flags, spec, install); err != nil { 721 return err 722 } 723 724 if err = o.buildHelmPatch(helm); err != nil { 725 return err 726 } 727 } else { 728 if !o.addon.Spec.InstallSpec.GetEnabled() { 729 fmt.Fprintf(o.Out, "%s/%s is already disabled\n", o.GVR.GroupResource().String(), o.Names[0]) 730 return cmdutil.ErrExit 731 } 732 install["enabled"] = false 733 } 734 735 if err = unstructured.SetNestedField(spec, install, "install"); err != nil { 736 return err 737 } 738 739 if err = unstructured.SetNestedField(spec, helm, "helm"); err != nil { 740 return err 741 } 742 743 obj := unstructured.Unstructured{ 744 Object: map[string]interface{}{ 745 "spec": spec, 746 }, 747 } 748 if len(status) > 0 { 749 phase := "" 750 if p, ok := status["phase"]; ok && p != nil { 751 phase = p.(string) 752 } 753 fmt.Printf("patching addon 'status.phase=%s' to 'status.phase=%v' will result addon install spec (spec.install) not being updated\n", 754 o.addon.Status.Phase, phase) 755 obj.Object["status"] = status 756 o.Subresource = "status" 757 } 758 bytes, err := obj.MarshalJSON() 759 if err != nil { 760 return err 761 } 762 o.Patch = string(bytes) 763 return nil 764 } 765 766 func addonListRun(o *list.ListOptions) error { 767 // if format is JSON or YAML, use default printer to output the result. 768 if o.Format == printer.JSON || o.Format == printer.YAML { 769 _, err := o.Run() 770 return err 771 } 772 773 // get and output the result 774 o.Print = false 775 r, err := o.Run() 776 if err != nil { 777 return err 778 } 779 780 infos, err := r.Infos() 781 if err != nil { 782 return err 783 } 784 785 if len(infos) == 0 { 786 fmt.Fprintln(o.IOStreams.Out, "No addon found") 787 return nil 788 } 789 790 printRows := func(tbl *printer.TablePrinter) error { 791 // sort addons with .status.Phase then .metadata.name 792 sort.SliceStable(infos, func(i, j int) bool { 793 toAddon := func(idx int) *extensionsv1alpha1.Addon { 794 addon := &extensionsv1alpha1.Addon{} 795 if err := runtime.DefaultUnstructuredConverter.FromUnstructured(infos[idx].Object.(*unstructured.Unstructured).Object, addon); err != nil { 796 return nil 797 } 798 return addon 799 } 800 iAddon := toAddon(i) 801 jAddon := toAddon(j) 802 if iAddon == nil { 803 return true 804 } 805 if jAddon == nil { 806 return false 807 } 808 if iAddon.Status.Phase == jAddon.Status.Phase { 809 return iAddon.GetName() < jAddon.GetName() 810 } 811 return iAddon.Status.Phase < jAddon.Status.Phase 812 }) 813 for _, info := range infos { 814 addon := &extensionsv1alpha1.Addon{} 815 obj := info.Object.(*unstructured.Unstructured) 816 if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, addon); err != nil { 817 return err 818 } 819 extraNames := addon.GetExtraNames() 820 var selectors []string 821 var autoInstall bool 822 if addon.Spec.Installable != nil { 823 selectors = addon.Spec.Installable.GetSelectorsStrings() 824 autoInstall = addon.Spec.Installable.AutoInstall 825 } 826 label := obj.GetLabels() 827 provider := label[constant.AddonProviderLabelKey] 828 tbl.AddRow(addon.Name, 829 addon.Spec.Type, 830 provider, 831 addon.Status.Phase, 832 autoInstall, 833 strings.Join(selectors, ";"), 834 strings.Join(extraNames, ","), 835 ) 836 } 837 return nil 838 } 839 840 if err = printer.PrintTable(o.Out, nil, printRows, 841 "NAME", "TYPE", "PROVIDER", "STATUS", "AUTO-INSTALL", "AUTO-INSTALLABLE-SELECTOR", "EXTRAS"); err != nil { 842 return err 843 } 844 return nil 845 } 846 847 func (o *addonCmdOpts) installAndUpgradePlugins() error { 848 if len(o.addon.Spec.CliPlugins) == 0 { 849 return nil 850 } 851 852 plugin.InitPlugin() 853 854 paths := plugin.GetKbcliPluginPath() 855 indexes, err := plugin.ListIndexes(paths) 856 if err != nil { 857 return err 858 } 859 860 indexRepositoryToNme := make(map[string]string) 861 for _, index := range indexes { 862 indexRepositoryToNme[index.URL] = index.Name 863 } 864 865 var plugins []string 866 var names []string 867 for _, p := range o.addon.Spec.CliPlugins { 868 names = append(names, p.Name) 869 indexName, ok := indexRepositoryToNme[p.IndexRepository] 870 if !ok { 871 // index not found, add it 872 _, indexName = path.Split(p.IndexRepository) 873 if err := plugin.AddIndex(paths, indexName, p.IndexRepository); err != nil { 874 return err 875 } 876 } 877 plugins = append(plugins, fmt.Sprintf("%s/%s", indexName, p.Name)) 878 } 879 880 installOption := &plugin.PluginInstallOption{ 881 IOStreams: o.IOStreams, 882 } 883 upgradeOption := &plugin.UpgradeOptions{ 884 IOStreams: o.IOStreams, 885 } 886 887 // install plugins 888 if err := installOption.Complete(plugins); err != nil { 889 return err 890 } 891 if err := installOption.Install(); err != nil { 892 return err 893 } 894 895 // upgrade existed plugins 896 if err := upgradeOption.Complete(names); err != nil { 897 return err 898 } 899 if err := upgradeOption.Run(); err != nil { 900 return err 901 } 902 903 return nil 904 }