github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/util/flags/flags.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 flags 21 22 import ( 23 "context" 24 "fmt" 25 "strings" 26 27 "github.com/spf13/cobra" 28 "github.com/stoewer/go-strcase" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/apimachinery/pkg/runtime" 31 "k8s.io/client-go/dynamic" 32 "k8s.io/kube-openapi/pkg/validation/spec" 33 cmdutil "k8s.io/kubectl/pkg/cmd/util" 34 utilcomp "k8s.io/kubectl/pkg/util/completion" 35 36 appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1" 37 "github.com/1aal/kubeblocks/pkg/cli/types" 38 "github.com/1aal/kubeblocks/pkg/cli/util" 39 ) 40 41 const ( 42 CobraInt = "int" 43 CobraSting = "string" 44 CobraBool = "bool" 45 CobraFloat64 = "float64" 46 CobraStringArray = "stringArray" 47 CobraIntSlice = "intSlice" 48 CobraFloat64Slice = "float64Slice" 49 CobraBoolSlice = "boolSlice" 50 ) 51 52 const ( 53 typeString = "string" 54 typeNumber = "number" 55 typeInteger = "integer" 56 typeBoolean = "boolean" 57 typeObject = "object" 58 typeArray = "array" 59 ) 60 61 // AddClusterDefinitionFlag adds a flag "cluster-definition" for the cmd and stores the value of the flag 62 // in string p 63 func AddClusterDefinitionFlag(f cmdutil.Factory, cmd *cobra.Command, p *string) { 64 cmd.Flags().StringVar(p, "cluster-definition", *p, "Specify cluster definition, run \"kbcli clusterdefinition list\" to show all available cluster definition") 65 util.CheckErr(cmd.RegisterFlagCompletionFunc("cluster-definition", 66 func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 67 return utilcomp.CompGetResource(f, util.GVRToString(types.ClusterDefGVR()), toComplete), cobra.ShellCompDirectiveNoFileComp 68 })) 69 } 70 71 // BuildFlagsBySchema builds a flag.FlagSet by the given schema, convert the schema key 72 // to flag name, and convert the schema type to flag type. 73 func BuildFlagsBySchema(cmd *cobra.Command, schema *spec.Schema) error { 74 if schema == nil { 75 return nil 76 } 77 78 for name, prop := range schema.Properties { 79 if err := buildOneFlag(cmd, name, &prop, false); err != nil { 80 return err 81 } 82 } 83 84 for _, name := range schema.Required { 85 flagName := strcase.KebabCase(name) 86 // fixme: array/object type flag will be "--servers.name", but in schema.Required will check the "--servers" flag, do not mark the array type as required in schema.json 87 if schema.Properties[name].Type[0] == typeObject || schema.Properties[name].Type[0] == typeArray { 88 continue 89 } 90 if err := cmd.MarkFlagRequired(flagName); err != nil { 91 return err 92 } 93 } 94 95 return nil 96 } 97 98 // castOrZero is a helper function to cast a value to a specific type. 99 // If the cast fails, the zero value will be returned. 100 func castOrZero[T any](v any) T { 101 cv, _ := v.(T) 102 return cv 103 } 104 105 func buildOneFlag(cmd *cobra.Command, k string, s *spec.Schema, isArray bool) error { 106 name := strcase.KebabCase(k) 107 tpe := typeString 108 if len(s.Type) > 0 { 109 tpe = s.Type[0] 110 } 111 112 if isArray { 113 switch tpe { 114 case typeString: 115 cmd.Flags().StringArray(name, []string{castOrZero[string](s.Default)}, buildFlagDescription(s)) 116 case typeInteger: 117 cmd.Flags().IntSlice(name, []int{castOrZero[int](s.Default)}, buildFlagDescription(s)) 118 case typeNumber: 119 cmd.Flags().Float64Slice(name, []float64{castOrZero[float64](s.Default)}, buildFlagDescription(s)) 120 case typeBoolean: 121 cmd.Flags().BoolSlice(name, []bool{castOrZero[bool](s.Default)}, buildFlagDescription(s)) 122 case typeObject: 123 for subName, prop := range s.Properties { 124 if err := buildOneFlag(cmd, fmt.Sprintf("%s.%s", name, subName), &prop, true); err != nil { 125 return err 126 } 127 } 128 case typeArray: 129 return fmt.Errorf("unsupported build flags for object with array nested within an array") 130 default: 131 return fmt.Errorf("unsupported json schema type %s", s.Type) 132 } 133 134 registerFlagCompFunc(cmd, name, s) 135 } else { 136 switch tpe { 137 case typeString: 138 cmd.Flags().String(name, castOrZero[string](s.Default), buildFlagDescription(s)) 139 case typeInteger: 140 cmd.Flags().Int(name, int(castOrZero[float64](s.Default)), buildFlagDescription(s)) 141 case typeNumber: 142 cmd.Flags().Float64(name, castOrZero[float64](s.Default), buildFlagDescription(s)) 143 case typeBoolean: 144 cmd.Flags().Bool(name, castOrZero[bool](s.Default), buildFlagDescription(s)) 145 case typeObject: 146 for subName, prop := range s.Properties { 147 if err := buildOneFlag(cmd, fmt.Sprintf("%s.%s", name, subName), &prop, false); err != nil { 148 return err 149 } 150 } 151 case typeArray: 152 if err := buildOneFlag(cmd, name, s.Items.Schema, true); err != nil { 153 return err 154 } 155 default: 156 return fmt.Errorf("unsupported json schema type %s", s.Type) 157 } 158 159 registerFlagCompFunc(cmd, name, s) 160 } 161 // flag not support array type 162 163 return nil 164 } 165 166 func buildFlagDescription(s *spec.Schema) string { 167 desc := strings.Builder{} 168 desc.WriteString(s.Description) 169 170 var legalVals []string 171 for _, e := range s.Enum { 172 legalVals = append(legalVals, fmt.Sprintf("%v", e)) 173 } 174 if len(legalVals) > 0 { 175 desc.WriteString(fmt.Sprintf(" Legal values [%s].", strings.Join(legalVals, ", "))) 176 } 177 178 var valueRange []string 179 if s.Minimum != nil { 180 valueRange = append(valueRange, fmt.Sprintf("%v", *s.Minimum)) 181 } 182 if s.Maximum != nil { 183 valueRange = append(valueRange, fmt.Sprintf("%v", *s.Maximum)) 184 } 185 if len(valueRange) > 0 { 186 desc.WriteString(fmt.Sprintf(" Value range [%s].", strings.Join(valueRange, ", "))) 187 } 188 return desc.String() 189 } 190 191 func registerFlagCompFunc(cmd *cobra.Command, name string, s *spec.Schema) { 192 // register the enum entry for autocompletion 193 if len(s.Enum) > 0 { 194 var entries []string 195 for _, e := range s.Enum { 196 entries = append(entries, fmt.Sprintf("%s\t", e)) 197 } 198 _ = cmd.RegisterFlagCompletionFunc(name, 199 func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 200 return entries, cobra.ShellCompDirectiveNoFileComp 201 }) 202 return 203 } 204 } 205 206 // AddComponentFlag add flag "component" for cobra.Command and support auto complete for it 207 func AddComponentFlag(f cmdutil.Factory, cmd *cobra.Command, p *string, usage string) { 208 cmd.Flags().StringVar(p, "component", "", usage) 209 util.CheckErr(autoCompleteClusterComponent(cmd, f, "component")) 210 } 211 212 // AddComponentsFlag add flag "components" for cobra.Command and support auto complete for it 213 func AddComponentsFlag(f cmdutil.Factory, cmd *cobra.Command, p *[]string, usage string) { 214 cmd.Flags().StringSliceVar(p, "components", nil, usage) 215 util.CheckErr(autoCompleteClusterComponent(cmd, f, "components")) 216 } 217 218 func autoCompleteClusterComponent(cmd *cobra.Command, f cmdutil.Factory, flag string) error { 219 // getClusterByNames is a hack way to eliminate circular import, combining functions from the cluster.GetK8SClientObject and cluster.GetClusterByName 220 getClusterByName := func(dynamic dynamic.Interface, name string, namespace string) (*appsv1alpha1.Cluster, error) { 221 cluster := &appsv1alpha1.Cluster{} 222 223 unstructuredObj, err := dynamic.Resource(types.ClusterGVR()).Namespace(namespace).Get(context.TODO(), name, metav1.GetOptions{}) 224 if err != nil { 225 return nil, err 226 } 227 if err = runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredObj.UnstructuredContent(), cluster); err != nil { 228 return nil, err 229 } 230 return cluster, nil 231 } 232 233 autoComplete := func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 234 var components []string 235 if len(args) == 0 { 236 return components, cobra.ShellCompDirectiveNoFileComp 237 } 238 namespace, _, _ := f.ToRawKubeConfigLoader().Namespace() 239 dynamic, _ := f.DynamicClient() 240 cluster, _ := getClusterByName(dynamic, args[0], namespace) 241 for _, comp := range cluster.Spec.ComponentSpecs { 242 if strings.HasPrefix(comp.Name, toComplete) { 243 components = append(components, comp.Name) 244 } 245 } 246 return components, cobra.ShellCompDirectiveNoFileComp 247 } 248 return cmd.RegisterFlagCompletionFunc(flag, autoComplete) 249 250 }