github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/controller/component/component.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 component 21 22 import ( 23 "fmt" 24 "strings" 25 26 corev1 "k8s.io/api/core/v1" 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 29 appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1" 30 "github.com/1aal/kubeblocks/pkg/class" 31 cfgcore "github.com/1aal/kubeblocks/pkg/configuration/core" 32 "github.com/1aal/kubeblocks/pkg/constant" 33 intctrlutil "github.com/1aal/kubeblocks/pkg/controllerutil" 34 viper "github.com/1aal/kubeblocks/pkg/viperx" 35 ) 36 37 func BuildComponent(reqCtx intctrlutil.RequestCtx, 38 clsMgr *class.Manager, 39 cluster *appsv1alpha1.Cluster, 40 clusterDef *appsv1alpha1.ClusterDefinition, 41 clusterCompDef *appsv1alpha1.ClusterComponentDefinition, 42 clusterCompSpec *appsv1alpha1.ClusterComponentSpec, 43 serviceReferences map[string]*appsv1alpha1.ServiceDescriptor, 44 clusterCompVers ...*appsv1alpha1.ClusterComponentVersion, 45 ) (*SynthesizedComponent, error) { 46 return buildComponent(reqCtx, clsMgr, cluster, clusterDef, clusterCompDef, clusterCompSpec, serviceReferences, clusterCompVers...) 47 } 48 49 // buildComponent generates a new Component object, which is a mixture of 50 // component-related configs from input Cluster, ClusterDef and ClusterVersion. 51 func buildComponent(reqCtx intctrlutil.RequestCtx, 52 clsMgr *class.Manager, 53 cluster *appsv1alpha1.Cluster, 54 clusterDef *appsv1alpha1.ClusterDefinition, 55 clusterCompDef *appsv1alpha1.ClusterComponentDefinition, 56 clusterCompSpec *appsv1alpha1.ClusterComponentSpec, 57 serviceReferences map[string]*appsv1alpha1.ServiceDescriptor, 58 clusterCompVers ...*appsv1alpha1.ClusterComponentVersion, 59 ) (*SynthesizedComponent, error) { 60 hasSimplifiedAPI := func() bool { 61 return cluster.Spec.Replicas != nil || 62 !cluster.Spec.Resources.CPU.IsZero() || 63 !cluster.Spec.Resources.Memory.IsZero() || 64 !cluster.Spec.Storage.Size.IsZero() || 65 cluster.Spec.Monitor.MonitoringInterval != nil || 66 cluster.Spec.Network != nil || 67 len(cluster.Spec.Tenancy) > 0 || 68 len(cluster.Spec.AvailabilityPolicy) > 0 69 } 70 71 fillSimplifiedAPI := func() { 72 // fill simplified api only to first defined component 73 if len(clusterDef.Spec.ComponentDefs) == 0 || 74 clusterDef.Spec.ComponentDefs[0].Name != clusterCompDef.Name { 75 return 76 } 77 // return if none of simplified api is defined 78 if !hasSimplifiedAPI() { 79 return 80 } 81 if clusterCompSpec == nil { 82 clusterCompSpec = &appsv1alpha1.ClusterComponentSpec{} 83 clusterCompSpec.Name = clusterCompDef.Name 84 } 85 if cluster.Spec.Replicas != nil { 86 clusterCompSpec.Replicas = *cluster.Spec.Replicas 87 } 88 dataVolumeName := "data" 89 for _, v := range clusterCompDef.VolumeTypes { 90 if v.Type == appsv1alpha1.VolumeTypeData { 91 dataVolumeName = v.Name 92 } 93 } 94 if !cluster.Spec.Resources.CPU.IsZero() || !cluster.Spec.Resources.Memory.IsZero() { 95 clusterCompSpec.Resources.Limits = corev1.ResourceList{} 96 } 97 if !cluster.Spec.Resources.CPU.IsZero() { 98 clusterCompSpec.Resources.Limits["cpu"] = cluster.Spec.Resources.CPU 99 } 100 if !cluster.Spec.Resources.Memory.IsZero() { 101 clusterCompSpec.Resources.Limits["memory"] = cluster.Spec.Resources.Memory 102 } 103 if !cluster.Spec.Storage.Size.IsZero() { 104 clusterCompSpec.VolumeClaimTemplates = []appsv1alpha1.ClusterComponentVolumeClaimTemplate{ 105 { 106 Name: dataVolumeName, 107 Spec: appsv1alpha1.PersistentVolumeClaimSpec{ 108 AccessModes: []corev1.PersistentVolumeAccessMode{ 109 corev1.ReadWriteOnce, 110 }, 111 Resources: corev1.ResourceRequirements{ 112 Requests: corev1.ResourceList{ 113 "storage": cluster.Spec.Storage.Size, 114 }, 115 }, 116 }, 117 }, 118 } 119 } 120 if cluster.Spec.Monitor.MonitoringInterval != nil { 121 if len(cluster.Spec.Monitor.MonitoringInterval.StrVal) == 0 && cluster.Spec.Monitor.MonitoringInterval.IntVal == 0 { 122 clusterCompSpec.Monitor = false 123 } else { 124 clusterCompSpec.Monitor = true 125 // TODO: should also set interval 126 } 127 } 128 if cluster.Spec.Network != nil { 129 clusterCompSpec.Services = []appsv1alpha1.ClusterComponentService{} 130 if cluster.Spec.Network.HostNetworkAccessible { 131 svc := appsv1alpha1.ClusterComponentService{ 132 Name: "vpc", 133 ServiceType: "LoadBalancer", 134 } 135 switch getCloudProvider() { 136 case CloudProviderAWS: 137 svc.Annotations = map[string]string{ 138 "service.beta.kubernetes.io/aws-load-balancer-type": "nlb", 139 "service.beta.kubernetes.io/aws-load-balancer-internal": "true", 140 } 141 case CloudProviderGCP: 142 svc.Annotations = map[string]string{ 143 "networking.gke.io/load-balancer-type": "Internal", 144 } 145 case CloudProviderAliyun: 146 svc.Annotations = map[string]string{ 147 "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-address-type": "intranet", 148 } 149 case CloudProviderAzure: 150 svc.Annotations = map[string]string{ 151 "service.beta.kubernetes.io/azure-load-balancer-internal": "true", 152 } 153 } 154 clusterCompSpec.Services = append(clusterCompSpec.Services, svc) 155 } 156 if cluster.Spec.Network.PubliclyAccessible { 157 svc := appsv1alpha1.ClusterComponentService{ 158 Name: "public", 159 ServiceType: "LoadBalancer", 160 } 161 switch getCloudProvider() { 162 case CloudProviderAWS: 163 svc.Annotations = map[string]string{ 164 "service.beta.kubernetes.io/aws-load-balancer-type": "nlb", 165 "service.beta.kubernetes.io/aws-load-balancer-internal": "false", 166 } 167 case CloudProviderAliyun: 168 svc.Annotations = map[string]string{ 169 "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-address-type": "internet", 170 } 171 case CloudProviderAzure: 172 svc.Annotations = map[string]string{ 173 "service.beta.kubernetes.io/azure-load-balancer-internal": "false", 174 } 175 } 176 clusterCompSpec.Services = append(clusterCompSpec.Services, svc) 177 } 178 } 179 } 180 181 // priority: cluster.spec.componentSpecs > simplified api (e.g. cluster.spec.storage etc.) > cluster template 182 if clusterCompSpec == nil { 183 fillSimplifiedAPI() 184 } 185 if clusterCompSpec == nil { 186 return nil, nil 187 } 188 189 var err error 190 // make a copy of clusterCompDef 191 clusterCompDefObj := clusterCompDef.DeepCopy() 192 component := &SynthesizedComponent{ 193 ClusterDefName: clusterDef.Name, 194 ClusterName: cluster.Name, 195 ClusterUID: string(cluster.UID), 196 Name: clusterCompSpec.Name, 197 CompDefName: clusterCompDefObj.Name, 198 CharacterType: clusterCompDefObj.CharacterType, 199 WorkloadType: clusterCompDefObj.WorkloadType, 200 StatelessSpec: clusterCompDefObj.StatelessSpec, 201 StatefulSpec: clusterCompDefObj.StatefulSpec, 202 ConsensusSpec: clusterCompDefObj.ConsensusSpec, 203 ReplicationSpec: clusterCompDefObj.ReplicationSpec, 204 RSMSpec: clusterCompDefObj.RSMSpec, 205 PodSpec: clusterCompDefObj.PodSpec, 206 Probes: clusterCompDefObj.Probes, 207 LogConfigs: clusterCompDefObj.LogConfigs, 208 HorizontalScalePolicy: clusterCompDefObj.HorizontalScalePolicy, 209 ConfigTemplates: clusterCompDefObj.ConfigSpecs, 210 ScriptTemplates: clusterCompDefObj.ScriptSpecs, 211 VolumeTypes: clusterCompDefObj.VolumeTypes, 212 VolumeProtection: clusterCompDefObj.VolumeProtectionSpec, 213 CustomLabelSpecs: clusterCompDefObj.CustomLabelSpecs, 214 SwitchoverSpec: clusterCompDefObj.SwitchoverSpec, 215 StatefulSetWorkload: clusterCompDefObj.GetStatefulSetWorkload(), 216 MinAvailable: clusterCompSpec.GetMinAvailable(clusterCompDefObj.GetMinAvailable()), 217 Replicas: clusterCompSpec.Replicas, 218 EnabledLogs: clusterCompSpec.EnabledLogs, 219 TLS: clusterCompSpec.TLS, 220 Issuer: clusterCompSpec.Issuer, 221 ComponentDef: clusterCompSpec.ComponentDefRef, 222 ServiceAccountName: clusterCompSpec.ServiceAccountName, 223 } 224 225 if len(clusterCompVers) > 0 && clusterCompVers[0] != nil { 226 // only accept 1st ClusterVersion override context 227 clusterCompVer := clusterCompVers[0] 228 component.ConfigTemplates = cfgcore.MergeConfigTemplates(clusterCompVer.ConfigSpecs, component.ConfigTemplates) 229 // override component.PodSpec.InitContainers and component.PodSpec.Containers 230 for _, c := range clusterCompVer.VersionsCtx.InitContainers { 231 component.PodSpec.InitContainers = appendOrOverrideContainerAttr(component.PodSpec.InitContainers, c) 232 } 233 for _, c := range clusterCompVer.VersionsCtx.Containers { 234 component.PodSpec.Containers = appendOrOverrideContainerAttr(component.PodSpec.Containers, c) 235 } 236 // override component.SwitchoverSpec 237 overrideSwitchoverSpecAttr(component.SwitchoverSpec, clusterCompVer.SwitchoverSpec) 238 } 239 240 // handle component.PodSpec extra settings 241 // set affinity and tolerations 242 affinity := BuildAffinity(cluster, clusterCompSpec) 243 if component.PodSpec.Affinity, err = BuildPodAffinity(cluster, affinity, component); err != nil { 244 reqCtx.Log.Error(err, "build pod affinity failed.") 245 return nil, err 246 } 247 component.PodSpec.TopologySpreadConstraints = BuildPodTopologySpreadConstraints(cluster, affinity, component) 248 if component.PodSpec.Tolerations, err = BuildTolerations(cluster, clusterCompSpec); err != nil { 249 reqCtx.Log.Error(err, "build pod tolerations failed.") 250 return nil, err 251 } 252 253 if clusterCompSpec.VolumeClaimTemplates != nil { 254 component.VolumeClaimTemplates = clusterCompSpec.ToVolumeClaimTemplates() 255 } 256 257 if clusterCompSpec.Resources.Requests != nil || clusterCompSpec.Resources.Limits != nil { 258 component.PodSpec.Containers[0].Resources = clusterCompSpec.Resources 259 } 260 if err = updateResources(cluster, component, *clusterCompSpec, clsMgr); err != nil { 261 reqCtx.Log.Error(err, "update class resources failed") 262 return nil, err 263 } 264 265 if clusterCompDefObj.Service != nil { 266 service := corev1.Service{Spec: clusterCompDefObj.Service.ToSVCSpec()} 267 service.Spec.Type = corev1.ServiceTypeClusterIP 268 component.Services = append(component.Services, service) 269 for _, item := range clusterCompSpec.Services { 270 service = corev1.Service{ 271 ObjectMeta: metav1.ObjectMeta{ 272 Name: item.Name, 273 Annotations: item.Annotations, 274 }, 275 Spec: service.Spec, 276 } 277 service.Spec.Type = item.ServiceType 278 component.Services = append(component.Services, service) 279 } 280 } 281 282 buildMonitorConfig(clusterCompDefObj, clusterCompSpec, component) 283 284 // lorry container requires a service account with adequate privileges. 285 // If lorry required and the serviceAccountName is not set, 286 // a default serviceAccountName will be assigned. 287 if component.ServiceAccountName == "" && component.Probes != nil { 288 component.ServiceAccountName = "kb-" + component.ClusterName 289 } 290 // set component.PodSpec.ServiceAccountName 291 component.PodSpec.ServiceAccountName = component.ServiceAccountName 292 if err = buildLorryContainers(reqCtx, component); err != nil { 293 reqCtx.Log.Error(err, "build probe container failed.") 294 return nil, err 295 } 296 297 replaceContainerPlaceholderTokens(component, GetEnvReplacementMapForConnCredential(cluster.GetName())) 298 299 if err = buildComponentRef(clusterDef, cluster, clusterCompDefObj, clusterCompSpec, component); err != nil { 300 reqCtx.Log.Error(err, "failed to merge componentRef") 301 return nil, err 302 } 303 304 if serviceReferences != nil { 305 component.ServiceReferences = serviceReferences 306 } 307 308 return component, nil 309 } 310 311 // appendOrOverrideContainerAttr appends targetContainer to compContainers or overrides the attributes of compContainers with a given targetContainer, 312 // if targetContainer does not exist in compContainers, it will be appended. otherwise it will be updated with the attributes of the target container. 313 func appendOrOverrideContainerAttr(compContainers []corev1.Container, targetContainer corev1.Container) []corev1.Container { 314 index, compContainer := intctrlutil.GetContainerByName(compContainers, targetContainer.Name) 315 if compContainer == nil { 316 compContainers = append(compContainers, targetContainer) 317 } else { 318 doContainerAttrOverride(&compContainers[index], targetContainer) 319 } 320 return compContainers 321 } 322 323 // doContainerAttrOverride overrides the attributes in compContainer with the attributes in container. 324 func doContainerAttrOverride(compContainer *corev1.Container, container corev1.Container) { 325 if compContainer == nil { 326 return 327 } 328 if container.Image != "" { 329 compContainer.Image = container.Image 330 } 331 if len(container.Command) != 0 { 332 compContainer.Command = container.Command 333 } 334 if len(container.Args) != 0 { 335 compContainer.Args = container.Args 336 } 337 if container.WorkingDir != "" { 338 compContainer.WorkingDir = container.WorkingDir 339 } 340 if len(container.Ports) != 0 { 341 compContainer.Ports = container.Ports 342 } 343 if len(container.EnvFrom) != 0 { 344 compContainer.EnvFrom = container.EnvFrom 345 } 346 if len(container.Env) != 0 { 347 compContainer.Env = container.Env 348 } 349 if container.Resources.Limits != nil || container.Resources.Requests != nil { 350 compContainer.Resources = container.Resources 351 } 352 if len(container.VolumeMounts) != 0 { 353 compContainer.VolumeMounts = container.VolumeMounts 354 } 355 if len(container.VolumeDevices) != 0 { 356 compContainer.VolumeDevices = container.VolumeDevices 357 } 358 if container.LivenessProbe != nil { 359 compContainer.LivenessProbe = container.LivenessProbe 360 } 361 if container.ReadinessProbe != nil { 362 compContainer.ReadinessProbe = container.ReadinessProbe 363 } 364 if container.StartupProbe != nil { 365 compContainer.StartupProbe = container.StartupProbe 366 } 367 if container.Lifecycle != nil { 368 compContainer.Lifecycle = container.Lifecycle 369 } 370 if container.TerminationMessagePath != "" { 371 compContainer.TerminationMessagePath = container.TerminationMessagePath 372 } 373 if container.TerminationMessagePolicy != "" { 374 compContainer.TerminationMessagePolicy = container.TerminationMessagePolicy 375 } 376 if container.ImagePullPolicy != "" { 377 compContainer.ImagePullPolicy = container.ImagePullPolicy 378 } 379 if container.SecurityContext != nil { 380 compContainer.SecurityContext = container.SecurityContext 381 } 382 } 383 384 // GetEnvReplacementMapForConnCredential gets the replacement map for connect credential 385 func GetEnvReplacementMapForConnCredential(clusterName string) map[string]string { 386 return map[string]string{ 387 constant.KBConnCredentialPlaceHolder: GenerateConnCredential(clusterName), 388 } 389 } 390 391 func replaceContainerPlaceholderTokens(component *SynthesizedComponent, namedValuesMap map[string]string) { 392 // replace env[].valueFrom.secretKeyRef.name variables 393 for _, cc := range [][]corev1.Container{component.PodSpec.InitContainers, component.PodSpec.Containers} { 394 for _, c := range cc { 395 c.Env = ReplaceSecretEnvVars(namedValuesMap, c.Env) 396 } 397 } 398 } 399 400 // GetReplacementMapForBuiltInEnv gets the replacement map for KubeBlocks built-in environment variables. 401 func GetReplacementMapForBuiltInEnv(clusterName, clusterUID, componentName string) map[string]string { 402 cc := fmt.Sprintf("%s-%s", clusterName, componentName) 403 replacementMap := map[string]string{ 404 constant.KBClusterNamePlaceHolder: clusterName, 405 constant.KBCompNamePlaceHolder: componentName, 406 constant.KBClusterCompNamePlaceHolder: cc, 407 constant.KBComponentEnvCMPlaceHolder: fmt.Sprintf("%s-env", cc), 408 } 409 if len(clusterUID) > 8 { 410 replacementMap[constant.KBClusterUIDPostfix8PlaceHolder] = clusterUID[len(clusterUID)-8:] 411 } else { 412 replacementMap[constant.KBClusterUIDPostfix8PlaceHolder] = clusterUID 413 } 414 return replacementMap 415 } 416 417 // ReplaceNamedVars replaces the placeholder in targetVar if it is match and returns the replaced result 418 func ReplaceNamedVars(namedValuesMap map[string]string, targetVar string, limits int, matchAll bool) string { 419 for placeHolderKey, mappingValue := range namedValuesMap { 420 r := strings.Replace(targetVar, placeHolderKey, mappingValue, limits) 421 // early termination on matching, when matchAll = false 422 if r != targetVar && !matchAll { 423 return r 424 } 425 targetVar = r 426 } 427 return targetVar 428 } 429 430 // ReplaceSecretEnvVars replaces the env secret value with namedValues and returns new envs 431 func ReplaceSecretEnvVars(namedValuesMap map[string]string, envs []corev1.EnvVar) []corev1.EnvVar { 432 newEnvs := make([]corev1.EnvVar, 0, len(envs)) 433 for _, e := range envs { 434 if e.ValueFrom == nil || e.ValueFrom.SecretKeyRef == nil { 435 continue 436 } 437 name := ReplaceNamedVars(namedValuesMap, e.ValueFrom.SecretKeyRef.Name, 1, false) 438 if name != e.ValueFrom.SecretKeyRef.Name { 439 e.ValueFrom.SecretKeyRef.Name = name 440 } 441 newEnvs = append(newEnvs, e) 442 } 443 return newEnvs 444 } 445 446 func GenerateConnCredential(clusterName string) string { 447 return fmt.Sprintf("%s-conn-credential", clusterName) 448 } 449 450 func GenerateDefaultServiceDescriptorName(clusterName string) string { 451 return fmt.Sprintf("kbsd-%s", GenerateConnCredential(clusterName)) 452 } 453 454 // overrideSwitchoverSpecAttr overrides the attributes in switchoverSpec with the attributes of SwitchoverShortSpec in clusterVersion. 455 func overrideSwitchoverSpecAttr(switchoverSpec *appsv1alpha1.SwitchoverSpec, cvSwitchoverSpec *appsv1alpha1.SwitchoverShortSpec) { 456 if switchoverSpec == nil || cvSwitchoverSpec == nil || cvSwitchoverSpec.CmdExecutorConfig == nil { 457 return 458 } 459 applyCmdExecutorConfig := func(cmdExecutorConfig *appsv1alpha1.CmdExecutorConfig) { 460 if cmdExecutorConfig == nil { 461 return 462 } 463 if len(cvSwitchoverSpec.CmdExecutorConfig.Image) > 0 { 464 cmdExecutorConfig.Image = cvSwitchoverSpec.CmdExecutorConfig.Image 465 } 466 if len(cvSwitchoverSpec.CmdExecutorConfig.Env) > 0 { 467 cmdExecutorConfig.Env = cvSwitchoverSpec.CmdExecutorConfig.Env 468 } 469 } 470 if switchoverSpec.WithCandidate != nil { 471 applyCmdExecutorConfig(switchoverSpec.WithCandidate.CmdExecutorConfig) 472 } 473 if switchoverSpec.WithoutCandidate != nil { 474 applyCmdExecutorConfig(switchoverSpec.WithoutCandidate.CmdExecutorConfig) 475 } 476 } 477 478 func GenerateComponentEnvName(clusterName, componentName string) string { 479 return fmt.Sprintf("%s-%s-env", clusterName, componentName) 480 } 481 482 func updateResources(cluster *appsv1alpha1.Cluster, component *SynthesizedComponent, clusterCompSpec appsv1alpha1.ClusterComponentSpec, clsMgr *class.Manager) error { 483 if ignoreResourceConstraint(cluster) { 484 return nil 485 } 486 487 if clsMgr == nil { 488 return nil 489 } 490 491 expectResources, err := clsMgr.GetResources(cluster.Spec.ClusterDefRef, &clusterCompSpec) 492 if err != nil || expectResources == nil { 493 return err 494 } 495 496 actualResources := component.PodSpec.Containers[0].Resources 497 if actualResources.Requests == nil { 498 actualResources.Requests = corev1.ResourceList{} 499 } 500 if actualResources.Limits == nil { 501 actualResources.Limits = corev1.ResourceList{} 502 } 503 for k, v := range expectResources { 504 actualResources.Requests[k] = v 505 actualResources.Limits[k] = v 506 } 507 component.PodSpec.Containers[0].Resources = actualResources 508 return nil 509 } 510 511 func getCloudProvider() CloudProvider { 512 k8sVersion := viper.GetString(constant.CfgKeyServerInfo) 513 if strings.Contains(k8sVersion, "eks") { 514 return CloudProviderAWS 515 } 516 if strings.Contains(k8sVersion, "gke") { 517 return CloudProviderGCP 518 } 519 if strings.Contains(k8sVersion, "aliyun") { 520 return CloudProviderAliyun 521 } 522 if strings.Contains(k8sVersion, "tke") { 523 return CloudProviderTencent 524 } 525 return CloudProviderUnknown 526 } 527 528 func GetConfigSpecByName(component *SynthesizedComponent, configSpec string) *appsv1alpha1.ComponentConfigSpec { 529 for i := range component.ConfigTemplates { 530 template := &component.ConfigTemplates[i] 531 if template.Name == configSpec { 532 return template 533 } 534 } 535 return nil 536 }