github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/cluster/label.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 "encoding/json" 24 "fmt" 25 "strings" 26 27 jsonpatch "github.com/evanphx/json-patch" 28 "github.com/spf13/cobra" 29 "k8s.io/apimachinery/pkg/api/meta" 30 "k8s.io/apimachinery/pkg/runtime" 31 "k8s.io/apimachinery/pkg/runtime/schema" 32 ktypes "k8s.io/apimachinery/pkg/types" 33 "k8s.io/cli-runtime/pkg/genericiooptions" 34 "k8s.io/cli-runtime/pkg/resource" 35 cmdutil "k8s.io/kubectl/pkg/cmd/util" 36 "k8s.io/kubectl/pkg/util/templates" 37 38 "github.com/1aal/kubeblocks/pkg/cli/cluster" 39 "github.com/1aal/kubeblocks/pkg/cli/types" 40 "github.com/1aal/kubeblocks/pkg/cli/util" 41 ) 42 43 var ( 44 labelExample = templates.Examples(` 45 # list label for clusters with specified name 46 kbcli cluster label mycluster --list 47 48 # add label 'env' and value 'dev' for clusters with specified name 49 kbcli cluster label mycluster env=dev 50 51 # add label 'env' and value 'dev' for all clusters 52 kbcli cluster label env=dev --all 53 54 # add label 'env' and value 'dev' for the clusters that match the selector 55 kbcli cluster label env=dev -l type=mysql 56 57 # update cluster with the label 'env' with value 'test', overwriting any existing value 58 kbcli cluster label mycluster --overwrite env=test 59 60 # delete label env for clusters with specified name 61 kbcli cluster label mycluster env-`) 62 ) 63 64 type LabelOptions struct { 65 Factory cmdutil.Factory 66 GVR schema.GroupVersionResource 67 68 // Common user flags 69 overwrite bool 70 all bool 71 list bool 72 selector string 73 74 // results of arg parsing 75 resources []string 76 newLabels map[string]string 77 removeLabels []string 78 79 namespace string 80 enforceNamespace bool 81 dryRunStrategy cmdutil.DryRunStrategy 82 builder *resource.Builder 83 unstructuredClientForMapping func(mapping *meta.RESTMapping) (resource.RESTClient, error) 84 85 genericiooptions.IOStreams 86 } 87 88 func NewLabelOptions(f cmdutil.Factory, streams genericiooptions.IOStreams, gvr schema.GroupVersionResource) *LabelOptions { 89 return &LabelOptions{ 90 Factory: f, 91 GVR: gvr, 92 IOStreams: streams, 93 } 94 } 95 96 func NewLabelCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 97 o := NewLabelOptions(f, streams, types.ClusterGVR()) 98 cmd := &cobra.Command{ 99 Use: "label NAME", 100 Short: "Update the labels on cluster", 101 Example: labelExample, 102 ValidArgsFunction: util.ResourceNameCompletionFunc(f, o.GVR), 103 Run: func(cmd *cobra.Command, args []string) { 104 util.CheckErr(o.complete(cmd, args)) 105 util.CheckErr(o.validate()) 106 util.CheckErr(o.run()) 107 }, 108 } 109 110 cmd.Flags().BoolVar(&o.overwrite, "overwrite", o.overwrite, "If true, allow labels to be overwritten, otherwise reject label updates that overwrite existing labels.") 111 cmd.Flags().BoolVar(&o.all, "all", o.all, "Select all cluster") 112 cmd.Flags().BoolVar(&o.list, "list", o.list, "If true, display the labels of the clusters") 113 cmdutil.AddDryRunFlag(cmd) 114 cmdutil.AddLabelSelectorFlagVar(cmd, &o.selector) 115 116 return cmd 117 } 118 119 func (o *LabelOptions) complete(cmd *cobra.Command, args []string) error { 120 var err error 121 122 o.dryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) 123 if err != nil { 124 return err 125 } 126 127 // parse resources and labels 128 resources, labelArgs, err := cmdutil.GetResourcesAndPairs(args, "label") 129 if err != nil { 130 return err 131 } 132 o.resources = resources 133 o.newLabels, o.removeLabels, err = parseLabels(labelArgs) 134 if err != nil { 135 return err 136 } 137 138 o.namespace, o.enforceNamespace, err = o.Factory.ToRawKubeConfigLoader().Namespace() 139 if err != nil { 140 return nil 141 } 142 o.builder = o.Factory.NewBuilder() 143 o.unstructuredClientForMapping = o.Factory.UnstructuredClientForMapping 144 return nil 145 } 146 147 func (o *LabelOptions) validate() error { 148 if o.all && len(o.selector) > 0 { 149 return fmt.Errorf("cannot set --all and --selector at the same time") 150 } 151 152 if !o.all && len(o.selector) == 0 && len(o.resources) == 0 { 153 return fmt.Errorf("at least one cluster is required") 154 } 155 156 if len(o.newLabels) < 1 && len(o.removeLabels) < 1 && !o.list { 157 return fmt.Errorf("at least one label update is required") 158 } 159 return nil 160 } 161 162 func (o *LabelOptions) run() error { 163 r := o.builder. 164 Unstructured(). 165 NamespaceParam(o.namespace).DefaultNamespace(). 166 LabelSelector(o.selector). 167 ResourceTypeOrNameArgs(o.all, append([]string{util.GVRToString(o.GVR)}, o.resources...)...). 168 ContinueOnError(). 169 Latest(). 170 Flatten(). 171 Do() 172 173 if err := r.Err(); err != nil { 174 return err 175 } 176 177 infos, err := r.Infos() 178 if err != nil { 179 return err 180 } 181 182 if len(infos) == 0 { 183 return fmt.Errorf("no clusters found") 184 } 185 186 for _, info := range infos { 187 obj := info.Object 188 oldData, err := json.Marshal(obj) 189 if err != nil { 190 return err 191 } 192 193 if o.dryRunStrategy == cmdutil.DryRunClient || o.list { 194 err = labelFunc(obj, o.overwrite, o.newLabels, o.removeLabels) 195 if err != nil { 196 return err 197 } 198 } else { 199 name, namespace := info.Name, info.Namespace 200 if err != nil { 201 return err 202 } 203 accessor, err := meta.Accessor(obj) 204 if err != nil { 205 return err 206 } 207 for _, label := range o.removeLabels { 208 if _, ok := accessor.GetLabels()[label]; !ok { 209 fmt.Fprintf(o.Out, "label %q not found.\n", label) 210 } 211 } 212 213 if err := labelFunc(obj, o.overwrite, o.newLabels, o.removeLabels); err != nil { 214 return err 215 } 216 217 newObj, err := json.Marshal(obj) 218 if err != nil { 219 return err 220 } 221 patchBytes, err := jsonpatch.CreateMergePatch(oldData, newObj) 222 createPatch := err == nil 223 mapping := info.ResourceMapping() 224 client, err := o.unstructuredClientForMapping(mapping) 225 if err != nil { 226 return err 227 } 228 helper := resource.NewHelper(client, mapping). 229 DryRun(o.dryRunStrategy == cmdutil.DryRunServer) 230 if createPatch { 231 _, err = helper.Patch(namespace, name, ktypes.MergePatchType, patchBytes, nil) 232 } else { 233 _, err = helper.Replace(namespace, name, false, obj) 234 } 235 if err != nil { 236 return err 237 } 238 } 239 } 240 241 if o.list { 242 dynamic, err := o.Factory.DynamicClient() 243 if err != nil { 244 return err 245 } 246 247 client, err := o.Factory.KubernetesClientSet() 248 if err != nil { 249 return err 250 } 251 252 opt := &cluster.PrinterOptions{ 253 ShowLabels: true, 254 } 255 256 p := cluster.NewPrinter(o.IOStreams.Out, cluster.PrintLabels, opt) 257 for _, info := range infos { 258 if err = addRow(dynamic, client, info.Namespace, info.Name, p); err != nil { 259 return err 260 } 261 } 262 p.Print() 263 } 264 265 return nil 266 } 267 268 func parseLabels(spec []string) (map[string]string, []string, error) { 269 labels := map[string]string{} 270 var remove []string 271 for _, labelSpec := range spec { 272 switch { 273 case strings.Contains(labelSpec, "="): 274 parts := strings.Split(labelSpec, "=") 275 if len(parts) != 2 { 276 return nil, nil, fmt.Errorf("invalid label spec: %s", labelSpec) 277 } 278 labels[parts[0]] = parts[1] 279 case strings.HasSuffix(labelSpec, "-"): 280 remove = append(remove, labelSpec[:len(labelSpec)-1]) 281 default: 282 return nil, nil, fmt.Errorf("unknown label spec: %s", labelSpec) 283 } 284 } 285 for _, removeLabel := range remove { 286 if _, found := labels[removeLabel]; found { 287 return nil, nil, fmt.Errorf("cannot modify and remove label within the same command") 288 } 289 } 290 return labels, remove, nil 291 } 292 293 func validateNoOverwrites(obj runtime.Object, labels map[string]string) error { 294 accessor, err := meta.Accessor(obj) 295 if err != nil { 296 return err 297 } 298 299 objLabels := accessor.GetLabels() 300 if objLabels == nil { 301 return nil 302 } 303 304 for key := range labels { 305 if _, found := objLabels[key]; found { 306 return fmt.Errorf("'%s' already has a value (%s), and --overwrite is false", key, objLabels[key]) 307 } 308 } 309 return nil 310 } 311 312 func labelFunc(obj runtime.Object, overwrite bool, labels map[string]string, remove []string) error { 313 accessor, err := meta.Accessor(obj) 314 if err != nil { 315 return err 316 } 317 if !overwrite { 318 if err := validateNoOverwrites(obj, labels); err != nil { 319 return err 320 } 321 } 322 323 objLabels := accessor.GetLabels() 324 if objLabels == nil { 325 objLabels = make(map[string]string) 326 } 327 328 for key, value := range labels { 329 objLabels[key] = value 330 } 331 for _, label := range remove { 332 delete(objLabels, label) 333 } 334 accessor.SetLabels(objLabels) 335 336 return nil 337 }