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 }