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  }