github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/cluster/config_diff.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 "fmt" 24 "reflect" 25 26 "github.com/spf13/cast" 27 "github.com/spf13/cobra" 28 "k8s.io/cli-runtime/pkg/genericiooptions" 29 cmdutil "k8s.io/kubectl/pkg/cmd/util" 30 "k8s.io/kubectl/pkg/util/templates" 31 "sigs.k8s.io/controller-runtime/pkg/client" 32 33 appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1" 34 "github.com/1aal/kubeblocks/pkg/cli/printer" 35 "github.com/1aal/kubeblocks/pkg/cli/types" 36 "github.com/1aal/kubeblocks/pkg/cli/util" 37 "github.com/1aal/kubeblocks/pkg/configuration/core" 38 "github.com/1aal/kubeblocks/pkg/constant" 39 "github.com/1aal/kubeblocks/pkg/unstructured" 40 ) 41 42 type configDiffOptions struct { 43 baseOptions *describeOpsOptions 44 45 clusterName string 46 componentName string 47 templateNames []string 48 baseVersion *appsv1alpha1.OpsRequest 49 diffVersion *appsv1alpha1.OpsRequest 50 } 51 52 var ( 53 diffConfigureExample = templates.Examples(` 54 # compare config files 55 kbcli cluster diff-config opsrequest1 opsrequest2`) 56 ) 57 58 func (o *configDiffOptions) complete(args []string) error { 59 isValidReconfigureOps := func(ops *appsv1alpha1.OpsRequest) bool { 60 return ops.Spec.Type == appsv1alpha1.ReconfiguringType && ops.Spec.Reconfigure != nil 61 } 62 63 if len(args) != 2 { 64 return core.MakeError("missing opsrequest name") 65 } 66 67 if err := o.baseOptions.complete(args); err != nil { 68 return err 69 } 70 71 baseVersion := &appsv1alpha1.OpsRequest{} 72 diffVersion := &appsv1alpha1.OpsRequest{} 73 if err := util.GetResourceObjectFromGVR(types.OpsGVR(), client.ObjectKey{ 74 Namespace: o.baseOptions.namespace, 75 Name: args[0], 76 }, o.baseOptions.dynamic, baseVersion); err != nil { 77 return core.WrapError(err, "failed to get ops CR [%s]", args[0]) 78 } 79 if err := util.GetResourceObjectFromGVR(types.OpsGVR(), client.ObjectKey{ 80 Namespace: o.baseOptions.namespace, 81 Name: args[1], 82 }, o.baseOptions.dynamic, diffVersion); err != nil { 83 return core.WrapError(err, "failed to get ops CR [%s]", args[1]) 84 } 85 86 if !isValidReconfigureOps(baseVersion) { 87 return core.MakeError("opsrequest is not valid reconfiguring operation [%s]", client.ObjectKeyFromObject(baseVersion)) 88 } 89 90 if !isValidReconfigureOps(diffVersion) { 91 return core.MakeError("opsrequest is not valid reconfiguring operation [%s]", client.ObjectKeyFromObject(diffVersion)) 92 } 93 94 if !o.maybeCompareOps(baseVersion, diffVersion) { 95 return core.MakeError("failed to diff, not same cluster, or same component, or template.") 96 } 97 98 o.baseVersion = baseVersion 99 o.diffVersion = diffVersion 100 return nil 101 } 102 103 func findTemplateStatusByName(status *appsv1alpha1.ReconfiguringStatus, tplName string) *appsv1alpha1.ConfigurationItemStatus { 104 if status == nil { 105 return nil 106 } 107 108 for i := range status.ConfigurationStatus { 109 s := &status.ConfigurationStatus[i] 110 if s.Name == tplName { 111 return s 112 } 113 } 114 return nil 115 } 116 117 func (o *configDiffOptions) validate() error { 118 var ( 119 baseStatus = o.baseVersion.Status 120 diffStatus = o.diffVersion.Status 121 ) 122 123 if baseStatus.Phase != appsv1alpha1.OpsSucceedPhase { 124 return core.MakeError("require reconfiguring phase is success!, name: %s, phase: %s", o.baseVersion.Name, baseStatus.Phase) 125 } 126 if diffStatus.Phase != appsv1alpha1.OpsSucceedPhase { 127 return core.MakeError("require reconfiguring phase is success!, name: %s, phase: %s", o.diffVersion.Name, diffStatus.Phase) 128 } 129 130 for _, tplName := range o.templateNames { 131 s1 := findTemplateStatusByName(baseStatus.ReconfiguringStatus, tplName) 132 s2 := findTemplateStatusByName(diffStatus.ReconfiguringStatus, tplName) 133 if s1 == nil || len(s1.LastAppliedConfiguration) == 0 { 134 return core.MakeError("invalid reconfiguring status. CR[%v]", client.ObjectKeyFromObject(o.baseVersion)) 135 } 136 if s2 == nil || len(s2.LastAppliedConfiguration) == 0 { 137 return core.MakeError("invalid reconfiguring status. CR[%v]", client.ObjectKeyFromObject(o.diffVersion)) 138 } 139 } 140 return nil 141 } 142 143 func (o *configDiffOptions) run() error { 144 configDiffs := make(map[string][]core.VisualizedParam, len(o.templateNames)) 145 baseConfigs := make(map[string]map[string]unstructured.ConfigObject) 146 for _, tplName := range o.templateNames { 147 diff, baseObj, err := o.diffConfig(tplName) 148 if err != nil { 149 return err 150 } 151 configDiffs[tplName] = diff 152 baseConfigs[tplName] = baseObj 153 } 154 155 printer.PrintTitle("DIFF-CONFIG RESULT") 156 for tplName, diff := range configDiffs { 157 configObjects := baseConfigs[tplName] 158 for _, params := range diff { 159 printer.PrintLineWithTabSeparator( 160 printer.NewPair(" ConfigFile", printer.BoldYellow(params.Key)), 161 printer.NewPair("TemplateName", tplName), 162 printer.NewPair("ComponentName", o.componentName), 163 printer.NewPair("ClusterName", o.clusterName), 164 printer.NewPair("UpdateType", string(params.UpdateType)), 165 ) 166 fmt.Fprintf(o.baseOptions.Out, "\n") 167 tbl := printer.NewTablePrinter(o.baseOptions.Out) 168 tbl.SetHeader("ParameterName", o.baseVersion.Name, o.diffVersion.Name) 169 configObj := configObjects[params.Key] 170 for _, v := range params.Parameters { 171 baseValue := "null" 172 if configObj != nil { 173 baseValue = cast.ToString(configObj.Get(v.Key)) 174 } 175 tbl.AddRow(v.Key, baseValue, v.Value) 176 } 177 tbl.Print() 178 fmt.Fprintf(o.baseOptions.Out, "\n\n") 179 } 180 } 181 return nil 182 } 183 184 func (o *configDiffOptions) maybeCompareOps(base *appsv1alpha1.OpsRequest, diff *appsv1alpha1.OpsRequest) bool { 185 getClusterName := func(ops client.Object) string { 186 labels := ops.GetLabels() 187 if len(labels) == 0 { 188 return "" 189 } 190 return labels[constant.AppInstanceLabelKey] 191 } 192 getComponentName := func(ops appsv1alpha1.OpsRequestSpec) string { 193 return ops.Reconfigure.ComponentName 194 } 195 getTemplateName := func(ops appsv1alpha1.OpsRequestSpec) []string { 196 configs := ops.Reconfigure.Configurations 197 names := make([]string, len(configs)) 198 for i, config := range configs { 199 names[i] = config.Name 200 } 201 return names 202 } 203 204 clusterName := getClusterName(base) 205 if len(clusterName) == 0 || clusterName != getClusterName(diff) { 206 return false 207 } 208 componentName := getComponentName(base.Spec) 209 if len(componentName) == 0 || componentName != getComponentName(diff.Spec) { 210 return false 211 } 212 templateNames := getTemplateName(base.Spec) 213 if len(templateNames) == 0 || !reflect.DeepEqual(templateNames, getTemplateName(diff.Spec)) { 214 return false 215 } 216 217 o.clusterName = clusterName 218 o.componentName = componentName 219 o.templateNames = templateNames 220 return true 221 } 222 223 func (o *configDiffOptions) diffConfig(tplName string) ([]core.VisualizedParam, map[string]unstructured.ConfigObject, error) { 224 var ( 225 tpl *appsv1alpha1.ComponentConfigSpec 226 configConstraint = &appsv1alpha1.ConfigConstraint{} 227 ) 228 229 tplList, err := util.GetConfigTemplateList(o.clusterName, o.baseOptions.namespace, o.baseOptions.dynamic, o.componentName, true) 230 if err != nil { 231 return nil, nil, err 232 } 233 if tpl = findTplByName(tplList, tplName); tpl == nil { 234 return nil, nil, core.MakeError("not found template: %s", tplName) 235 } 236 if err := util.GetResourceObjectFromGVR(types.ConfigConstraintGVR(), client.ObjectKey{ 237 Namespace: "", 238 Name: tpl.ConfigConstraintRef, 239 }, o.baseOptions.dynamic, configConstraint); err != nil { 240 return nil, nil, err 241 } 242 243 formatCfg := configConstraint.Spec.FormatterConfig 244 245 base := findTemplateStatusByName(o.baseVersion.Status.ReconfiguringStatus, tplName) 246 diff := findTemplateStatusByName(o.diffVersion.Status.ReconfiguringStatus, tplName) 247 patch, _, err := core.CreateConfigPatch(base.LastAppliedConfiguration, diff.LastAppliedConfiguration, formatCfg.Format, tpl.Keys, false) 248 if err != nil { 249 return nil, nil, err 250 } 251 252 baseConfigObj, err := core.LoadRawConfigObject(base.LastAppliedConfiguration, formatCfg, tpl.Keys) 253 if err != nil { 254 return nil, nil, err 255 } 256 return core.GenerateVisualizedParamsList(patch, formatCfg, nil), baseConfigObj, nil 257 } 258 259 // NewDiffConfigureCmd shows the difference between two configuration version. 260 func NewDiffConfigureCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 261 o := &configDiffOptions{baseOptions: newDescribeOpsOptions(f, streams)} 262 cmd := &cobra.Command{ 263 Use: "diff-config", 264 Short: "Show the difference in parameters between the two submitted OpsRequest.", 265 Aliases: []string{"diff"}, 266 Example: diffConfigureExample, 267 ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.OpsGVR()), 268 Run: func(cmd *cobra.Command, args []string) { 269 util.CheckErr(o.complete(args)) 270 util.CheckErr(o.validate()) 271 util.CheckErr(o.run()) 272 }, 273 } 274 return cmd 275 }