github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/controllers/apps/configuration/config_util.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  	"fmt"
    25  	"reflect"
    26  
    27  	"github.com/go-logr/logr"
    28  	corev1 "k8s.io/api/core/v1"
    29  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    30  	"k8s.io/apimachinery/pkg/conversion"
    31  	"sigs.k8s.io/controller-runtime/pkg/client"
    32  	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    33  
    34  	appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    35  	cfgcm "github.com/1aal/kubeblocks/pkg/configuration/config_manager"
    36  	"github.com/1aal/kubeblocks/pkg/configuration/core"
    37  	"github.com/1aal/kubeblocks/pkg/configuration/openapi"
    38  	"github.com/1aal/kubeblocks/pkg/configuration/validate"
    39  	"github.com/1aal/kubeblocks/pkg/constant"
    40  	intctrlutil "github.com/1aal/kubeblocks/pkg/controllerutil"
    41  	"github.com/1aal/kubeblocks/pkg/generics"
    42  )
    43  
    44  type ValidateConfigMap func(configTpl, ns string) (*corev1.ConfigMap, error)
    45  type ValidateConfigSchema func(tpl *appsv1alpha1.CustomParametersValidation) (bool, error)
    46  
    47  func checkConfigLabels(object client.Object, requiredLabs []string) bool {
    48  	labels := object.GetLabels()
    49  	if len(labels) == 0 {
    50  		return false
    51  	}
    52  
    53  	for _, label := range requiredLabs {
    54  		if _, ok := labels[label]; !ok {
    55  			return false
    56  		}
    57  	}
    58  
    59  	// reconfigure ConfigMap for db instance
    60  	if ins, ok := labels[constant.CMConfigurationTypeLabelKey]; !ok || ins != constant.ConfigInstanceType {
    61  		return false
    62  	}
    63  
    64  	return checkEnableCfgUpgrade(object)
    65  }
    66  
    67  func getConfigMapByTemplateName(cli client.Client, ctx intctrlutil.RequestCtx, templateName, ns string) (*corev1.ConfigMap, error) {
    68  	if len(templateName) == 0 {
    69  		return nil, fmt.Errorf("required configmap reference name is empty! [%v]", templateName)
    70  	}
    71  
    72  	configObj := &corev1.ConfigMap{}
    73  	if err := cli.Get(ctx.Ctx, client.ObjectKey{
    74  		Namespace: ns,
    75  		Name:      templateName,
    76  	}, configObj); err != nil {
    77  		ctx.Log.Error(err, "failed to get config template cm object!", "configMapName", templateName)
    78  		return nil, err
    79  	}
    80  
    81  	return configObj, nil
    82  }
    83  
    84  func checkConfigConstraint(ctx intctrlutil.RequestCtx, configConstraint *appsv1alpha1.ConfigConstraint) (bool, error) {
    85  	// validate configuration template
    86  	validateConfigSchema := func(ccSchema *appsv1alpha1.CustomParametersValidation) (bool, error) {
    87  		if ccSchema == nil || len(ccSchema.CUE) == 0 {
    88  			return true, nil
    89  		}
    90  
    91  		err := validate.CueValidate(ccSchema.CUE)
    92  		return err == nil, err
    93  	}
    94  
    95  	// validate schema
    96  	if ok, err := validateConfigSchema(configConstraint.Spec.ConfigurationSchema); !ok || err != nil {
    97  		ctx.Log.Error(err, "failed to validate template schema!", "configMapName", fmt.Sprintf("%v", configConstraint.Spec.ConfigurationSchema))
    98  		return ok, err
    99  	}
   100  	return true, nil
   101  }
   102  
   103  func ReconcileConfigSpecsForReferencedCR[T generics.Object, PT generics.PObject[T]](client client.Client, ctx intctrlutil.RequestCtx, obj PT) error {
   104  	if ok, err := checkConfigTemplate(client, ctx, obj); !ok || err != nil {
   105  		return fmt.Errorf("failed to check config template: %v", err)
   106  	}
   107  	if ok, err := updateLabelsByConfigSpec(client, ctx, obj); !ok || err != nil {
   108  		return fmt.Errorf("failed to update using config template info: %v", err)
   109  	}
   110  	if _, err := updateConfigMapFinalizer(client, ctx, obj); err != nil {
   111  		return fmt.Errorf("failed to update config map finalizer: %v", err)
   112  	}
   113  	return nil
   114  }
   115  
   116  func DeleteConfigMapFinalizer(cli client.Client, ctx intctrlutil.RequestCtx, obj client.Object) error {
   117  	handler := func(configSpecs []appsv1alpha1.ComponentConfigSpec) (bool, error) {
   118  		return true, batchDeleteConfigMapFinalizer(cli, ctx, configSpecs, obj)
   119  	}
   120  	_, err := handleConfigTemplate(obj, handler)
   121  	return err
   122  }
   123  
   124  func validateConfigMapOwners(cli client.Client, ctx intctrlutil.RequestCtx, labels client.MatchingLabels, check func(obj client.Object) bool, objLists ...client.ObjectList) (bool, error) {
   125  	for _, objList := range objLists {
   126  		if err := cli.List(ctx.Ctx, objList, labels, client.Limit(2)); err != nil {
   127  			return false, err
   128  		}
   129  		v, err := conversion.EnforcePtr(objList)
   130  		if err != nil {
   131  			return false, err
   132  		}
   133  		items := v.FieldByName("Items")
   134  		if !items.IsValid() || items.Kind() != reflect.Slice || items.Len() > 1 {
   135  			return false, nil
   136  		}
   137  		if items.Len() == 0 {
   138  			continue
   139  		}
   140  
   141  		val := items.Index(0)
   142  		// fetch object pointer
   143  		if val.CanAddr() {
   144  			val = val.Addr()
   145  		}
   146  		if !val.CanInterface() || !check(val.Interface().(client.Object)) {
   147  			return false, nil
   148  		}
   149  	}
   150  	return true, nil
   151  }
   152  
   153  func batchDeleteConfigMapFinalizer(cli client.Client, ctx intctrlutil.RequestCtx, configSpecs []appsv1alpha1.ComponentConfigSpec, cr client.Object) error {
   154  	validator := func(obj client.Object) bool {
   155  		return obj.GetName() == cr.GetName() && obj.GetNamespace() == cr.GetNamespace()
   156  	}
   157  	for _, configSpec := range configSpecs {
   158  		labels := client.MatchingLabels{
   159  			core.GenerateTPLUniqLabelKeyWithConfig(configSpec.Name): configSpec.TemplateRef,
   160  		}
   161  		if ok, err := validateConfigMapOwners(cli, ctx, labels, validator, &appsv1alpha1.ClusterVersionList{}, &appsv1alpha1.ClusterDefinitionList{}); err != nil {
   162  			return err
   163  		} else if !ok {
   164  			continue
   165  		}
   166  		if err := deleteConfigMapFinalizer(cli, ctx, configSpec); err != nil {
   167  			return err
   168  		}
   169  	}
   170  	return nil
   171  }
   172  
   173  func updateConfigMapFinalizer(client client.Client, ctx intctrlutil.RequestCtx, obj client.Object) (bool, error) {
   174  	handler := func(configSpecs []appsv1alpha1.ComponentConfigSpec) (bool, error) {
   175  		return true, batchUpdateConfigMapFinalizer(client, ctx, configSpecs)
   176  	}
   177  	return handleConfigTemplate(obj, handler)
   178  }
   179  
   180  func batchUpdateConfigMapFinalizer(cli client.Client, ctx intctrlutil.RequestCtx, configSpecs []appsv1alpha1.ComponentConfigSpec) error {
   181  	for _, configSpec := range configSpecs {
   182  		if err := updateConfigMapFinalizerImpl(cli, ctx, configSpec); err != nil {
   183  			return err
   184  		}
   185  	}
   186  	return nil
   187  }
   188  
   189  func updateConfigMapFinalizerImpl(cli client.Client, ctx intctrlutil.RequestCtx, configSpec appsv1alpha1.ComponentConfigSpec) error {
   190  	// step1: add finalizer
   191  	// step2: add labels: CMConfigurationTypeLabelKey
   192  	// step3: update immutable
   193  
   194  	cmObj, err := getConfigMapByTemplateName(cli, ctx, configSpec.TemplateRef, configSpec.Namespace)
   195  	if err != nil {
   196  		ctx.Log.Error(err, "failed to get template cm object!", "configMapName", cmObj.Name)
   197  		return err
   198  	}
   199  
   200  	if controllerutil.ContainsFinalizer(cmObj, constant.ConfigurationTemplateFinalizerName) {
   201  		return nil
   202  	}
   203  
   204  	patch := client.MergeFrom(cmObj.DeepCopy())
   205  
   206  	if cmObj.Labels == nil {
   207  		cmObj.Labels = map[string]string{}
   208  	}
   209  	cmObj.Labels[constant.CMConfigurationTypeLabelKey] = constant.ConfigTemplateType
   210  	controllerutil.AddFinalizer(cmObj, constant.ConfigurationTemplateFinalizerName)
   211  
   212  	// cmObj.Immutable = &tpl.Spec.Immutable
   213  	return cli.Patch(ctx.Ctx, cmObj, patch)
   214  }
   215  
   216  func deleteConfigMapFinalizer(cli client.Client, ctx intctrlutil.RequestCtx, configSpec appsv1alpha1.ComponentConfigSpec) error {
   217  	cmObj, err := getConfigMapByTemplateName(cli, ctx, configSpec.TemplateRef, configSpec.Namespace)
   218  	if err != nil && apierrors.IsNotFound(err) {
   219  		return nil
   220  	} else if err != nil {
   221  		ctx.Log.Error(err, "failed to get config template cm object!", "configMapName", configSpec.TemplateRef)
   222  		return err
   223  	}
   224  
   225  	if !controllerutil.ContainsFinalizer(cmObj, constant.ConfigurationTemplateFinalizerName) {
   226  		return nil
   227  	}
   228  
   229  	patch := client.MergeFrom(cmObj.DeepCopy())
   230  	controllerutil.RemoveFinalizer(cmObj, constant.ConfigurationTemplateFinalizerName)
   231  	return cli.Patch(ctx.Ctx, cmObj, patch)
   232  }
   233  
   234  type ConfigTemplateHandler func([]appsv1alpha1.ComponentConfigSpec) (bool, error)
   235  type ComponentValidateHandler func(component *appsv1alpha1.ClusterComponentDefinition) error
   236  
   237  func handleConfigTemplate(object client.Object, handler ConfigTemplateHandler, handler2 ...ComponentValidateHandler) (bool, error) {
   238  	var (
   239  		err             error
   240  		configTemplates []appsv1alpha1.ComponentConfigSpec
   241  	)
   242  	switch cr := object.(type) {
   243  	case *appsv1alpha1.ClusterDefinition:
   244  		configTemplates, err = getConfigTemplateFromCD(cr, handler2...)
   245  	case *appsv1alpha1.ClusterVersion:
   246  		configTemplates = getConfigTemplateFromCV(cr)
   247  	default:
   248  		return false, core.MakeError("not support CR type: %v", cr)
   249  	}
   250  
   251  	switch {
   252  	case err != nil:
   253  		return false, err
   254  	case len(configTemplates) > 0:
   255  		return handler(configTemplates)
   256  	default:
   257  		return true, nil
   258  	}
   259  }
   260  
   261  func getConfigTemplateFromCV(appVer *appsv1alpha1.ClusterVersion) []appsv1alpha1.ComponentConfigSpec {
   262  	configTemplates := make([]appsv1alpha1.ComponentConfigSpec, 0)
   263  	for _, component := range appVer.Spec.ComponentVersions {
   264  		if len(component.ConfigSpecs) > 0 {
   265  			configTemplates = append(configTemplates, component.ConfigSpecs...)
   266  		}
   267  	}
   268  	return configTemplates
   269  }
   270  
   271  func getConfigTemplateFromCD(clusterDef *appsv1alpha1.ClusterDefinition, validators ...ComponentValidateHandler) ([]appsv1alpha1.ComponentConfigSpec, error) {
   272  	configTemplates := make([]appsv1alpha1.ComponentConfigSpec, 0)
   273  	for _, component := range clusterDef.Spec.ComponentDefs {
   274  		// For compatibility with the previous lifecycle management of configurationSpec.TemplateRef, it is necessary to convert ScriptSpecs to ConfigSpecs,
   275  		// ensuring that the script-related configmap is not allowed to be deleted.
   276  		for _, scriptSpec := range component.ScriptSpecs {
   277  			configTemplates = append(configTemplates, appsv1alpha1.ComponentConfigSpec{
   278  				ComponentTemplateSpec: scriptSpec,
   279  			})
   280  		}
   281  		if len(component.ConfigSpecs) == 0 {
   282  			continue
   283  		}
   284  		configTemplates = append(configTemplates, component.ConfigSpecs...)
   285  		// Check reload configure config template
   286  		for _, validator := range validators {
   287  			if err := validator(&component); err != nil {
   288  				return nil, err
   289  			}
   290  		}
   291  	}
   292  	return configTemplates, nil
   293  }
   294  
   295  func checkConfigTemplate(client client.Client, ctx intctrlutil.RequestCtx, obj client.Object) (bool, error) {
   296  	handler := func(configSpecs []appsv1alpha1.ComponentConfigSpec) (bool, error) {
   297  		return validateConfigTemplate(client, ctx, configSpecs)
   298  	}
   299  	return handleConfigTemplate(obj, handler)
   300  }
   301  
   302  func updateLabelsByConfigSpec[T generics.Object, PT generics.PObject[T]](cli client.Client, ctx intctrlutil.RequestCtx, obj PT) (bool, error) {
   303  	handler := func(configSpecs []appsv1alpha1.ComponentConfigSpec) (bool, error) {
   304  		patch := client.MergeFrom(PT(obj.DeepCopy()))
   305  		labels := obj.GetLabels()
   306  		if labels == nil {
   307  			labels = map[string]string{}
   308  		}
   309  		for _, configSpec := range configSpecs {
   310  			labels[core.GenerateTPLUniqLabelKeyWithConfig(configSpec.Name)] = configSpec.TemplateRef
   311  			if len(configSpec.ConfigConstraintRef) != 0 {
   312  				labels[core.GenerateConstraintsUniqLabelKeyWithConfig(configSpec.ConfigConstraintRef)] = configSpec.ConfigConstraintRef
   313  			}
   314  		}
   315  		obj.SetLabels(labels)
   316  		return true, cli.Patch(ctx.Ctx, obj, patch)
   317  	}
   318  	return handleConfigTemplate(obj, handler)
   319  }
   320  
   321  func validateConfigTemplate(cli client.Client, ctx intctrlutil.RequestCtx, configSpecs []appsv1alpha1.ComponentConfigSpec) (bool, error) {
   322  	// validate ConfigTemplate
   323  	foundAndCheckConfigSpec := func(configSpec appsv1alpha1.ComponentConfigSpec, logger logr.Logger) (*appsv1alpha1.ConfigConstraint, error) {
   324  		if _, err := getConfigMapByTemplateName(cli, ctx, configSpec.TemplateRef, configSpec.Namespace); err != nil {
   325  			logger.Error(err, "failed to get config template cm object!")
   326  			return nil, err
   327  		}
   328  		if configSpec.VolumeName == "" && len(configSpec.AsEnvFrom) == 0 {
   329  			return nil, core.MakeError("config template volume name and envFrom is empty!")
   330  		}
   331  		if configSpec.ConfigConstraintRef == "" {
   332  			return nil, nil
   333  		}
   334  		configKey := client.ObjectKey{
   335  			Namespace: "",
   336  			Name:      configSpec.ConfigConstraintRef,
   337  		}
   338  		configObj := &appsv1alpha1.ConfigConstraint{}
   339  		if err := cli.Get(ctx.Ctx, configKey, configObj); err != nil {
   340  			logger.Error(err, "failed to get template cm object!")
   341  			return nil, err
   342  		}
   343  		return configObj, nil
   344  	}
   345  
   346  	for _, templateRef := range configSpecs {
   347  		logger := ctx.Log.WithValues("templateName", templateRef.Name).WithValues("configMapName", templateRef.TemplateRef)
   348  		configConstraint, err := foundAndCheckConfigSpec(templateRef, logger)
   349  		if err != nil {
   350  			logger.Error(err, "failed to validate config template!")
   351  			return false, err
   352  		}
   353  		if configConstraint == nil || configConstraint.Spec.ReloadOptions == nil {
   354  			continue
   355  		}
   356  		if err := cfgcm.ValidateReloadOptions(configConstraint.Spec.ReloadOptions, cli, ctx.Ctx); err != nil {
   357  			return false, err
   358  		}
   359  		if !validateConfigConstraintStatus(configConstraint.Status) {
   360  			errMsg := fmt.Sprintf("Configuration template CR[%s] status not ready! current status: %s", configConstraint.Name, configConstraint.Status.Phase)
   361  			logger.V(1).Info(errMsg)
   362  			return false, fmt.Errorf(errMsg)
   363  		}
   364  	}
   365  	return true, nil
   366  }
   367  
   368  func validateConfigConstraintStatus(ccStatus appsv1alpha1.ConfigConstraintStatus) bool {
   369  	return ccStatus.Phase == appsv1alpha1.CCAvailablePhase
   370  }
   371  
   372  func updateConfigConstraintStatus(cli client.Client, ctx intctrlutil.RequestCtx, configConstraint *appsv1alpha1.ConfigConstraint, phase appsv1alpha1.ConfigConstraintPhase) error {
   373  	patch := client.MergeFrom(configConstraint.DeepCopy())
   374  	configConstraint.Status.Phase = phase
   375  	configConstraint.Status.ObservedGeneration = configConstraint.Generation
   376  	return cli.Status().Patch(ctx.Ctx, configConstraint, patch)
   377  }
   378  
   379  func createConfigPatch(cfg *corev1.ConfigMap, formatter *appsv1alpha1.FormatterConfig, cmKeys []string) (*core.ConfigPatchInfo, bool, error) {
   380  	// support full update
   381  	if formatter == nil {
   382  		return nil, true, nil
   383  	}
   384  	lastConfig, err := getLastVersionConfig(cfg)
   385  	if err != nil {
   386  		return nil, false, core.WrapError(err, "failed to get last version data. config[%v]", client.ObjectKeyFromObject(cfg))
   387  	}
   388  
   389  	return core.CreateConfigPatch(lastConfig, cfg.Data, formatter.Format, cmKeys, true)
   390  }
   391  
   392  func updateConfigSchema(cc *appsv1alpha1.ConfigConstraint, cli client.Client, ctx context.Context) error {
   393  	schema := cc.Spec.ConfigurationSchema
   394  	if schema == nil || schema.CUE == "" || schema.Schema != nil {
   395  		return nil
   396  	}
   397  
   398  	// Because the conversion of cue to openAPISchema is restricted, and the definition of some cue may not be converted into openAPISchema, and won't return error.
   399  	openAPISchema, err := openapi.GenerateOpenAPISchema(schema.CUE, cc.Spec.CfgSchemaTopLevelName)
   400  	if err != nil {
   401  		return err
   402  	}
   403  	if openAPISchema == nil {
   404  		return nil
   405  	}
   406  
   407  	ccPatch := client.MergeFrom(cc.DeepCopy())
   408  	cc.Spec.ConfigurationSchema.Schema = openAPISchema
   409  	return cli.Patch(ctx, cc, ccPatch)
   410  }