github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/class/class_utils.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  	"fmt"
    24  	"sort"
    25  	"strings"
    26  	"sync"
    27  	"text/template"
    28  
    29  	"github.com/ghodss/yaml"
    30  	"golang.org/x/exp/slices"
    31  	corev1 "k8s.io/api/core/v1"
    32  
    33  	"github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    34  )
    35  
    36  var (
    37  	Any = v1alpha1.ClassDefRef{}
    38  )
    39  
    40  type Manager struct {
    41  	sync.RWMutex
    42  
    43  	classes map[string][]*ComponentClassWithRef
    44  
    45  	constraints map[string]*v1alpha1.ComponentResourceConstraint
    46  }
    47  
    48  func NewManager(classDefinitionList v1alpha1.ComponentClassDefinitionList, constraintList v1alpha1.ComponentResourceConstraintList) (*Manager, error) {
    49  	classes, err := getClasses(classDefinitionList)
    50  	if err != nil {
    51  		return nil, err
    52  	}
    53  	constraints := make(map[string]*v1alpha1.ComponentResourceConstraint)
    54  	for idx := range constraintList.Items {
    55  		constraint := constraintList.Items[idx]
    56  		constraints[constraint.Name] = &constraint
    57  	}
    58  	return &Manager{classes: classes, constraints: constraints}, nil
    59  }
    60  
    61  // HasClass returns true if the component has the specified class
    62  func (r *Manager) HasClass(compType string, classDefRef v1alpha1.ClassDefRef) bool {
    63  	compClasses, ok := r.classes[compType]
    64  	if !ok || len(compClasses) == 0 {
    65  		return false
    66  	}
    67  	if classDefRef == Any {
    68  		return true
    69  	}
    70  
    71  	idx := slices.IndexFunc(compClasses, func(v *ComponentClassWithRef) bool {
    72  		if classDefRef.Name != "" && classDefRef.Name != v.ClassDefRef.Name {
    73  			return false
    74  		}
    75  		return classDefRef.Class == v.ClassDefRef.Class
    76  	})
    77  	return idx >= 0
    78  }
    79  
    80  var (
    81  	ErrClassNotFound   = fmt.Errorf("class not found")
    82  	ErrInvalidResource = fmt.Errorf("resource is not conform to the constraints, please check the ComponentResourceConstraint API")
    83  )
    84  
    85  // ValidateResources validates if the resources of the component is invalid
    86  func (r *Manager) ValidateResources(clusterDefRef string, comp *v1alpha1.ClusterComponentSpec) error {
    87  	if comp.ClassDefRef != nil && comp.ClassDefRef.Class != "" {
    88  		if r.HasClass(comp.ComponentDefRef, *comp.ClassDefRef) {
    89  			return nil
    90  		}
    91  		return ErrClassNotFound
    92  	}
    93  
    94  	var rules []v1alpha1.ResourceConstraintRule
    95  	for _, constraint := range r.constraints {
    96  		rules = append(rules, constraint.FindRules(clusterDefRef, comp.ComponentDefRef)...)
    97  	}
    98  	if len(rules) == 0 {
    99  		return nil
   100  	}
   101  
   102  	for _, rule := range rules {
   103  		if !rule.ValidateResources(comp.Resources.Requests) {
   104  			continue
   105  		}
   106  
   107  		// validate volume
   108  		match := true
   109  		// all volumes should match the rules
   110  		for _, volume := range comp.VolumeClaimTemplates {
   111  			if !rule.ValidateStorage(volume.Spec.Resources.Requests.Storage()) {
   112  				match = false
   113  				break
   114  			}
   115  		}
   116  		if match {
   117  			return nil
   118  		}
   119  	}
   120  	return ErrInvalidResource
   121  }
   122  
   123  func (r *Manager) GetResources(clusterDefRef string, comp *v1alpha1.ClusterComponentSpec) (corev1.ResourceList, error) {
   124  	result := corev1.ResourceList{}
   125  
   126  	if comp.ClassDefRef != nil && comp.ClassDefRef.Class != "" {
   127  		cls, err := r.ChooseClass(comp)
   128  		if err != nil {
   129  			return result, err
   130  		}
   131  		return corev1.ResourceList{corev1.ResourceCPU: cls.CPU, corev1.ResourceMemory: cls.Memory}, nil
   132  	}
   133  
   134  	var rules []v1alpha1.ResourceConstraintRule
   135  	for _, constraint := range r.constraints {
   136  		rules = append(rules, constraint.FindRules(clusterDefRef, comp.ComponentDefRef)...)
   137  	}
   138  	if len(rules) == 0 {
   139  		return nil, nil
   140  	}
   141  
   142  	var resourcesList []corev1.ResourceList
   143  	for _, rule := range rules {
   144  		resources := corev1.ResourceList{}
   145  		for k, v := range comp.Resources.Requests {
   146  			resources[k] = v
   147  		}
   148  		if !rule.ValidateResources(resources) {
   149  			continue
   150  		}
   151  		match := true
   152  		for _, volume := range comp.VolumeClaimTemplates {
   153  			if !rule.ValidateStorage(volume.Spec.Resources.Requests.Storage()) {
   154  				match = false
   155  				break
   156  			}
   157  		}
   158  		if !match {
   159  			continue
   160  		}
   161  		if resources.Cpu().IsZero() && resources.Memory().IsZero() {
   162  			resourcesList = append(resourcesList, rule.GetMinimalResources())
   163  		} else {
   164  			resourcesList = append(resourcesList, rule.CompleteResources(resources))
   165  		}
   166  	}
   167  	if len(resourcesList) == 0 {
   168  		return nil, ErrInvalidResource
   169  	}
   170  	sort.Sort(ByResourceList(resourcesList))
   171  	return resourcesList[0], nil
   172  }
   173  
   174  // ChooseClass chooses the classes to be used for a given component with constraints
   175  func (r *Manager) ChooseClass(comp *v1alpha1.ClusterComponentSpec) (*ComponentClassWithRef, error) {
   176  	var (
   177  		cls     *ComponentClassWithRef
   178  		classes = r.classes[comp.ComponentDefRef]
   179  	)
   180  	switch {
   181  	case comp.ClassDefRef != nil && comp.ClassDefRef.Class != "":
   182  		if classes == nil {
   183  			return nil, fmt.Errorf("can not find classes for component %s", comp.ComponentDefRef)
   184  		}
   185  		for _, v := range classes {
   186  			if comp.ClassDefRef.Name != "" && comp.ClassDefRef.Name != v.ClassDefRef.Name {
   187  				continue
   188  			}
   189  
   190  			if comp.ClassDefRef.Class != v.ClassDefRef.Class {
   191  				continue
   192  			}
   193  
   194  			if cls == nil || cls.Cmp(&v.ComponentClass) > 0 {
   195  				cls = v
   196  			}
   197  		}
   198  		if cls == nil {
   199  			return nil, fmt.Errorf("unknown component class %s", comp.ClassDefRef.Class)
   200  		}
   201  	case classes != nil:
   202  		candidates := filterClassByResources(classes, comp.Resources.Requests)
   203  		if len(candidates) == 0 {
   204  			return nil, fmt.Errorf("can not find matching class for component %s", comp.Name)
   205  		}
   206  		sort.Sort(ByClassResource(candidates))
   207  		cls = candidates[0]
   208  	}
   209  	return cls, nil
   210  }
   211  
   212  func (r *Manager) GetClasses() map[string][]*ComponentClassWithRef {
   213  	return r.classes
   214  }
   215  
   216  func filterClassByResources(classes []*ComponentClassWithRef, resources corev1.ResourceList) []*ComponentClassWithRef {
   217  	var candidates []*ComponentClassWithRef
   218  	for _, cls := range classes {
   219  		if !resources.Cpu().IsZero() && !resources.Cpu().Equal(cls.CPU) {
   220  			continue
   221  		}
   222  		if !resources.Memory().IsZero() && !resources.Memory().Equal(cls.Memory) {
   223  			continue
   224  		}
   225  		candidates = append(candidates, cls)
   226  	}
   227  	return candidates
   228  }
   229  
   230  // GetCustomClassObjectName returns the name of the ComponentClassDefinition object containing the custom classes
   231  func GetCustomClassObjectName(cdName string, componentName string) string {
   232  	return fmt.Sprintf("kb.classes.custom.%s.%s", cdName, componentName)
   233  }
   234  
   235  func getClasses(classDefinitionList v1alpha1.ComponentClassDefinitionList) (map[string][]*ComponentClassWithRef, error) {
   236  	var (
   237  		compTypeLabel    = "apps.kubeblocks.io/component-def-ref"
   238  		componentClasses = make(map[string][]*ComponentClassWithRef)
   239  	)
   240  	for _, classDefinition := range classDefinitionList.Items {
   241  		componentType := classDefinition.GetLabels()[compTypeLabel]
   242  		if componentType == "" {
   243  			return nil, fmt.Errorf("can not find component type label %s", compTypeLabel)
   244  		}
   245  		var (
   246  			classes []*ComponentClassWithRef
   247  		)
   248  		if classDefinition.GetGeneration() != 0 &&
   249  			classDefinition.Status.ObservedGeneration == classDefinition.GetGeneration() {
   250  			for idx := range classDefinition.Status.Classes {
   251  				cls := classDefinition.Status.Classes[idx]
   252  				classes = append(classes, &ComponentClassWithRef{
   253  					ComponentClass: cls,
   254  					ClassDefRef:    v1alpha1.ClassDefRef{Name: classDefinition.Name, Class: cls.Name},
   255  				})
   256  			}
   257  		} else {
   258  			classMap, err := ParseComponentClasses(classDefinition)
   259  			if err != nil {
   260  				return nil, err
   261  			}
   262  			for k, v := range classMap {
   263  				classes = append(classes, &ComponentClassWithRef{
   264  					ClassDefRef:    k,
   265  					ComponentClass: *v,
   266  				})
   267  			}
   268  		}
   269  		componentClasses[componentType] = append(componentClasses[componentType], classes...)
   270  	}
   271  
   272  	return componentClasses, nil
   273  }
   274  
   275  // ParseComponentClasses parses ComponentClassDefinition to component classes
   276  func ParseComponentClasses(classDefinition v1alpha1.ComponentClassDefinition) (map[v1alpha1.ClassDefRef]*v1alpha1.ComponentClass, error) {
   277  	genClass := func(nameTpl string, bodyTpl string, vars []string, args []string) (v1alpha1.ComponentClass, error) {
   278  		var result v1alpha1.ComponentClass
   279  		values := make(map[string]interface{})
   280  		for index, key := range vars {
   281  			values[key] = args[index]
   282  		}
   283  
   284  		classStr, err := renderTemplate(bodyTpl, values)
   285  		if err != nil {
   286  			return result, err
   287  		}
   288  
   289  		if err = yaml.Unmarshal([]byte(classStr), &result); err != nil {
   290  			return result, err
   291  		}
   292  
   293  		name, err := renderTemplate(nameTpl, values)
   294  		if err != nil {
   295  			return result, err
   296  		}
   297  		result.Name = name
   298  		return result, nil
   299  	}
   300  
   301  	parser := func(group v1alpha1.ComponentClassGroup, series v1alpha1.ComponentClassSeries, class v1alpha1.ComponentClass) (*v1alpha1.ComponentClass, error) {
   302  		if len(class.Args) > 0 {
   303  			cls, err := genClass(series.NamingTemplate, group.Template, group.Vars, class.Args)
   304  			if err != nil {
   305  				return nil, err
   306  			}
   307  
   308  			if class.Name == "" && cls.Name != "" {
   309  				class.Name = cls.Name
   310  			}
   311  			class.CPU = cls.CPU
   312  			class.Memory = cls.Memory
   313  		}
   314  		result := &v1alpha1.ComponentClass{
   315  			Name:   class.Name,
   316  			CPU:    class.CPU,
   317  			Memory: class.Memory,
   318  		}
   319  		return result, nil
   320  	}
   321  
   322  	result := make(map[v1alpha1.ClassDefRef]*v1alpha1.ComponentClass)
   323  	for _, group := range classDefinition.Spec.Groups {
   324  		for _, series := range group.Series {
   325  			for _, class := range series.Classes {
   326  				out, err := parser(group, series, class)
   327  				if err != nil {
   328  					return nil, err
   329  				}
   330  				key := v1alpha1.ClassDefRef{Name: classDefinition.Name, Class: out.Name}
   331  				if _, exists := result[key]; exists {
   332  					return nil, fmt.Errorf("component class name conflicted: %s", out.Name)
   333  				}
   334  				result[key] = out
   335  			}
   336  		}
   337  	}
   338  	return result, nil
   339  }
   340  
   341  func renderTemplate(tpl string, values map[string]interface{}) (string, error) {
   342  	engine, err := template.New("").Parse(tpl)
   343  	if err != nil {
   344  		return "", err
   345  	}
   346  	var buf strings.Builder
   347  	if err := engine.Execute(&buf, values); err != nil {
   348  		return "", err
   349  	}
   350  	return buf.String(), nil
   351  }