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  }