github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/util/flags/flags.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 flags
    21  
    22  import (
    23  	"context"
    24  	"fmt"
    25  	"strings"
    26  
    27  	"github.com/spf13/cobra"
    28  	"github.com/stoewer/go-strcase"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/runtime"
    31  	"k8s.io/client-go/dynamic"
    32  	"k8s.io/kube-openapi/pkg/validation/spec"
    33  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    34  	utilcomp "k8s.io/kubectl/pkg/util/completion"
    35  
    36  	appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    37  	"github.com/1aal/kubeblocks/pkg/cli/types"
    38  	"github.com/1aal/kubeblocks/pkg/cli/util"
    39  )
    40  
    41  const (
    42  	CobraInt          = "int"
    43  	CobraSting        = "string"
    44  	CobraBool         = "bool"
    45  	CobraFloat64      = "float64"
    46  	CobraStringArray  = "stringArray"
    47  	CobraIntSlice     = "intSlice"
    48  	CobraFloat64Slice = "float64Slice"
    49  	CobraBoolSlice    = "boolSlice"
    50  )
    51  
    52  const (
    53  	typeString  = "string"
    54  	typeNumber  = "number"
    55  	typeInteger = "integer"
    56  	typeBoolean = "boolean"
    57  	typeObject  = "object"
    58  	typeArray   = "array"
    59  )
    60  
    61  // AddClusterDefinitionFlag adds a flag "cluster-definition" for the cmd and stores the value of the flag
    62  // in string p
    63  func AddClusterDefinitionFlag(f cmdutil.Factory, cmd *cobra.Command, p *string) {
    64  	cmd.Flags().StringVar(p, "cluster-definition", *p, "Specify cluster definition, run \"kbcli clusterdefinition list\" to show all available cluster definition")
    65  	util.CheckErr(cmd.RegisterFlagCompletionFunc("cluster-definition",
    66  		func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
    67  			return utilcomp.CompGetResource(f, util.GVRToString(types.ClusterDefGVR()), toComplete), cobra.ShellCompDirectiveNoFileComp
    68  		}))
    69  }
    70  
    71  // BuildFlagsBySchema builds a flag.FlagSet by the given schema, convert the schema key
    72  // to flag name, and convert the schema type to flag type.
    73  func BuildFlagsBySchema(cmd *cobra.Command, schema *spec.Schema) error {
    74  	if schema == nil {
    75  		return nil
    76  	}
    77  
    78  	for name, prop := range schema.Properties {
    79  		if err := buildOneFlag(cmd, name, &prop, false); err != nil {
    80  			return err
    81  		}
    82  	}
    83  
    84  	for _, name := range schema.Required {
    85  		flagName := strcase.KebabCase(name)
    86  		// fixme: array/object type flag will be "--servers.name", but in schema.Required will check the "--servers" flag, do not mark the array type as required in schema.json
    87  		if schema.Properties[name].Type[0] == typeObject || schema.Properties[name].Type[0] == typeArray {
    88  			continue
    89  		}
    90  		if err := cmd.MarkFlagRequired(flagName); err != nil {
    91  			return err
    92  		}
    93  	}
    94  
    95  	return nil
    96  }
    97  
    98  // castOrZero is a helper function to cast a value to a specific type.
    99  // If the cast fails, the zero value will be returned.
   100  func castOrZero[T any](v any) T {
   101  	cv, _ := v.(T)
   102  	return cv
   103  }
   104  
   105  func buildOneFlag(cmd *cobra.Command, k string, s *spec.Schema, isArray bool) error {
   106  	name := strcase.KebabCase(k)
   107  	tpe := typeString
   108  	if len(s.Type) > 0 {
   109  		tpe = s.Type[0]
   110  	}
   111  
   112  	if isArray {
   113  		switch tpe {
   114  		case typeString:
   115  			cmd.Flags().StringArray(name, []string{castOrZero[string](s.Default)}, buildFlagDescription(s))
   116  		case typeInteger:
   117  			cmd.Flags().IntSlice(name, []int{castOrZero[int](s.Default)}, buildFlagDescription(s))
   118  		case typeNumber:
   119  			cmd.Flags().Float64Slice(name, []float64{castOrZero[float64](s.Default)}, buildFlagDescription(s))
   120  		case typeBoolean:
   121  			cmd.Flags().BoolSlice(name, []bool{castOrZero[bool](s.Default)}, buildFlagDescription(s))
   122  		case typeObject:
   123  			for subName, prop := range s.Properties {
   124  				if err := buildOneFlag(cmd, fmt.Sprintf("%s.%s", name, subName), &prop, true); err != nil {
   125  					return err
   126  				}
   127  			}
   128  		case typeArray:
   129  			return fmt.Errorf("unsupported build flags for object with array nested within an array")
   130  		default:
   131  			return fmt.Errorf("unsupported json schema type %s", s.Type)
   132  		}
   133  
   134  		registerFlagCompFunc(cmd, name, s)
   135  	} else {
   136  		switch tpe {
   137  		case typeString:
   138  			cmd.Flags().String(name, castOrZero[string](s.Default), buildFlagDescription(s))
   139  		case typeInteger:
   140  			cmd.Flags().Int(name, int(castOrZero[float64](s.Default)), buildFlagDescription(s))
   141  		case typeNumber:
   142  			cmd.Flags().Float64(name, castOrZero[float64](s.Default), buildFlagDescription(s))
   143  		case typeBoolean:
   144  			cmd.Flags().Bool(name, castOrZero[bool](s.Default), buildFlagDescription(s))
   145  		case typeObject:
   146  			for subName, prop := range s.Properties {
   147  				if err := buildOneFlag(cmd, fmt.Sprintf("%s.%s", name, subName), &prop, false); err != nil {
   148  					return err
   149  				}
   150  			}
   151  		case typeArray:
   152  			if err := buildOneFlag(cmd, name, s.Items.Schema, true); err != nil {
   153  				return err
   154  			}
   155  		default:
   156  			return fmt.Errorf("unsupported json schema type %s", s.Type)
   157  		}
   158  
   159  		registerFlagCompFunc(cmd, name, s)
   160  	}
   161  	// flag not support array type
   162  
   163  	return nil
   164  }
   165  
   166  func buildFlagDescription(s *spec.Schema) string {
   167  	desc := strings.Builder{}
   168  	desc.WriteString(s.Description)
   169  
   170  	var legalVals []string
   171  	for _, e := range s.Enum {
   172  		legalVals = append(legalVals, fmt.Sprintf("%v", e))
   173  	}
   174  	if len(legalVals) > 0 {
   175  		desc.WriteString(fmt.Sprintf(" Legal values [%s].", strings.Join(legalVals, ", ")))
   176  	}
   177  
   178  	var valueRange []string
   179  	if s.Minimum != nil {
   180  		valueRange = append(valueRange, fmt.Sprintf("%v", *s.Minimum))
   181  	}
   182  	if s.Maximum != nil {
   183  		valueRange = append(valueRange, fmt.Sprintf("%v", *s.Maximum))
   184  	}
   185  	if len(valueRange) > 0 {
   186  		desc.WriteString(fmt.Sprintf(" Value range [%s].", strings.Join(valueRange, ", ")))
   187  	}
   188  	return desc.String()
   189  }
   190  
   191  func registerFlagCompFunc(cmd *cobra.Command, name string, s *spec.Schema) {
   192  	// register the enum entry for autocompletion
   193  	if len(s.Enum) > 0 {
   194  		var entries []string
   195  		for _, e := range s.Enum {
   196  			entries = append(entries, fmt.Sprintf("%s\t", e))
   197  		}
   198  		_ = cmd.RegisterFlagCompletionFunc(name,
   199  			func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   200  				return entries, cobra.ShellCompDirectiveNoFileComp
   201  			})
   202  		return
   203  	}
   204  }
   205  
   206  // AddComponentFlag add flag "component" for cobra.Command and support auto complete for it
   207  func AddComponentFlag(f cmdutil.Factory, cmd *cobra.Command, p *string, usage string) {
   208  	cmd.Flags().StringVar(p, "component", "", usage)
   209  	util.CheckErr(autoCompleteClusterComponent(cmd, f, "component"))
   210  }
   211  
   212  // AddComponentsFlag add flag "components" for cobra.Command and support auto complete for it
   213  func AddComponentsFlag(f cmdutil.Factory, cmd *cobra.Command, p *[]string, usage string) {
   214  	cmd.Flags().StringSliceVar(p, "components", nil, usage)
   215  	util.CheckErr(autoCompleteClusterComponent(cmd, f, "components"))
   216  }
   217  
   218  func autoCompleteClusterComponent(cmd *cobra.Command, f cmdutil.Factory, flag string) error {
   219  	// getClusterByNames is a hack way to eliminate circular import, combining functions from the cluster.GetK8SClientObject and cluster.GetClusterByName
   220  	getClusterByName := func(dynamic dynamic.Interface, name string, namespace string) (*appsv1alpha1.Cluster, error) {
   221  		cluster := &appsv1alpha1.Cluster{}
   222  
   223  		unstructuredObj, err := dynamic.Resource(types.ClusterGVR()).Namespace(namespace).Get(context.TODO(), name, metav1.GetOptions{})
   224  		if err != nil {
   225  			return nil, err
   226  		}
   227  		if err = runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredObj.UnstructuredContent(), cluster); err != nil {
   228  			return nil, err
   229  		}
   230  		return cluster, nil
   231  	}
   232  
   233  	autoComplete := func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   234  		var components []string
   235  		if len(args) == 0 {
   236  			return components, cobra.ShellCompDirectiveNoFileComp
   237  		}
   238  		namespace, _, _ := f.ToRawKubeConfigLoader().Namespace()
   239  		dynamic, _ := f.DynamicClient()
   240  		cluster, _ := getClusterByName(dynamic, args[0], namespace)
   241  		for _, comp := range cluster.Spec.ComponentSpecs {
   242  			if strings.HasPrefix(comp.Name, toComplete) {
   243  				components = append(components, comp.Name)
   244  			}
   245  		}
   246  		return components, cobra.ShellCompDirectiveNoFileComp
   247  	}
   248  	return cmd.RegisterFlagCompletionFunc(flag, autoComplete)
   249  
   250  }