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 }