github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/class/create.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 class 21 22 import ( 23 "context" 24 "fmt" 25 "os" 26 "strings" 27 28 "github.com/ghodss/yaml" 29 "github.com/spf13/cobra" 30 corev1 "k8s.io/api/core/v1" 31 "k8s.io/apimachinery/pkg/api/errors" 32 "k8s.io/apimachinery/pkg/api/resource" 33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 34 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 35 "k8s.io/apimachinery/pkg/runtime" 36 "k8s.io/cli-runtime/pkg/genericiooptions" 37 "k8s.io/client-go/dynamic" 38 cmdutil "k8s.io/kubectl/pkg/cmd/util" 39 utilcomp "k8s.io/kubectl/pkg/util/completion" 40 "k8s.io/kubectl/pkg/util/templates" 41 42 "github.com/1aal/kubeblocks/apis/apps/v1alpha1" 43 "github.com/1aal/kubeblocks/pkg/class" 44 "github.com/1aal/kubeblocks/pkg/cli/types" 45 "github.com/1aal/kubeblocks/pkg/cli/util" 46 "github.com/1aal/kubeblocks/pkg/constant" 47 ) 48 49 type CreateOptions struct { 50 genericiooptions.IOStreams 51 52 // REVIEW: make this field a parameter which can be set by user 53 objectName string 54 55 Factory cmdutil.Factory 56 dynamic dynamic.Interface 57 ClusterDefRef string 58 ComponentType string 59 ClassName string 60 CPU string 61 Memory string 62 File string 63 } 64 65 var classCreateExamples = templates.Examples(` 66 # Create a class for component mysql in cluster definition apecloud-mysql, which has 1 CPU core and 1Gi memory 67 kbcli class create custom-1c1g --cluster-definition apecloud-mysql --type mysql --cpu 1 --memory 1Gi 68 69 # Create classes for component mysql in cluster definition apecloud-mysql, with classes defined in file 70 kbcli class create --cluster-definition apecloud-mysql --type mysql --file ./classes.yaml 71 `) 72 73 func NewCreateCommand(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 74 o := CreateOptions{IOStreams: streams} 75 cmd := &cobra.Command{ 76 Use: "create [NAME]", 77 Short: "Create a class", 78 Example: classCreateExamples, 79 Run: func(cmd *cobra.Command, args []string) { 80 util.CheckErr(o.complete(f)) 81 util.CheckErr(o.validate(args)) 82 util.CheckErr(o.run()) 83 }, 84 } 85 cmd.Flags().StringVar(&o.ClusterDefRef, "cluster-definition", "", "Specify cluster definition, run \"kbcli clusterdefinition list\" to show all available cluster definitions") 86 util.CheckErr(cmd.MarkFlagRequired("cluster-definition")) 87 cmd.Flags().StringVar(&o.ComponentType, "type", "", "Specify component type") 88 util.CheckErr(cmd.MarkFlagRequired("type")) 89 90 cmd.Flags().StringVar(&o.CPU, corev1.ResourceCPU.String(), "", "Specify component CPU cores") 91 cmd.Flags().StringVar(&o.Memory, corev1.ResourceMemory.String(), "", "Specify component memory size") 92 93 cmd.Flags().StringVar(&o.File, "file", "", "Specify file path of class definition YAML") 94 95 // register flag completion func 96 registerFlagCompletionFunc(cmd, f) 97 98 return cmd 99 } 100 101 func (o *CreateOptions) validate(args []string) error { 102 // validate creating by resource arguments 103 if o.File != "" { 104 return nil 105 } 106 107 // validate cpu and memory 108 if _, err := resource.ParseQuantity(o.CPU); err != nil { 109 return err 110 } 111 if _, err := resource.ParseQuantity(o.Memory); err != nil { 112 return err 113 } 114 115 // validate class name 116 if len(args) == 0 { 117 return fmt.Errorf("missing class name") 118 } 119 o.ClassName = args[0] 120 121 return nil 122 } 123 124 func (o *CreateOptions) complete(f cmdutil.Factory) error { 125 var err error 126 o.dynamic, err = f.DynamicClient() 127 return err 128 } 129 130 func (o *CreateOptions) run() error { 131 clsMgr, err := GetManager(o.dynamic, o.ClusterDefRef) 132 if err != nil { 133 return err 134 } 135 136 constraints, err := GetResourceConstraints(o.dynamic) 137 if err != nil { 138 return err 139 } 140 141 var ( 142 classInstances []*v1alpha1.ComponentClass 143 componentClassGroups []v1alpha1.ComponentClassGroup 144 ) 145 146 if o.File != "" { 147 data, err := os.ReadFile(o.File) 148 if err != nil { 149 return err 150 } 151 if err := yaml.Unmarshal(data, &componentClassGroups); err != nil { 152 return err 153 } 154 classDefinition := v1alpha1.ComponentClassDefinition{ 155 Spec: v1alpha1.ComponentClassDefinitionSpec{Groups: componentClassGroups}, 156 } 157 newClasses, err := class.ParseComponentClasses(classDefinition) 158 if err != nil { 159 return err 160 } 161 for _, cls := range newClasses { 162 classInstances = append(classInstances, cls) 163 } 164 } else { 165 cls := v1alpha1.ComponentClass{Name: o.ClassName, CPU: resource.MustParse(o.CPU), Memory: resource.MustParse(o.Memory)} 166 if err != nil { 167 return err 168 } 169 componentClassGroups = []v1alpha1.ComponentClassGroup{ 170 { 171 Series: []v1alpha1.ComponentClassSeries{ 172 { 173 Classes: []v1alpha1.ComponentClass{cls}, 174 }, 175 }, 176 }, 177 } 178 classInstances = append(classInstances, &cls) 179 } 180 181 var ( 182 classNames []string 183 objName = o.objectName 184 ) 185 if objName == "" { 186 objName = class.GetCustomClassObjectName(o.ClusterDefRef, o.ComponentType) 187 } 188 189 var rules []v1alpha1.ResourceConstraintRule 190 for _, constraint := range constraints { 191 rules = append(rules, constraint.FindRules(o.ClusterDefRef, o.ComponentType)...) 192 } 193 194 for _, item := range classInstances { 195 clsDefRef := v1alpha1.ClassDefRef{Name: objName, Class: item.Name} 196 if clsMgr.HasClass(o.ComponentType, clsDefRef) { 197 return fmt.Errorf("class name conflicted %s", item.Name) 198 } 199 classNames = append(classNames, item.Name) 200 201 if len(rules) == 0 { 202 continue 203 } 204 205 match := false 206 for _, rule := range rules { 207 if rule.ValidateResources(item.ToResourceRequirements().Requests) { 208 match = true 209 break 210 } 211 } 212 if !match { 213 return fmt.Errorf("class %s does not conform to its constraints", item.Name) 214 } 215 } 216 217 obj, err := o.dynamic.Resource(types.ComponentClassDefinitionGVR()).Get(context.TODO(), objName, metav1.GetOptions{}) 218 if err != nil && !errors.IsNotFound(err) { 219 return err 220 } 221 222 var classDefinition v1alpha1.ComponentClassDefinition 223 if err == nil { 224 if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &classDefinition); err != nil { 225 return err 226 } 227 classDefinition.Spec.Groups = append(classDefinition.Spec.Groups, componentClassGroups...) 228 unstructuredMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&classDefinition) 229 if err != nil { 230 return err 231 } 232 if _, err = o.dynamic.Resource(types.ComponentClassDefinitionGVR()).Update( 233 context.Background(), &unstructured.Unstructured{Object: unstructuredMap}, metav1.UpdateOptions{}); err != nil { 234 return err 235 } 236 } else { 237 gvr := types.ComponentClassDefinitionGVR() 238 classDefinition = v1alpha1.ComponentClassDefinition{ 239 TypeMeta: metav1.TypeMeta{ 240 Kind: types.KindComponentClassDefinition, 241 APIVersion: gvr.Group + "/" + gvr.Version, 242 }, 243 ObjectMeta: metav1.ObjectMeta{ 244 Name: class.GetCustomClassObjectName(o.ClusterDefRef, o.ComponentType), 245 Labels: map[string]string{ 246 constant.ClusterDefLabelKey: o.ClusterDefRef, 247 types.ClassProviderLabelKey: "user", 248 constant.KBAppComponentDefRefLabelKey: o.ComponentType, 249 }, 250 }, 251 Spec: v1alpha1.ComponentClassDefinitionSpec{ 252 Groups: componentClassGroups, 253 }, 254 } 255 unstructuredMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&classDefinition) 256 if err != nil { 257 return err 258 } 259 if _, err = o.dynamic.Resource(types.ComponentClassDefinitionGVR()).Create( 260 context.Background(), &unstructured.Unstructured{Object: unstructuredMap}, metav1.CreateOptions{}); err != nil { 261 return err 262 } 263 } 264 _, _ = fmt.Fprintf(o.Out, "Successfully create class [%s].\n", strings.Join(classNames, ",")) 265 return nil 266 } 267 268 func registerFlagCompletionFunc(cmd *cobra.Command, f cmdutil.Factory) { 269 util.CheckErr(cmd.RegisterFlagCompletionFunc( 270 "cluster-definition", 271 func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 272 return utilcomp.CompGetResource(f, util.GVRToString(types.ClusterDefGVR()), toComplete), cobra.ShellCompDirectiveNoFileComp 273 })) 274 util.CheckErr(cmd.RegisterFlagCompletionFunc( 275 "type", 276 func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 277 var ( 278 componentTypes []string 279 selector string 280 compTypeLabel = "apps.kubeblocks.io/component-def-ref" 281 ) 282 283 client, err := f.DynamicClient() 284 if err != nil { 285 return componentTypes, cobra.ShellCompDirectiveNoFileComp 286 } 287 288 clusterDefinition, err := cmd.Flags().GetString("cluster-definition") 289 if err == nil && clusterDefinition != "" { 290 selector = fmt.Sprintf("%s=%s,%s", constant.ClusterDefLabelKey, clusterDefinition, types.ClassProviderLabelKey) 291 } 292 objs, err := client.Resource(types.ComponentClassDefinitionGVR()).List(context.Background(), metav1.ListOptions{LabelSelector: selector}) 293 if err != nil { 294 return componentTypes, cobra.ShellCompDirectiveNoFileComp 295 } 296 var classDefinitionList v1alpha1.ComponentClassDefinitionList 297 if err = runtime.DefaultUnstructuredConverter.FromUnstructured(objs.UnstructuredContent(), &classDefinitionList); err != nil { 298 return componentTypes, cobra.ShellCompDirectiveNoFileComp 299 } 300 for _, item := range classDefinitionList.Items { 301 componentType := item.Labels[compTypeLabel] 302 if componentType != "" { 303 componentTypes = append(componentTypes, componentType) 304 } 305 } 306 return componentTypes, cobra.ShellCompDirectiveNoFileComp 307 })) 308 }