github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/controller/configuration/template_wrapper.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 configuration
    21  
    22  import (
    23  	"context"
    24  	"encoding/json"
    25  	"reflect"
    26  	"strconv"
    27  	"strings"
    28  
    29  	corev1 "k8s.io/api/core/v1"
    30  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    31  	"k8s.io/apimachinery/pkg/runtime/schema"
    32  	"sigs.k8s.io/controller-runtime/pkg/client"
    33  
    34  	appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    35  	"github.com/1aal/kubeblocks/pkg/configuration/core"
    36  	cfgutil "github.com/1aal/kubeblocks/pkg/configuration/util"
    37  	"github.com/1aal/kubeblocks/pkg/configuration/validate"
    38  	"github.com/1aal/kubeblocks/pkg/constant"
    39  	"github.com/1aal/kubeblocks/pkg/controller/component"
    40  	"github.com/1aal/kubeblocks/pkg/controller/factory"
    41  	intctrlutil "github.com/1aal/kubeblocks/pkg/controllerutil"
    42  	"github.com/1aal/kubeblocks/pkg/generics"
    43  )
    44  
    45  type templateRenderValidator = func(map[string]string) error
    46  
    47  type renderWrapper struct {
    48  	templateBuilder *configTemplateBuilder
    49  
    50  	volumes             map[string]appsv1alpha1.ComponentTemplateSpec
    51  	templateAnnotations map[string]string
    52  	renderedObjs        []client.Object
    53  
    54  	ctx     context.Context
    55  	cli     client.Client
    56  	cluster *appsv1alpha1.Cluster
    57  }
    58  
    59  func newTemplateRenderWrapper(templateBuilder *configTemplateBuilder, cluster *appsv1alpha1.Cluster, ctx context.Context, cli client.Client) renderWrapper {
    60  	return renderWrapper{
    61  		ctx:     ctx,
    62  		cli:     cli,
    63  		cluster: cluster,
    64  
    65  		templateBuilder:     templateBuilder,
    66  		templateAnnotations: make(map[string]string),
    67  		volumes:             make(map[string]appsv1alpha1.ComponentTemplateSpec),
    68  	}
    69  }
    70  
    71  func (wrapper *renderWrapper) checkRerenderTemplateSpec(cfgCMName string, localObjs []client.Object) (*corev1.ConfigMap, error) {
    72  	cmKey := client.ObjectKey{
    73  		Name:      cfgCMName,
    74  		Namespace: wrapper.cluster.Namespace,
    75  	}
    76  
    77  	cmObj := &corev1.ConfigMap{}
    78  	localObject := findMatchedLocalObject(localObjs, cmKey, generics.ToGVK(cmObj))
    79  	if localObject != nil {
    80  		if cm, ok := localObject.(*corev1.ConfigMap); ok {
    81  			return cm, nil
    82  		}
    83  	}
    84  
    85  	cmErr := wrapper.cli.Get(wrapper.ctx, cmKey, cmObj)
    86  	if cmErr != nil && !apierrors.IsNotFound(cmErr) {
    87  		// An unexpected error occurs
    88  		return nil, cmErr
    89  	}
    90  	if cmErr != nil {
    91  		// Config is not exists
    92  		return nil, nil
    93  	}
    94  
    95  	return cmObj, nil
    96  }
    97  
    98  func (wrapper *renderWrapper) renderConfigTemplate(cluster *appsv1alpha1.Cluster,
    99  	component *component.SynthesizedComponent, localObjs []client.Object, configuration *appsv1alpha1.Configuration) error {
   100  	revision := fromConfiguration(configuration)
   101  	for _, configSpec := range component.ConfigTemplates {
   102  		var item *appsv1alpha1.ConfigurationItemDetail
   103  		cmName := core.GetComponentCfgName(cluster.Name, component.Name, configSpec.Name)
   104  		origCMObj, err := wrapper.checkRerenderTemplateSpec(cmName, localObjs)
   105  		if err != nil {
   106  			return err
   107  		}
   108  		if origCMObj != nil {
   109  			wrapper.addVolumeMountMeta(configSpec.ComponentTemplateSpec, origCMObj, false)
   110  			continue
   111  		}
   112  		if configuration != nil {
   113  			item = configuration.Spec.GetConfigurationItem(configSpec.Name)
   114  		}
   115  		newCMObj, err := wrapper.rerenderConfigTemplate(cluster, component, configSpec, item)
   116  		if err != nil {
   117  			return err
   118  		}
   119  		if err := applyUpdatedParameters(item, newCMObj, configSpec, wrapper.cli, wrapper.ctx); err != nil {
   120  			return err
   121  		}
   122  		if err := wrapper.addRenderedObject(configSpec.ComponentTemplateSpec, newCMObj, configuration); err != nil {
   123  			return err
   124  		}
   125  		if err := updateConfigMetaForCM(newCMObj, item, revision); err != nil {
   126  			return err
   127  		}
   128  	}
   129  	return nil
   130  }
   131  
   132  func fromConfiguration(configuration *appsv1alpha1.Configuration) string {
   133  	if configuration == nil {
   134  		return ""
   135  	}
   136  	return strconv.FormatInt(configuration.GetGeneration(), 10)
   137  }
   138  
   139  func updateConfigMetaForCM(newCMObj *corev1.ConfigMap, item *appsv1alpha1.ConfigurationItemDetail, revision string) (err error) {
   140  	if item == nil {
   141  		return
   142  	}
   143  
   144  	annotations := newCMObj.GetAnnotations()
   145  	if annotations == nil {
   146  		annotations = make(map[string]string)
   147  	}
   148  	b, err := json.Marshal(item)
   149  	if err != nil {
   150  		return err
   151  	}
   152  	annotations[constant.ConfigAppliedVersionAnnotationKey] = string(b)
   153  	hash, _ := cfgutil.ComputeHash(newCMObj.Data)
   154  	annotations[constant.CMInsCurrentConfigurationHashLabelKey] = hash
   155  	annotations[constant.ConfigurationRevision] = revision
   156  	annotations[constant.CMConfigurationTemplateVersion] = item.Version
   157  	newCMObj.Annotations = annotations
   158  	return
   159  }
   160  
   161  func applyUpdatedParameters(item *appsv1alpha1.ConfigurationItemDetail, cm *corev1.ConfigMap, configSpec appsv1alpha1.ComponentConfigSpec, cli client.Client, ctx context.Context) (err error) {
   162  	var newData map[string]string
   163  	var configConstraint *appsv1alpha1.ConfigConstraint
   164  
   165  	if item == nil || len(item.ConfigFileParams) == 0 {
   166  		return
   167  	}
   168  	if configSpec.ConfigConstraintRef != "" {
   169  		configConstraint, err = fetchConfigConstraint(configSpec.ConfigConstraintRef, ctx, cli)
   170  	}
   171  	if err != nil {
   172  		return
   173  	}
   174  	newData, err = DoMerge(cm.Data, item.ConfigFileParams, configConstraint, configSpec)
   175  	if err != nil {
   176  		return
   177  	}
   178  	cm.Data = newData
   179  	return
   180  }
   181  
   182  func (wrapper *renderWrapper) rerenderConfigTemplate(cluster *appsv1alpha1.Cluster,
   183  	component *component.SynthesizedComponent,
   184  	configSpec appsv1alpha1.ComponentConfigSpec,
   185  	item *appsv1alpha1.ConfigurationItemDetail,
   186  ) (*corev1.ConfigMap, error) {
   187  	cmName := core.GetComponentCfgName(cluster.Name, component.Name, configSpec.Name)
   188  	newCMObj, err := generateConfigMapFromTpl(cluster,
   189  		component,
   190  		wrapper.templateBuilder,
   191  		cmName,
   192  		configSpec.ConfigConstraintRef,
   193  		configSpec.ComponentTemplateSpec,
   194  		wrapper.ctx,
   195  		wrapper.cli,
   196  		func(m map[string]string) error {
   197  			return validateRenderedData(m, configSpec, wrapper.ctx, wrapper.cli)
   198  		})
   199  	if err != nil {
   200  		return nil, err
   201  	}
   202  	// render user specified template
   203  	if item != nil && item.ImportTemplateRef != nil {
   204  		newData, err := mergerConfigTemplate(
   205  			&appsv1alpha1.LegacyRenderedTemplateSpec{
   206  				ConfigTemplateExtension: *item.ImportTemplateRef,
   207  			},
   208  			wrapper.templateBuilder,
   209  			configSpec,
   210  			newCMObj.Data,
   211  			wrapper.ctx,
   212  			wrapper.cli)
   213  		if err != nil {
   214  			return nil, err
   215  		}
   216  		newCMObj.Data = newData
   217  	}
   218  	UpdateCMConfigSpecLabels(newCMObj, configSpec)
   219  	return newCMObj, nil
   220  }
   221  
   222  func (wrapper *renderWrapper) renderScriptTemplate(cluster *appsv1alpha1.Cluster, component *component.SynthesizedComponent,
   223  	localObjs []client.Object) error {
   224  	for _, templateSpec := range component.ScriptTemplates {
   225  		cmName := core.GetComponentCfgName(cluster.Name, component.Name, templateSpec.Name)
   226  		object := findMatchedLocalObject(localObjs, client.ObjectKey{
   227  			Name:      cmName,
   228  			Namespace: wrapper.cluster.Namespace}, generics.ToGVK(&corev1.ConfigMap{}))
   229  		if object != nil {
   230  			wrapper.addVolumeMountMeta(templateSpec, object, false)
   231  			continue
   232  		}
   233  
   234  		// Generate ConfigMap objects for config files
   235  		cm, err := generateConfigMapFromTpl(cluster, component, wrapper.templateBuilder, cmName, "", templateSpec, wrapper.ctx, wrapper.cli, nil)
   236  		if err != nil {
   237  			return err
   238  		}
   239  		if err := wrapper.addRenderedObject(templateSpec, cm, nil); err != nil {
   240  			return err
   241  		}
   242  	}
   243  	return nil
   244  }
   245  
   246  func (wrapper *renderWrapper) addRenderedObject(templateSpec appsv1alpha1.ComponentTemplateSpec, cm *corev1.ConfigMap, configuration *appsv1alpha1.Configuration) (err error) {
   247  	// The owner of the configmap object is a cluster,
   248  	// in order to manage the life cycle of configmap
   249  	if configuration != nil {
   250  		err = intctrlutil.SetControllerReference(configuration, cm)
   251  	} else {
   252  		err = intctrlutil.SetOwnerReference(wrapper.cluster, cm)
   253  	}
   254  	if err != nil {
   255  		return err
   256  	}
   257  
   258  	core.SetParametersUpdateSource(cm, constant.ReconfigureManagerSource)
   259  	wrapper.addVolumeMountMeta(templateSpec, cm, true)
   260  	return nil
   261  }
   262  
   263  func (wrapper *renderWrapper) addVolumeMountMeta(templateSpec appsv1alpha1.ComponentTemplateSpec, object client.Object, rendered bool) {
   264  	wrapper.volumes[object.GetName()] = templateSpec
   265  	if rendered {
   266  		wrapper.renderedObjs = append(wrapper.renderedObjs, object)
   267  	}
   268  	wrapper.templateAnnotations[core.GenerateTPLUniqLabelKeyWithConfig(templateSpec.Name)] = object.GetName()
   269  }
   270  
   271  func (wrapper *renderWrapper) CheckAndPatchConfigResource(origCMObj *corev1.ConfigMap, newData map[string]string) error {
   272  	if origCMObj == nil {
   273  		return nil
   274  	}
   275  	if reflect.DeepEqual(origCMObj.Data, newData) {
   276  		return nil
   277  	}
   278  
   279  	patch := client.MergeFrom(origCMObj.DeepCopy())
   280  	origCMObj.Data = newData
   281  	if origCMObj.Annotations == nil {
   282  		origCMObj.Annotations = make(map[string]string)
   283  	}
   284  	core.SetParametersUpdateSource(origCMObj, constant.ReconfigureManagerSource)
   285  	rawData, err := json.Marshal(origCMObj.Data)
   286  	if err != nil {
   287  		return err
   288  	}
   289  
   290  	origCMObj.Annotations[corev1.LastAppliedConfigAnnotation] = string(rawData)
   291  	return wrapper.cli.Patch(wrapper.ctx, origCMObj, patch)
   292  }
   293  
   294  func findMatchedLocalObject(localObjs []client.Object, objKey client.ObjectKey, gvk schema.GroupVersionKind) client.Object {
   295  	for _, obj := range localObjs {
   296  		if obj.GetName() == objKey.Name && obj.GetNamespace() == objKey.Namespace {
   297  			if generics.ToGVK(obj) == gvk {
   298  				return obj
   299  			}
   300  		}
   301  	}
   302  	return nil
   303  }
   304  
   305  func UpdateCMConfigSpecLabels(cm *corev1.ConfigMap, configSpec appsv1alpha1.ComponentConfigSpec) {
   306  	if cm.Labels == nil {
   307  		cm.Labels = make(map[string]string)
   308  	}
   309  
   310  	cm.Labels[constant.CMConfigurationSpecProviderLabelKey] = configSpec.Name
   311  	cm.Labels[constant.CMConfigurationTemplateNameLabelKey] = configSpec.TemplateRef
   312  	if configSpec.ConfigConstraintRef != "" {
   313  		cm.Labels[constant.CMConfigurationConstraintsNameLabelKey] = configSpec.ConfigConstraintRef
   314  	}
   315  
   316  	if len(configSpec.Keys) != 0 {
   317  		cm.Labels[constant.CMConfigurationCMKeysLabelKey] = strings.Join(configSpec.Keys, ",")
   318  	}
   319  }
   320  
   321  // generateConfigMapFromTpl renders config file by config template provided by provider.
   322  func generateConfigMapFromTpl(cluster *appsv1alpha1.Cluster,
   323  	component *component.SynthesizedComponent,
   324  	tplBuilder *configTemplateBuilder,
   325  	cmName string,
   326  	configConstraintName string,
   327  	templateSpec appsv1alpha1.ComponentTemplateSpec,
   328  	ctx context.Context,
   329  	cli client.Client, dataValidator templateRenderValidator) (*corev1.ConfigMap, error) {
   330  	// Render config template by TplEngine
   331  	// The template namespace must be the same as the ClusterDefinition namespace
   332  	configs, err := renderConfigMapTemplate(tplBuilder, templateSpec, ctx, cli)
   333  	if err != nil {
   334  		return nil, err
   335  	}
   336  
   337  	if dataValidator != nil {
   338  		if err = dataValidator(configs); err != nil {
   339  			return nil, err
   340  		}
   341  	}
   342  
   343  	// Using ConfigMap cue template render to configmap of config
   344  	return factory.BuildConfigMapWithTemplate(cluster, component, configs, cmName, templateSpec), nil
   345  }
   346  
   347  // renderConfigMapTemplate renders config file using template engine
   348  func renderConfigMapTemplate(
   349  	templateBuilder *configTemplateBuilder,
   350  	templateSpec appsv1alpha1.ComponentTemplateSpec,
   351  	ctx context.Context,
   352  	cli client.Client) (map[string]string, error) {
   353  	cmObj := &corev1.ConfigMap{}
   354  	//  Require template configmap exist
   355  	if err := cli.Get(ctx, client.ObjectKey{
   356  		Namespace: templateSpec.Namespace,
   357  		Name:      templateSpec.TemplateRef,
   358  	}, cmObj); err != nil {
   359  		return nil, err
   360  	}
   361  
   362  	if len(cmObj.Data) == 0 {
   363  		return map[string]string{}, nil
   364  	}
   365  
   366  	templateBuilder.setTemplateName(templateSpec.TemplateRef)
   367  	renderedData, err := templateBuilder.render(cmObj.Data)
   368  	if err != nil {
   369  		return nil, core.WrapError(err, "failed to render configmap")
   370  	}
   371  	return renderedData, nil
   372  }
   373  
   374  func fetchConfigConstraint(ccName string, ctx context.Context, cli client.Client) (*appsv1alpha1.ConfigConstraint, error) {
   375  	ccKey := client.ObjectKey{
   376  		Name: ccName,
   377  	}
   378  	configConstraint := &appsv1alpha1.ConfigConstraint{}
   379  	if err := cli.Get(ctx, ccKey, configConstraint); err != nil {
   380  		return nil, core.WrapError(err, "failed to get ConfigConstraint, key[%s]", ccName)
   381  	}
   382  	return configConstraint, nil
   383  }
   384  
   385  // validateRenderedData validates config file against constraint
   386  func validateRenderedData(
   387  	renderedData map[string]string,
   388  	configSpec appsv1alpha1.ComponentConfigSpec,
   389  	ctx context.Context,
   390  	cli client.Client) error {
   391  	if configSpec.ConfigConstraintRef == "" {
   392  		return nil
   393  	}
   394  	configConstraint, err := fetchConfigConstraint(configSpec.ConfigConstraintRef, ctx, cli)
   395  	if err != nil {
   396  		return err
   397  	}
   398  	return validateRawData(renderedData, configSpec, &configConstraint.Spec)
   399  }
   400  
   401  func validateRawData(renderedData map[string]string, configSpec appsv1alpha1.ComponentConfigSpec, cc *appsv1alpha1.ConfigConstraintSpec) error {
   402  	configChecker := validate.NewConfigValidator(cc, validate.WithKeySelector(configSpec.Keys))
   403  	// NOTE: It is necessary to verify the correctness of the data
   404  	if err := configChecker.Validate(renderedData); err != nil {
   405  		return core.WrapError(err, "failed to validate configmap")
   406  	}
   407  	return nil
   408  }