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 }