github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/configuration/config_manager/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 configmanager
    21  
    22  import (
    23  	"context"
    24  	"fmt"
    25  	"path/filepath"
    26  
    27  	"gopkg.in/yaml.v2"
    28  	corev1 "k8s.io/api/core/v1"
    29  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    32  	apiruntime "k8s.io/apimachinery/pkg/runtime"
    33  	"sigs.k8s.io/controller-runtime/pkg/client"
    34  	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    35  
    36  	appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    37  	"github.com/1aal/kubeblocks/pkg/configuration/core"
    38  	cfgutil "github.com/1aal/kubeblocks/pkg/configuration/util"
    39  	"github.com/1aal/kubeblocks/pkg/constant"
    40  	viper "github.com/1aal/kubeblocks/pkg/viperx"
    41  )
    42  
    43  const (
    44  	configTemplateName = "reload.yaml"
    45  	scriptVolumePrefix = "cm-script-"
    46  	configVolumePrefix = "cm-config-"
    47  
    48  	scriptConfigField    = "scripts"
    49  	formatterConfigField = "formatterConfig"
    50  
    51  	configManagerConfigVolumeName = "config-manager-config"
    52  	configManagerConfig           = "config-manager.yaml"
    53  	configManagerConfigMountPoint = "/opt/config-manager"
    54  	configManagerCMPrefix         = "sidecar-"
    55  )
    56  
    57  const (
    58  	KBScriptVolumePath = "/opt/kb-tools/reload"
    59  	KBConfigVolumePath = "/opt/kb-tools/config"
    60  
    61  	KBTOOLSScriptsPathEnv  = "TOOLS_SCRIPTS_PATH"
    62  	KBConfigManagerPathEnv = "TOOLS_PATH"
    63  )
    64  
    65  const KBConfigSpecLazyRenderedYamlFile = "lazy-rendered-config.yaml"
    66  
    67  func BuildConfigManagerContainerParams(cli client.Client, ctx context.Context, managerParams *CfgManagerBuildParams, volumeDirs []corev1.VolumeMount) error {
    68  	var volume *corev1.VolumeMount
    69  	var buildParam *ConfigSpecMeta
    70  
    71  	allVolumeMounts := getWatchedVolume(volumeDirs, managerParams.ConfigSpecsBuildParams)
    72  	for i := range managerParams.ConfigSpecsBuildParams {
    73  		buildParam = &managerParams.ConfigSpecsBuildParams[i]
    74  		volume = FindVolumeMount(managerParams.Volumes, buildParam.ConfigSpec.VolumeName)
    75  		if volume == nil {
    76  			logger.Info(fmt.Sprintf("volume mount not be use : %s", buildParam.ConfigSpec.VolumeName))
    77  			continue
    78  		}
    79  		buildParam.MountPoint = volume.MountPath
    80  		if err := buildConfigSpecHandleMeta(cli, ctx, buildParam, managerParams); err != nil {
    81  			return err
    82  		}
    83  		if err := buildLazyRenderedConfig(cli, ctx, buildParam, managerParams); err != nil {
    84  			return err
    85  		}
    86  	}
    87  	downwardAPIVolumes := buildDownwardAPIVolumes(managerParams)
    88  	allVolumeMounts = append(allVolumeMounts, downwardAPIVolumes...)
    89  	managerParams.Volumes = append(managerParams.Volumes, downwardAPIVolumes...)
    90  	return buildConfigManagerArgs(managerParams, allVolumeMounts, cli, ctx)
    91  }
    92  
    93  func getWatchedVolume(volumeDirs []corev1.VolumeMount, buildParams []ConfigSpecMeta) []corev1.VolumeMount {
    94  	enableWatchVolume := func(volume corev1.VolumeMount) bool {
    95  		for _, param := range buildParams {
    96  			if param.ConfigSpec.VolumeName != volume.Name {
    97  				continue
    98  			}
    99  			switch param.ReloadType {
   100  			case appsv1alpha1.TPLScriptType:
   101  				return core.IsWatchModuleForTplTrigger(param.ReloadOptions.TPLScriptTrigger)
   102  			case appsv1alpha1.ShellType:
   103  				return core.IsWatchModuleForShellTrigger(param.ReloadOptions.ShellTrigger)
   104  			default:
   105  				return true
   106  			}
   107  		}
   108  		return false
   109  	}
   110  
   111  	allVolumeMounts := make([]corev1.VolumeMount, 0, len(volumeDirs))
   112  	for _, volume := range volumeDirs {
   113  		if enableWatchVolume(volume) {
   114  			allVolumeMounts = append(allVolumeMounts, volume)
   115  		}
   116  	}
   117  	return allVolumeMounts
   118  }
   119  
   120  // buildLazyRenderedConfig prepare secondary render config and volume
   121  func buildLazyRenderedConfig(cli client.Client, ctx context.Context, param *ConfigSpecMeta, manager *CfgManagerBuildParams) error {
   122  	processYamlConfig := func(cm *corev1.ConfigMap) error {
   123  		renderMeta := ConfigLazyRenderedMeta{
   124  			ComponentConfigSpec: &param.ConfigSpec,
   125  			Templates:           cfgutil.ToSet(cm.Data).AsSlice(),
   126  			FormatterConfig:     param.FormatterConfig,
   127  		}
   128  		b, err := cfgutil.ToYamlConfig(renderMeta)
   129  		if err != nil {
   130  			return err
   131  		}
   132  		cm.Data[KBConfigSpecLazyRenderedYamlFile] = string(b)
   133  		return nil
   134  	}
   135  
   136  	secondaryTemplate := param.ConfigSpec.LegacyRenderedConfigSpec
   137  	if secondaryTemplate == nil {
   138  		return nil
   139  	}
   140  	referenceCMKey := client.ObjectKey{
   141  		Namespace: secondaryTemplate.Namespace,
   142  		Name:      secondaryTemplate.TemplateRef,
   143  	}
   144  	configCMKey := client.ObjectKey{
   145  		Namespace: manager.Cluster.GetNamespace(),
   146  		Name:      fmt.Sprintf("%s%s-%s", configManagerCMPrefix, secondaryTemplate.TemplateRef, manager.Cluster.GetName()),
   147  	}
   148  	if err := checkOrCreateConfigMap(referenceCMKey, configCMKey, cli, ctx, manager.Cluster, processYamlConfig); err != nil {
   149  		return err
   150  	}
   151  	buildLazyRenderedConfigVolume(configCMKey.Name, manager, GetConfigMountPoint(param.ConfigSpec), GetConfigVolumeName(param.ConfigSpec), param.ConfigSpec)
   152  	return nil
   153  }
   154  
   155  func buildDownwardAPIVolumes(params *CfgManagerBuildParams) []corev1.VolumeMount {
   156  	for _, buildParam := range params.ConfigSpecsBuildParams {
   157  		for _, info := range buildParam.DownwardAPIOptions {
   158  			if FindVolumeMount(params.DownwardAPIVolumes, info.Name) == nil {
   159  				buildDownwardAPIVolume(params, info)
   160  			}
   161  		}
   162  	}
   163  	return params.DownwardAPIVolumes
   164  }
   165  
   166  func buildConfigManagerArgs(params *CfgManagerBuildParams, volumeDirs []corev1.VolumeMount, cli client.Client, ctx context.Context) error {
   167  	args := buildConfigManagerCommonArgs(volumeDirs)
   168  	args = append(args, "--operator-update-enable")
   169  	args = append(args, "--tcp", viper.GetString(constant.ConfigManagerGPRCPortEnv))
   170  
   171  	if err := createOrUpdateConfigMap(fromConfigSpecMeta(params.ConfigSpecsBuildParams), params, cli, ctx); err != nil {
   172  		return err
   173  	}
   174  	args = append(args, "--config", filepath.Join(configManagerConfigMountPoint, configManagerConfig))
   175  	params.Args = args
   176  	return nil
   177  }
   178  
   179  func buildCMForConfig(manager *CfgManagerBuildParams, cmKey client.ObjectKey, config string) *corev1.ConfigMap {
   180  	return &corev1.ConfigMap{
   181  		ObjectMeta: metav1.ObjectMeta{
   182  			Name:      cmKey.Name,
   183  			Namespace: cmKey.Namespace,
   184  			Labels: map[string]string{
   185  				constant.AppInstanceLabelKey:    manager.Cluster.Name,
   186  				constant.AppManagedByLabelKey:   constant.AppName,
   187  				constant.KBAppComponentLabelKey: manager.ComponentName,
   188  			},
   189  		},
   190  		Data: map[string]string{
   191  			configManagerConfig: config,
   192  		},
   193  	}
   194  }
   195  
   196  func createOrUpdateConfigMap(configInfo []ConfigSpecInfo, manager *CfgManagerBuildParams, cli client.Client, ctx context.Context) error {
   197  	createConfigCM := func(configKey client.ObjectKey, config string) error {
   198  		scheme, err := appsv1alpha1.SchemeBuilder.Build()
   199  		if err != nil {
   200  			return err
   201  		}
   202  		cmObj := buildCMForConfig(manager, configKey, config)
   203  		if err := controllerutil.SetOwnerReference(manager.Cluster, cmObj, scheme); err != nil {
   204  			return err
   205  		}
   206  		return cli.Create(ctx, cmObj)
   207  	}
   208  	updateConfigCM := func(cm *corev1.ConfigMap, newConfig string) error {
   209  		patch := client.MergeFrom(cm.DeepCopy())
   210  		cm.Data[configManagerConfig] = newConfig
   211  		return cli.Patch(ctx, cm, patch)
   212  	}
   213  
   214  	config, err := cfgutil.ToYamlConfig(configInfo)
   215  	if err != nil {
   216  		return err
   217  	}
   218  	cmObj := &corev1.ConfigMap{}
   219  	cmKey := client.ObjectKey{
   220  		Namespace: manager.Cluster.GetNamespace(),
   221  		Name:      fmt.Sprintf("%s%s-%s-config-manager-config", configManagerCMPrefix, manager.Cluster.GetName(), manager.ComponentName),
   222  	}
   223  	err = cli.Get(ctx, cmKey, cmObj)
   224  	switch {
   225  	default:
   226  		return err
   227  	case err == nil:
   228  		err = updateConfigCM(cmObj, string(config))
   229  	case apierrors.IsNotFound(err):
   230  		err = createConfigCM(cmKey, string(config))
   231  	}
   232  	if err == nil {
   233  		buildReloadScriptVolume(cmKey.Name, manager, configManagerConfigMountPoint, configManagerConfigVolumeName)
   234  	}
   235  	return err
   236  }
   237  
   238  func fromConfigSpecMeta(metas []ConfigSpecMeta) []ConfigSpecInfo {
   239  	configSpecs := make([]ConfigSpecInfo, 0, len(metas))
   240  	for _, meta := range metas {
   241  		configSpecs = append(configSpecs, meta.ConfigSpecInfo)
   242  	}
   243  	return configSpecs
   244  }
   245  
   246  func FindVolumeMount(volumeDirs []corev1.VolumeMount, volumeName string) *corev1.VolumeMount {
   247  	for i := range volumeDirs {
   248  		if volumeDirs[i].Name == volumeName {
   249  			return &volumeDirs[i]
   250  		}
   251  	}
   252  	return nil
   253  }
   254  
   255  func buildConfigSpecHandleMeta(cli client.Client, ctx context.Context, buildParam *ConfigSpecMeta, cmBuildParam *CfgManagerBuildParams) error {
   256  	for _, script := range buildParam.ScriptConfig {
   257  		if err := buildCfgManagerScripts(script, cmBuildParam, cli, ctx, buildParam.ConfigSpec); err != nil {
   258  			return err
   259  		}
   260  	}
   261  	if buildParam.ReloadType == appsv1alpha1.TPLScriptType {
   262  		return buildTPLScriptCM(buildParam, cmBuildParam, cli, ctx)
   263  	}
   264  	return nil
   265  }
   266  
   267  func buildTPLScriptCM(configSpecBuildMeta *ConfigSpecMeta, manager *CfgManagerBuildParams, cli client.Client, ctx context.Context) error {
   268  	var (
   269  		options      = configSpecBuildMeta.TPLScriptTrigger
   270  		formatConfig = configSpecBuildMeta.FormatterConfig
   271  		mountPoint   = GetScriptsMountPoint(configSpecBuildMeta.ConfigSpec)
   272  	)
   273  
   274  	reloadYamlFn := func(cm *corev1.ConfigMap) error {
   275  		newData, err := checkAndUpdateReloadYaml(cm.Data, configTemplateName, formatConfig)
   276  		if err != nil {
   277  			return err
   278  		}
   279  		cm.Data = newData
   280  		return nil
   281  	}
   282  
   283  	referenceCMKey := client.ObjectKey{
   284  		Namespace: options.Namespace,
   285  		Name:      options.ScriptConfigMapRef,
   286  	}
   287  	scriptCMKey := client.ObjectKey{
   288  		Namespace: manager.Cluster.GetNamespace(),
   289  		Name:      fmt.Sprintf("%s%s-%s", configManagerCMPrefix, options.ScriptConfigMapRef, manager.Cluster.GetName()),
   290  	}
   291  	if err := checkOrCreateConfigMap(referenceCMKey, scriptCMKey, cli, ctx, manager.Cluster, reloadYamlFn); err != nil {
   292  		return err
   293  	}
   294  	buildReloadScriptVolume(scriptCMKey.Name, manager, mountPoint, GetScriptsVolumeName(configSpecBuildMeta.ConfigSpec))
   295  	configSpecBuildMeta.TPLConfig = filepath.Join(mountPoint, configTemplateName)
   296  	return nil
   297  }
   298  
   299  func buildDownwardAPIVolume(manager *CfgManagerBuildParams, fieldInfo appsv1alpha1.DownwardAPIOption) {
   300  	manager.DownwardAPIVolumes = append(manager.DownwardAPIVolumes, corev1.VolumeMount{
   301  		Name:      fieldInfo.Name,
   302  		MountPath: fieldInfo.MountPoint,
   303  	})
   304  	manager.CMConfigVolumes = append(manager.CMConfigVolumes, corev1.Volume{
   305  		Name: fieldInfo.Name,
   306  		VolumeSource: corev1.VolumeSource{
   307  			DownwardAPI: &corev1.DownwardAPIVolumeSource{
   308  				Items: fieldInfo.Items,
   309  			}},
   310  	})
   311  }
   312  
   313  func buildReloadScriptVolume(scriptCMName string, manager *CfgManagerBuildParams, mountPoint, volumeName string) {
   314  	var execMode int32 = 0755
   315  	manager.Volumes = append(manager.Volumes, corev1.VolumeMount{
   316  		Name:      volumeName,
   317  		MountPath: mountPoint,
   318  	})
   319  	manager.ScriptVolume = append(manager.ScriptVolume, corev1.Volume{
   320  		Name: volumeName,
   321  		VolumeSource: corev1.VolumeSource{
   322  			ConfigMap: &corev1.ConfigMapVolumeSource{
   323  				LocalObjectReference: corev1.LocalObjectReference{Name: scriptCMName},
   324  				DefaultMode:          &execMode,
   325  			},
   326  		},
   327  	})
   328  }
   329  
   330  func buildLazyRenderedConfigVolume(cmName string, manager *CfgManagerBuildParams, mountPoint, volumeName string, configSpec appsv1alpha1.ComponentConfigSpec) {
   331  	n := len(manager.Volumes)
   332  	manager.Volumes = append(manager.Volumes, corev1.VolumeMount{
   333  		Name:      volumeName,
   334  		MountPath: mountPoint,
   335  	})
   336  	manager.CMConfigVolumes = append(manager.CMConfigVolumes, corev1.Volume{
   337  		Name: volumeName,
   338  		VolumeSource: corev1.VolumeSource{
   339  			ConfigMap: &corev1.ConfigMapVolumeSource{
   340  				LocalObjectReference: corev1.LocalObjectReference{Name: cmName},
   341  			},
   342  		},
   343  	})
   344  	manager.ConfigLazyRenderedVolumes[configSpec.VolumeName] = manager.Volumes[n]
   345  }
   346  
   347  func checkOrCreateConfigMap(referenceCM client.ObjectKey, scriptCMKey client.ObjectKey, cli client.Client, ctx context.Context, cluster *appsv1alpha1.Cluster, fn func(cm *corev1.ConfigMap) error) error {
   348  	var (
   349  		err error
   350  
   351  		refCM     = corev1.ConfigMap{}
   352  		sidecarCM = corev1.ConfigMap{}
   353  	)
   354  
   355  	if err = cli.Get(ctx, referenceCM, &refCM); err != nil {
   356  		return err
   357  	}
   358  	if err = cli.Get(ctx, scriptCMKey, &sidecarCM); err != nil {
   359  		if !apierrors.IsNotFound(err) {
   360  			return err
   361  		}
   362  
   363  		scheme, _ := appsv1alpha1.SchemeBuilder.Build()
   364  		sidecarCM.Data = refCM.Data
   365  		if fn != nil {
   366  			if err := fn(&sidecarCM); err != nil {
   367  				return err
   368  			}
   369  		}
   370  		sidecarCM.SetLabels(refCM.GetLabels())
   371  		sidecarCM.SetName(scriptCMKey.Name)
   372  		sidecarCM.SetNamespace(scriptCMKey.Namespace)
   373  		sidecarCM.SetLabels(refCM.Labels)
   374  		if err := controllerutil.SetOwnerReference(cluster, &sidecarCM, scheme); err != nil {
   375  			return err
   376  		}
   377  		if err := cli.Create(ctx, &sidecarCM); err != nil {
   378  			return err
   379  		}
   380  	}
   381  	return nil
   382  }
   383  
   384  func checkAndUpdateReloadYaml(data map[string]string, reloadConfig string, formatterConfig appsv1alpha1.FormatterConfig) (map[string]string, error) {
   385  	configObject := make(map[string]interface{})
   386  	if content, ok := data[reloadConfig]; ok {
   387  		if err := yaml.Unmarshal([]byte(content), &configObject); err != nil {
   388  			return nil, err
   389  		}
   390  	}
   391  	if res, _, _ := unstructured.NestedFieldNoCopy(configObject, scriptConfigField); res == nil {
   392  		return nil, core.MakeError("reload.yaml required field: %s", scriptConfigField)
   393  	}
   394  
   395  	formatObject, err := apiruntime.DefaultUnstructuredConverter.ToUnstructured(&formatterConfig)
   396  	if err != nil {
   397  		return nil, err
   398  	}
   399  	if err := unstructured.SetNestedField(configObject, formatObject, formatterConfigField); err != nil {
   400  		return nil, err
   401  	}
   402  	b, err := yaml.Marshal(configObject)
   403  	if err != nil {
   404  		return nil, err
   405  	}
   406  	data[reloadConfig] = string(b)
   407  	return data, nil
   408  }
   409  
   410  func buildCfgManagerScripts(options appsv1alpha1.ScriptConfig, manager *CfgManagerBuildParams, cli client.Client, ctx context.Context, configSpec appsv1alpha1.ComponentConfigSpec) error {
   411  	mountPoint := filepath.Join(KBScriptVolumePath, configSpec.Name)
   412  	referenceCMKey := client.ObjectKey{
   413  		Namespace: options.Namespace,
   414  		Name:      options.ScriptConfigMapRef,
   415  	}
   416  	scriptsCMKey := client.ObjectKey{
   417  		Namespace: manager.Cluster.GetNamespace(),
   418  		Name:      fmt.Sprintf("%s%s-%s", configManagerCMPrefix, options.ScriptConfigMapRef, manager.Cluster.GetName()),
   419  	}
   420  	if err := checkOrCreateConfigMap(referenceCMKey, scriptsCMKey, cli, ctx, manager.Cluster, nil); err != nil {
   421  		return err
   422  	}
   423  	buildReloadScriptVolume(scriptsCMKey.Name, manager, mountPoint, GetScriptsVolumeName(configSpec))
   424  	return nil
   425  }
   426  
   427  func GetConfigMountPoint(configSpec appsv1alpha1.ComponentConfigSpec) string {
   428  	return filepath.Join(KBConfigVolumePath, configSpec.Name)
   429  }
   430  
   431  func GetScriptsMountPoint(configSpec appsv1alpha1.ComponentConfigSpec) string {
   432  	return filepath.Join(KBScriptVolumePath, configSpec.Name)
   433  }
   434  
   435  func GetScriptsVolumeName(configSpec appsv1alpha1.ComponentConfigSpec) string {
   436  	return fmt.Sprintf("%s%s", scriptVolumePrefix, configSpec.Name)
   437  }
   438  
   439  func GetConfigVolumeName(configSpec appsv1alpha1.ComponentConfigSpec) string {
   440  	return fmt.Sprintf("%s%s", configVolumePrefix, configSpec.Name)
   441  }
   442  
   443  func buildConfigManagerCommonArgs(volumeDirs []corev1.VolumeMount) []string {
   444  	args := make([]string, 0)
   445  	// set grpc port
   446  	// args = append(args, "--tcp", viper.GetString(cfgcore.ConfigManagerGPRCPortEnv))
   447  	args = append(args, "--log-level", viper.GetString(constant.ConfigManagerLogLevel))
   448  	for _, volume := range volumeDirs {
   449  		args = append(args, "--volume-dir", volume.MountPath)
   450  	}
   451  	return args
   452  }