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  }