github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/controllers/apps/components/component_workload_builder.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 components 21 22 import ( 23 "fmt" 24 25 "golang.org/x/exp/slices" 26 appsv1 "k8s.io/api/apps/v1" 27 corev1 "k8s.io/api/core/v1" 28 "sigs.k8s.io/controller-runtime/pkg/client" 29 30 appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1" 31 workloads "github.com/1aal/kubeblocks/apis/workloads/v1alpha1" 32 "github.com/1aal/kubeblocks/pkg/controller/component" 33 "github.com/1aal/kubeblocks/pkg/controller/factory" 34 "github.com/1aal/kubeblocks/pkg/controller/model" 35 "github.com/1aal/kubeblocks/pkg/controller/plan" 36 intctrlutil "github.com/1aal/kubeblocks/pkg/controllerutil" 37 ) 38 39 type componentWorkloadBuilder interface { 40 // runtime, config, script, env, volume, service, monitor, probe 41 BuildEnv() componentWorkloadBuilder 42 BuildConfig() componentWorkloadBuilder 43 BuildWorkload() componentWorkloadBuilder 44 BuildPDB() componentWorkloadBuilder 45 BuildVolumeMount() componentWorkloadBuilder 46 BuildTLSCert() componentWorkloadBuilder 47 BuildTLSVolume() componentWorkloadBuilder 48 BuildCustomVolumes() componentWorkloadBuilder 49 50 Complete() error 51 } 52 53 type rsmComponentWorkloadBuilder struct { 54 reqCtx intctrlutil.RequestCtx 55 client client.Client 56 comp *rsmComponent 57 defaultAction *model.Action 58 error error 59 envConfig *corev1.ConfigMap 60 workload client.Object 61 localObjs []client.Object // cache the objects needed for configuration, should remove this after refactoring the configuration 62 } 63 64 var _ componentWorkloadBuilder = &rsmComponentWorkloadBuilder{} 65 66 func (b *rsmComponentWorkloadBuilder) BuildEnv() componentWorkloadBuilder { 67 buildfn := func() ([]client.Object, error) { 68 envCfg := factory.BuildEnvConfig(b.comp.GetCluster(), b.comp.GetSynthesizedComponent()) 69 b.envConfig = envCfg 70 b.localObjs = append(b.localObjs, envCfg) 71 return []client.Object{envCfg}, nil 72 } 73 return b.BuildWrapper(buildfn) 74 } 75 76 func (b *rsmComponentWorkloadBuilder) BuildCustomVolumes() componentWorkloadBuilder { 77 return b.BuildWrapper(func() ([]client.Object, error) { 78 return nil, doBuildCustomVolumes(b.getRuntime(), b.comp.GetCluster(), b.comp.GetName(), b.comp.GetNamespace()) 79 }) 80 } 81 82 func (b *rsmComponentWorkloadBuilder) BuildConfig() componentWorkloadBuilder { 83 buildfn := func() ([]client.Object, error) { 84 if b.workload == nil { 85 return nil, fmt.Errorf("build config but workload is nil, cluster: %s, component: %s", 86 b.comp.GetClusterName(), b.comp.GetName()) 87 } 88 89 err := plan.RenderConfigNScriptFiles( 90 &intctrlutil.ResourceCtx{ 91 Context: b.reqCtx.Ctx, 92 Client: b.client, 93 Namespace: b.comp.GetNamespace(), 94 ClusterName: b.comp.GetClusterName(), 95 ComponentName: b.comp.GetName(), 96 }, 97 b.comp.GetClusterVersion(), 98 b.comp.GetCluster(), 99 b.comp.GetSynthesizedComponent(), 100 b.workload, 101 b.getRuntime(), 102 b.localObjs) 103 return nil, err 104 } 105 return b.BuildWrapper(buildfn) 106 } 107 108 func (b *rsmComponentWorkloadBuilder) BuildWorkload() componentWorkloadBuilder { 109 buildfn := func() ([]client.Object, error) { 110 component := b.comp.GetSynthesizedComponent() 111 obj, err := factory.BuildRSM(b.reqCtx, b.comp.GetCluster(), component, b.envConfig.Name) 112 if err != nil { 113 return nil, err 114 } 115 116 b.workload = obj 117 118 return nil, nil // don't return sts here 119 } 120 return b.BuildWrapper(buildfn) 121 } 122 123 func (b *rsmComponentWorkloadBuilder) BuildPDB() componentWorkloadBuilder { 124 buildfn := func() ([]client.Object, error) { 125 // if without this handler, the cluster controller will occur error during reconciling. 126 // conditionally build PodDisruptionBudget 127 synthesizedComponent := b.comp.GetSynthesizedComponent() 128 if synthesizedComponent.MinAvailable != nil { 129 pdb := factory.BuildPDB(b.comp.GetCluster(), synthesizedComponent) 130 return []client.Object{pdb}, nil 131 } else { 132 panic("this shouldn't happen") 133 } 134 } 135 return b.BuildWrapper(buildfn) 136 } 137 138 func (b *rsmComponentWorkloadBuilder) BuildVolumeMount() componentWorkloadBuilder { 139 buildfn := func() ([]client.Object, error) { 140 if b.workload == nil { 141 return nil, fmt.Errorf("build volume mount but workload is nil, cluster: %s, component: %s", 142 b.comp.GetClusterName(), b.comp.GetName()) 143 } 144 145 podSpec := b.getRuntime() 146 for _, cc := range []*[]corev1.Container{&podSpec.Containers, &podSpec.InitContainers} { 147 volumes := podSpec.Volumes 148 for _, c := range *cc { 149 for _, v := range c.VolumeMounts { 150 // if persistence is not found, add emptyDir pod.spec.volumes[] 151 createfn := func(_ string) corev1.Volume { 152 return corev1.Volume{ 153 Name: v.Name, 154 VolumeSource: corev1.VolumeSource{ 155 EmptyDir: &corev1.EmptyDirVolumeSource{}, 156 }, 157 } 158 } 159 volumes, _ = intctrlutil.CreateOrUpdateVolume(volumes, v.Name, createfn, nil) 160 } 161 } 162 podSpec.Volumes = volumes 163 } 164 return nil, nil 165 } 166 return b.BuildWrapper(buildfn) 167 } 168 169 func (b *rsmComponentWorkloadBuilder) BuildTLSCert() componentWorkloadBuilder { 170 buildfn := func() ([]client.Object, error) { 171 cluster := b.comp.GetCluster() 172 component := b.comp.GetSynthesizedComponent() 173 if !component.TLS { 174 return nil, nil 175 } 176 if component.Issuer == nil { 177 return nil, fmt.Errorf("issuer shouldn't be nil when tls enabled") 178 } 179 180 objs := make([]client.Object, 0) 181 switch component.Issuer.Name { 182 case appsv1alpha1.IssuerUserProvided: 183 if err := plan.CheckTLSSecretRef(b.reqCtx.Ctx, b.client, cluster.Namespace, component.Issuer.SecretRef); err != nil { 184 return nil, err 185 } 186 case appsv1alpha1.IssuerKubeBlocks: 187 secret, err := plan.ComposeTLSSecret(cluster.Namespace, cluster.Name, component.Name) 188 if err != nil { 189 return nil, err 190 } 191 objs = append(objs, secret) 192 b.localObjs = append(b.localObjs, secret) 193 } 194 return objs, nil 195 } 196 return b.BuildWrapper(buildfn) 197 } 198 199 func (b *rsmComponentWorkloadBuilder) BuildTLSVolume() componentWorkloadBuilder { 200 buildfn := func() ([]client.Object, error) { 201 if b.workload == nil { 202 return nil, fmt.Errorf("build TLS volumes but workload is nil, cluster: %s, component: %s", 203 b.comp.GetClusterName(), b.comp.GetName()) 204 } 205 // build secret volume and volume mount 206 return nil, updateTLSVolumeAndVolumeMount(b.getRuntime(), b.comp.GetClusterName(), *b.comp.GetSynthesizedComponent()) 207 } 208 return b.BuildWrapper(buildfn) 209 } 210 211 func (b *rsmComponentWorkloadBuilder) Complete() error { 212 if b.error != nil { 213 return b.error 214 } 215 if b.workload == nil { 216 return fmt.Errorf("fail to create component workloads, cluster: %s, component: %s", 217 b.comp.GetClusterName(), b.comp.GetName()) 218 } 219 b.comp.setWorkload(b.workload, b.defaultAction) 220 return nil 221 } 222 223 func (b *rsmComponentWorkloadBuilder) BuildWrapper(buildfn func() ([]client.Object, error)) componentWorkloadBuilder { 224 if b.error != nil || buildfn == nil { 225 return b 226 } 227 objs, err := buildfn() 228 if err != nil { 229 b.error = err 230 } else { 231 cluster := b.comp.GetCluster() 232 component := b.comp.GetSynthesizedComponent() 233 if err = updateCustomLabelToObjs(cluster.Name, string(cluster.UID), component.Name, component.CustomLabelSpecs, objs); err != nil { 234 b.error = err 235 } 236 for _, obj := range objs { 237 b.comp.addResource(obj, b.defaultAction) 238 } 239 } 240 return b 241 } 242 243 func (b *rsmComponentWorkloadBuilder) getRuntime() *corev1.PodSpec { 244 switch w := b.workload.(type) { 245 case *appsv1.StatefulSet: 246 return &w.Spec.Template.Spec 247 case *appsv1.Deployment: 248 return &w.Spec.Template.Spec 249 case *workloads.ReplicatedStateMachine: 250 return &w.Spec.Template.Spec 251 default: 252 return nil 253 } 254 } 255 256 func updateTLSVolumeAndVolumeMount(podSpec *corev1.PodSpec, clusterName string, component component.SynthesizedComponent) error { 257 if !component.TLS { 258 return nil 259 } 260 261 // update volume 262 volumes := podSpec.Volumes 263 volume, err := composeTLSVolume(clusterName, component) 264 if err != nil { 265 return err 266 } 267 volumes = append(volumes, *volume) 268 podSpec.Volumes = volumes 269 270 // update volumeMount 271 for index, container := range podSpec.Containers { 272 volumeMounts := container.VolumeMounts 273 volumeMount := composeTLSVolumeMount() 274 volumeMounts = append(volumeMounts, volumeMount) 275 podSpec.Containers[index].VolumeMounts = volumeMounts 276 } 277 278 return nil 279 } 280 281 func composeTLSVolume(clusterName string, component component.SynthesizedComponent) (*corev1.Volume, error) { 282 if !component.TLS { 283 return nil, fmt.Errorf("can't compose TLS volume when TLS not enabled") 284 } 285 if component.Issuer == nil { 286 return nil, fmt.Errorf("issuer shouldn't be nil when TLS enabled") 287 } 288 if component.Issuer.Name == appsv1alpha1.IssuerUserProvided && component.Issuer.SecretRef == nil { 289 return nil, fmt.Errorf("secret ref shouldn't be nil when issuer is UserProvided") 290 } 291 292 var secretName, ca, cert, key string 293 switch component.Issuer.Name { 294 case appsv1alpha1.IssuerKubeBlocks: 295 secretName = plan.GenerateTLSSecretName(clusterName, component.Name) 296 ca = factory.CAName 297 cert = factory.CertName 298 key = factory.KeyName 299 case appsv1alpha1.IssuerUserProvided: 300 secretName = component.Issuer.SecretRef.Name 301 ca = component.Issuer.SecretRef.CA 302 cert = component.Issuer.SecretRef.Cert 303 key = component.Issuer.SecretRef.Key 304 } 305 volume := corev1.Volume{ 306 Name: factory.VolumeName, 307 VolumeSource: corev1.VolumeSource{ 308 Secret: &corev1.SecretVolumeSource{ 309 SecretName: secretName, 310 Items: []corev1.KeyToPath{ 311 {Key: ca, Path: factory.CAName}, 312 {Key: cert, Path: factory.CertName}, 313 {Key: key, Path: factory.KeyName}, 314 }, 315 Optional: func() *bool { o := false; return &o }(), 316 }, 317 }, 318 } 319 320 return &volume, nil 321 } 322 323 func composeTLSVolumeMount() corev1.VolumeMount { 324 return corev1.VolumeMount{ 325 Name: factory.VolumeName, 326 MountPath: factory.MountPath, 327 ReadOnly: true, 328 } 329 } 330 331 func newSourceFromResource(name string, source any) corev1.Volume { 332 volume := corev1.Volume{ 333 Name: name, 334 } 335 switch t := source.(type) { 336 default: 337 panic(fmt.Sprintf("unknown volume source type: %T", t)) 338 case *corev1.ConfigMapVolumeSource: 339 volume.VolumeSource.ConfigMap = t 340 case *corev1.SecretVolumeSource: 341 volume.VolumeSource.Secret = t 342 } 343 return volume 344 } 345 346 func doBuildCustomVolumes(podSpec *corev1.PodSpec, cluster *appsv1alpha1.Cluster, componentName string, namespace string) error { 347 comp := cluster.Spec.GetComponentByName(componentName) 348 if comp == nil || comp.UserResourceRefs == nil { 349 return nil 350 } 351 352 volumes := podSpec.Volumes 353 for _, configMap := range comp.UserResourceRefs.ConfigMapRefs { 354 volumes = append(volumes, newSourceFromResource(configMap.Name, configMap.ConfigMap.DeepCopy())) 355 } 356 for _, secret := range comp.UserResourceRefs.SecretRefs { 357 volumes = append(volumes, newSourceFromResource(secret.Name, secret.Secret.DeepCopy())) 358 } 359 podSpec.Volumes = volumes 360 buildVolumeMountForContainers(podSpec, *comp.UserResourceRefs) 361 return nil 362 } 363 364 func buildVolumeMountForContainers(podSpec *corev1.PodSpec, resourceRefs appsv1alpha1.UserResourceRefs) { 365 for _, configMap := range resourceRefs.ConfigMapRefs { 366 newVolumeMount(podSpec, configMap.ResourceMeta) 367 } 368 for _, secret := range resourceRefs.SecretRefs { 369 newVolumeMount(podSpec, secret.ResourceMeta) 370 } 371 } 372 373 func newVolumeMount(podSpec *corev1.PodSpec, res appsv1alpha1.ResourceMeta) { 374 for i := range podSpec.Containers { 375 container := &podSpec.Containers[i] 376 if slices.Contains(res.AsVolumeFrom, container.Name) { 377 container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ 378 Name: res.Name, 379 MountPath: res.MountPoint, 380 SubPath: res.SubPath, 381 }) 382 } 383 } 384 }