github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/cluster/update.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 cluster 21 22 import ( 23 "bytes" 24 "context" 25 "encoding/csv" 26 "encoding/json" 27 "fmt" 28 "strconv" 29 "strings" 30 "text/template" 31 32 "github.com/google/uuid" 33 "github.com/pkg/errors" 34 "github.com/robfig/cron/v3" 35 "github.com/spf13/cobra" 36 "github.com/spf13/pflag" 37 corev1 "k8s.io/api/core/v1" 38 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 39 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 40 "k8s.io/apimachinery/pkg/runtime" 41 "k8s.io/cli-runtime/pkg/genericiooptions" 42 "k8s.io/client-go/dynamic" 43 cmdutil "k8s.io/kubectl/pkg/cmd/util" 44 "k8s.io/kubectl/pkg/util/templates" 45 46 appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1" 47 dpv1alpha1 "github.com/1aal/kubeblocks/apis/dataprotection/v1alpha1" 48 "github.com/1aal/kubeblocks/pkg/cli/cluster" 49 "github.com/1aal/kubeblocks/pkg/cli/patch" 50 "github.com/1aal/kubeblocks/pkg/cli/types" 51 "github.com/1aal/kubeblocks/pkg/cli/util" 52 cfgcore "github.com/1aal/kubeblocks/pkg/configuration/core" 53 "github.com/1aal/kubeblocks/pkg/constant" 54 "github.com/1aal/kubeblocks/pkg/controller/configuration" 55 "github.com/1aal/kubeblocks/pkg/dataprotection/utils" 56 "github.com/1aal/kubeblocks/pkg/gotemplate" 57 ) 58 59 var clusterUpdateExample = templates.Examples(` 60 # update cluster mycluster termination policy to Delete 61 kbcli cluster update mycluster --termination-policy=Delete 62 63 # enable cluster monitor 64 kbcli cluster update mycluster --monitor=true 65 66 # enable all logs 67 kbcli cluster update mycluster --enable-all-logs=true 68 69 # update cluster topology keys and affinity 70 kbcli cluster update mycluster --topology-keys=kubernetes.io/hostname --pod-anti-affinity=Required 71 72 # update cluster tolerations 73 kbcli cluster update mycluster --tolerations='"key=engineType,value=mongo,operator=Equal,effect=NoSchedule","key=diskType,value=ssd,operator=Equal,effect=NoSchedule"' 74 75 # edit cluster 76 kbcli cluster update mycluster --edit 77 78 # enable cluster monitor and edit 79 # kbcli cluster update mycluster --monitor=true --edit 80 81 # enable cluster auto backup 82 kbcli cluster update mycluster --backup-enabled=true 83 84 # update cluster backup retention period 85 kbcli cluster update mycluster --backup-retention-period=1d 86 87 # update cluster backup method 88 kbcli cluster update mycluster --backup-method=snapshot 89 90 # update cluster backup cron expression 91 kbcli cluster update mycluster --backup-cron-expression="0 0 * * *" 92 93 # update cluster backup starting deadline minutes 94 kbcli cluster update mycluster --backup-starting-deadline-minutes=10 95 96 # update cluster backup repo name 97 kbcli cluster update mycluster --backup-repo-name=repo1 98 99 # update cluster backup pitr enabled 100 kbcli cluster update mycluster --pitr-enabled=true 101 `) 102 103 type updateOptions struct { 104 namespace string 105 dynamic dynamic.Interface 106 cluster *appsv1alpha1.Cluster 107 108 UpdatableFlags 109 *patch.Options 110 } 111 112 func NewUpdateCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 113 o := &updateOptions{Options: patch.NewOptions(f, streams, types.ClusterGVR())} 114 o.Options.OutputOperation = func(didPatch bool) string { 115 if didPatch { 116 return "updated" 117 } 118 return "updated (no change)" 119 } 120 121 cmd := &cobra.Command{ 122 Use: "update NAME", 123 Short: "Update the cluster settings, such as enable or disable monitor or log.", 124 Example: clusterUpdateExample, 125 ValidArgsFunction: util.ResourceNameCompletionFunc(f, o.GVR), 126 Run: func(cmd *cobra.Command, args []string) { 127 util.CheckErr(o.complete(cmd, args)) 128 util.CheckErr(o.Run(cmd)) 129 }, 130 } 131 o.UpdatableFlags.addFlags(cmd) 132 o.Options.AddFlags(cmd) 133 134 return cmd 135 } 136 137 func (o *updateOptions) complete(cmd *cobra.Command, args []string) error { 138 var err error 139 if len(args) == 0 { 140 return makeMissingClusterNameErr() 141 } 142 if len(args) > 1 { 143 return fmt.Errorf("only support to update one cluster") 144 } 145 o.Names = args 146 147 // record the flags that been set by user 148 var flags []*pflag.Flag 149 cmd.Flags().Visit(func(flag *pflag.Flag) { 150 flags = append(flags, flag) 151 }) 152 153 // nothing to do 154 if len(flags) == 0 { 155 return nil 156 } 157 158 if o.namespace, _, err = o.Factory.ToRawKubeConfigLoader().Namespace(); err != nil { 159 return err 160 } 161 if o.dynamic, err = o.Factory.DynamicClient(); err != nil { 162 return err 163 } 164 return o.buildPatch(flags) 165 } 166 167 func (o *updateOptions) buildPatch(flags []*pflag.Flag) error { 168 var err error 169 type buildFn func(obj map[string]interface{}, v pflag.Value, field string) error 170 171 buildFlagObj := func(obj map[string]interface{}, v pflag.Value, field string) error { 172 var val interface{} 173 switch v.Type() { 174 case "string": 175 val = v.String() 176 case "stringArray", "stringSlice": 177 val = v.(pflag.SliceValue).GetSlice() 178 case "stringToString": 179 valMap := make(map[string]interface{}, 0) 180 vStr := strings.Trim(v.String(), "[]") 181 if len(vStr) > 0 { 182 r := csv.NewReader(strings.NewReader(vStr)) 183 ss, err := r.Read() 184 if err != nil { 185 return err 186 } 187 for _, pair := range ss { 188 kv := strings.SplitN(pair, "=", 2) 189 if len(kv) != 2 { 190 return fmt.Errorf("%s must be formatted as key=value", pair) 191 } 192 valMap[kv[0]] = kv[1] 193 } 194 } 195 val = valMap 196 } 197 return unstructured.SetNestedField(obj, val, field) 198 } 199 200 buildTolObj := func(obj map[string]interface{}, v pflag.Value, field string) error { 201 tolerations, err := util.BuildTolerations(o.TolerationsRaw) 202 if err != nil { 203 return err 204 } 205 return unstructured.SetNestedField(obj, tolerations, field) 206 } 207 208 buildComps := func(obj map[string]interface{}, v pflag.Value, field string) error { 209 return o.buildComponents(field, v.String()) 210 } 211 212 buildBackup := func(obj map[string]interface{}, v pflag.Value, field string) error { 213 return o.buildBackup(field, v.String()) 214 } 215 216 spec := map[string]interface{}{} 217 affinity := map[string]interface{}{} 218 type filedObj struct { 219 field string 220 obj map[string]interface{} 221 fn buildFn 222 } 223 224 flagFieldMapping := map[string]*filedObj{ 225 "termination-policy": {field: "terminationPolicy", obj: spec, fn: buildFlagObj}, 226 "pod-anti-affinity": {field: "podAntiAffinity", obj: affinity, fn: buildFlagObj}, 227 "topology-keys": {field: "topologyKeys", obj: affinity, fn: buildFlagObj}, 228 "node-labels": {field: "nodeLabels", obj: affinity, fn: buildFlagObj}, 229 "tenancy": {field: "tenancy", obj: affinity, fn: buildFlagObj}, 230 231 // tolerations 232 "tolerations": {field: "tolerations", obj: spec, fn: buildTolObj}, 233 234 // monitor and logs 235 "monitoring-interval": {field: "monitor", obj: nil, fn: buildComps}, 236 "enable-all-logs": {field: "enable-all-logs", obj: nil, fn: buildComps}, 237 238 // backup config 239 "backup-enabled": {field: "enabled", obj: nil, fn: buildBackup}, 240 "backup-retention-period": {field: "retentionPeriod", obj: nil, fn: buildBackup}, 241 "backup-method": {field: "method", obj: nil, fn: buildBackup}, 242 "backup-cron-expression": {field: "cronExpression", obj: nil, fn: buildBackup}, 243 "backup-starting-deadline-minutes": {field: "startingDeadlineMinutes", obj: nil, fn: buildBackup}, 244 "backup-repo-name": {field: "repoName", obj: nil, fn: buildBackup}, 245 "pitr-enabled": {field: "pitrEnabled", obj: nil, fn: buildBackup}, 246 } 247 248 for _, flag := range flags { 249 if f, ok := flagFieldMapping[flag.Name]; ok { 250 if err = f.fn(f.obj, flag.Value, f.field); err != nil { 251 return err 252 } 253 } 254 } 255 256 if len(affinity) > 0 { 257 if err = unstructured.SetNestedField(spec, affinity, "affinity"); err != nil { 258 return err 259 } 260 } 261 262 if o.cluster != nil { 263 // if update the backup config, the backup method must have value 264 if o.cluster.Spec.Backup != nil { 265 backupPolicyListObj, err := o.dynamic.Resource(types.BackupPolicyGVR()).Namespace(o.namespace).List(context.Background(), metav1.ListOptions{ 266 LabelSelector: fmt.Sprintf("%s=%s", constant.AppInstanceLabelKey, o.cluster.Name), 267 }) 268 if err != nil { 269 return err 270 } 271 backupPolicyList := &dpv1alpha1.BackupPolicyList{} 272 if err := runtime.DefaultUnstructuredConverter.FromUnstructured(backupPolicyListObj.UnstructuredContent(), backupPolicyList); err != nil { 273 return err 274 } 275 276 defaultBackupMethod, backupMethodMap, err := utils.GetBackupMethodsFromBackupPolicy(backupPolicyList, "") 277 if err != nil { 278 return err 279 } 280 if o.cluster.Spec.Backup.Method == "" { 281 o.cluster.Spec.Backup.Method = defaultBackupMethod 282 } 283 if _, ok := backupMethodMap[o.cluster.Spec.Backup.Method]; !ok { 284 return fmt.Errorf("backup method %s is not supported, please view the supported backup methods by `kbcli cd describe %s`", o.cluster.Spec.Backup.Method, o.cluster.Spec.ClusterDefRef) 285 } 286 } 287 288 data, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&o.cluster.Spec) 289 if err != nil { 290 return err 291 } 292 293 if err = unstructured.SetNestedField(spec, data["componentSpecs"], "componentSpecs"); err != nil { 294 return err 295 } 296 297 if err = unstructured.SetNestedField(spec, data["backup"], "backup"); err != nil { 298 return err 299 } 300 } 301 302 obj := unstructured.Unstructured{ 303 Object: map[string]interface{}{ 304 "spec": spec, 305 }, 306 } 307 bytes, err := obj.MarshalJSON() 308 if err != nil { 309 return err 310 } 311 o.Patch = string(bytes) 312 return nil 313 } 314 315 func (o *updateOptions) buildComponents(field string, val string) error { 316 if o.cluster == nil { 317 c, err := cluster.GetClusterByName(o.dynamic, o.Names[0], o.namespace) 318 if err != nil { 319 return err 320 } 321 o.cluster = c 322 } 323 324 switch field { 325 case "monitor": 326 return o.updateMonitor(val) 327 case "enable-all-logs": 328 return o.updateEnabledLog(val) 329 default: 330 return nil 331 } 332 } 333 334 func (o *updateOptions) buildBackup(field string, val string) error { 335 if o.cluster == nil { 336 c, err := cluster.GetClusterByName(o.dynamic, o.Names[0], o.namespace) 337 if err != nil { 338 return err 339 } 340 o.cluster = c 341 } 342 if o.cluster.Spec.Backup == nil { 343 o.cluster.Spec.Backup = &appsv1alpha1.ClusterBackup{} 344 } 345 346 switch field { 347 case "enabled": 348 return o.updateBackupEnabled(val) 349 case "retentionPeriod": 350 return o.updateBackupRetentionPeriod(val) 351 case "method": 352 return o.updateBackupMethod(val) 353 case "cronExpression": 354 return o.updateBackupCronExpression(val) 355 case "startingDeadlineMinutes": 356 return o.updateBackupStartingDeadlineMinutes(val) 357 case "repoName": 358 return o.updateBackupRepoName(val) 359 case "pitrEnabled": 360 return o.updateBackupPitrEnabled(val) 361 default: 362 return nil 363 } 364 } 365 366 func (o *updateOptions) updateEnabledLog(val string) error { 367 boolVal, err := strconv.ParseBool(val) 368 if err != nil { 369 return err 370 } 371 372 // update --enabled-all-logs=false for all components 373 if !boolVal { 374 for index := range o.cluster.Spec.ComponentSpecs { 375 o.cluster.Spec.ComponentSpecs[index].EnabledLogs = nil 376 } 377 return nil 378 } 379 380 // update --enabled-all-logs=true for all components 381 cd, err := cluster.GetClusterDefByName(o.dynamic, o.cluster.Spec.ClusterDefRef) 382 if err != nil { 383 return err 384 } 385 // set --enabled-all-logs at cluster components 386 setEnableAllLogs(o.cluster, cd) 387 if err = o.reconfigureLogVariables(o.cluster, cd); err != nil { 388 return errors.Wrap(err, "failed to reconfigure log variables of target cluster") 389 } 390 return nil 391 } 392 393 const logsBlockName = "logsBlock" 394 const logsTemplateName = "template-logs-block" 395 const topTPLLogsObject = "component" 396 const defaultSectionName = "default" 397 398 // reconfigureLogVariables reconfigures the log variables of cluster 399 func (o *updateOptions) reconfigureLogVariables(c *appsv1alpha1.Cluster, cd *appsv1alpha1.ClusterDefinition) error { 400 var ( 401 err error 402 configSpec *appsv1alpha1.ComponentConfigSpec 403 logValue *gotemplate.TplValues 404 ) 405 406 createReconfigureOps := func(compSpec appsv1alpha1.ClusterComponentSpec, configSpec *appsv1alpha1.ComponentConfigSpec, logValue *gotemplate.TplValues) error { 407 var ( 408 buf bytes.Buffer 409 keyName string 410 configTemplate *corev1.ConfigMap 411 formatter *appsv1alpha1.FormatterConfig 412 logTPL *template.Template 413 logVariables map[string]string 414 unstructuredObj *unstructured.Unstructured 415 ) 416 417 if configTemplate, formatter, err = findConfigTemplateInfo(o.dynamic, configSpec); err != nil { 418 return err 419 } 420 if keyName, logTPL, err = findLogsBlockTPL(configTemplate.Data); err != nil { 421 return err 422 } 423 if logTPL == nil { 424 return nil 425 } 426 if err = logTPL.Execute(&buf, logValue); err != nil { 427 return err 428 } 429 // TODO: very hack logic for ini config file 430 formatter.FormatterOptions = appsv1alpha1.FormatterOptions{IniConfig: &appsv1alpha1.IniConfig{SectionName: defaultSectionName}} 431 if logVariables, err = cfgcore.TransformConfigFileToKeyValueMap(keyName, formatter, buf.Bytes()); err != nil { 432 return err 433 } 434 // build OpsRequest and apply this OpsRequest 435 opsRequest := buildLogsReconfiguringOps(c.Name, c.Namespace, compSpec.Name, configSpec.Name, keyName, logVariables) 436 if unstructuredObj, err = util.ConvertObjToUnstructured(opsRequest); err != nil { 437 return err 438 } 439 return util.CreateResourceIfAbsent(o.dynamic, types.OpsGVR(), c.Namespace, unstructuredObj) 440 } 441 442 for _, compSpec := range c.Spec.ComponentSpecs { 443 if configSpec, err = findFirstConfigSpec(c.Spec.ComponentSpecs, cd.Spec.ComponentDefs, compSpec.Name); err != nil { 444 return err 445 } 446 if logValue, err = buildLogsTPLValues(&compSpec); err != nil { 447 return err 448 } 449 if err = createReconfigureOps(compSpec, configSpec, logValue); err != nil { 450 return err 451 } 452 } 453 return nil 454 } 455 456 func findFirstConfigSpec( 457 compSpecs []appsv1alpha1.ClusterComponentSpec, 458 cdCompSpecs []appsv1alpha1.ClusterComponentDefinition, 459 compName string) (*appsv1alpha1.ComponentConfigSpec, error) { 460 configSpecs, err := util.GetConfigTemplateListWithResource(compSpecs, cdCompSpecs, nil, compName, true) 461 if err != nil { 462 return nil, err 463 } 464 if len(configSpecs) == 0 { 465 return nil, errors.Errorf("no config templates for component %s", compName) 466 } 467 return &configSpecs[0], nil 468 } 469 470 func findConfigTemplateInfo(dynamic dynamic.Interface, configSpec *appsv1alpha1.ComponentConfigSpec) (*corev1.ConfigMap, *appsv1alpha1.FormatterConfig, error) { 471 if configSpec == nil { 472 return nil, nil, errors.New("configTemplateSpec is nil") 473 } 474 configTemplate, err := cluster.GetConfigMapByName(dynamic, configSpec.Namespace, configSpec.TemplateRef) 475 if err != nil { 476 return nil, nil, err 477 } 478 configConstraint, err := cluster.GetConfigConstraintByName(dynamic, configSpec.ConfigConstraintRef) 479 if err != nil { 480 return nil, nil, err 481 } 482 return configTemplate, configConstraint.Spec.FormatterConfig, nil 483 } 484 485 func newConfigTemplateEngine() *template.Template { 486 customizedFuncMap := configuration.BuiltInCustomFunctions(nil, nil, nil) 487 engine := gotemplate.NewTplEngine(nil, customizedFuncMap, logsTemplateName, nil, context.TODO()) 488 return engine.GetTplEngine() 489 } 490 491 func findLogsBlockTPL(confData map[string]string) (string, *template.Template, error) { 492 engine := newConfigTemplateEngine() 493 for key, value := range confData { 494 if !strings.Contains(value, logsBlockName) { 495 continue 496 } 497 tpl, err := engine.Parse(value) 498 if err != nil { 499 return key, nil, err 500 } 501 logTPL := tpl.Lookup(logsBlockName) 502 // find target logs template 503 if logTPL != nil { 504 return key, logTPL, nil 505 } 506 return "", nil, errors.New("no logs config template found") 507 } 508 return "", nil, nil 509 } 510 511 func buildLogsTPLValues(compSpec *appsv1alpha1.ClusterComponentSpec) (*gotemplate.TplValues, error) { 512 compMap := map[string]interface{}{} 513 bytesData, err := json.Marshal(compSpec) 514 if err != nil { 515 return nil, err 516 } 517 err = json.Unmarshal(bytesData, &compMap) 518 if err != nil { 519 return nil, err 520 } 521 value := gotemplate.TplValues{ 522 topTPLLogsObject: compMap, 523 } 524 return &value, nil 525 } 526 527 func buildLogsReconfiguringOps(clusterName, namespace, compName, configName, keyName string, variables map[string]string) *appsv1alpha1.OpsRequest { 528 opsName := fmt.Sprintf("%s-%s", "logs-reconfigure", uuid.NewString()) 529 opsRequest := util.NewOpsRequestForReconfiguring(opsName, namespace, clusterName) 530 parameterPairs := make([]appsv1alpha1.ParameterPair, 0, len(variables)) 531 for key, value := range variables { 532 v := value 533 parameterPairs = append(parameterPairs, appsv1alpha1.ParameterPair{ 534 Key: key, 535 Value: &v, 536 }) 537 } 538 var keys []appsv1alpha1.ParameterConfig 539 keys = append(keys, appsv1alpha1.ParameterConfig{ 540 Key: keyName, 541 Parameters: parameterPairs, 542 }) 543 var configurations []appsv1alpha1.ConfigurationItem 544 configurations = append(configurations, appsv1alpha1.ConfigurationItem{ 545 Keys: keys, 546 Name: configName, 547 }) 548 reconfigure := opsRequest.Spec.Reconfigure 549 reconfigure.ComponentName = compName 550 reconfigure.Configurations = append(reconfigure.Configurations, configurations...) 551 return opsRequest 552 } 553 554 func (o *updateOptions) updateMonitor(val string) error { 555 intVal, err := strconv.ParseInt(val, 10, 32) 556 if err != nil { 557 return err 558 } 559 560 for i := range o.cluster.Spec.ComponentSpecs { 561 o.cluster.Spec.ComponentSpecs[i].Monitor = intVal != 0 562 } 563 return nil 564 } 565 566 func (o *updateOptions) updateBackupEnabled(val string) error { 567 boolVal, err := strconv.ParseBool(val) 568 if err != nil { 569 return err 570 } 571 o.cluster.Spec.Backup.Enabled = &boolVal 572 return nil 573 } 574 575 func (o *updateOptions) updateBackupRetentionPeriod(val string) error { 576 // if val is empty, do nothing 577 if len(val) == 0 { 578 return nil 579 } 580 581 // judge whether val end with the 'd'|'h' character 582 lastChar := val[len(val)-1] 583 if lastChar != 'd' && lastChar != 'h' { 584 return fmt.Errorf("invalid retention period: %s, only support d|h", val) 585 } 586 587 o.cluster.Spec.Backup.RetentionPeriod = dpv1alpha1.RetentionPeriod(val) 588 return nil 589 } 590 591 func (o *updateOptions) updateBackupMethod(val string) error { 592 // TODO(ldm): validate backup method are defined in the backup policy. 593 o.cluster.Spec.Backup.Method = val 594 return nil 595 } 596 597 func (o *updateOptions) updateBackupCronExpression(val string) error { 598 // judge whether val is a valid cron expression 599 if _, err := cron.ParseStandard(val); err != nil { 600 return fmt.Errorf("invalid cron expression: %s, please see https://en.wikipedia.org/wiki/Cron", val) 601 } 602 603 o.cluster.Spec.Backup.CronExpression = val 604 return nil 605 } 606 607 func (o *updateOptions) updateBackupStartingDeadlineMinutes(val string) error { 608 intVal, err := strconv.ParseInt(val, 10, 64) 609 if err != nil { 610 return err 611 } 612 o.cluster.Spec.Backup.StartingDeadlineMinutes = &intVal 613 return nil 614 } 615 616 func (o *updateOptions) updateBackupRepoName(val string) error { 617 o.cluster.Spec.Backup.RepoName = val 618 return nil 619 } 620 621 func (o *updateOptions) updateBackupPitrEnabled(val string) error { 622 boolVal, err := strconv.ParseBool(val) 623 if err != nil { 624 return err 625 } 626 o.cluster.Spec.Backup.PITREnabled = &boolVal 627 return nil 628 }