github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/util/util.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 util
    21  
    22  import (
    23  	"context"
    24  	"crypto/rand"
    25  	"crypto/rsa"
    26  	"crypto/x509"
    27  	"encoding/json"
    28  	"encoding/pem"
    29  	"fmt"
    30  	"io"
    31  	"math"
    32  	mrand "math/rand"
    33  	"net/http"
    34  	"os"
    35  	"os/exec"
    36  	"path"
    37  	"path/filepath"
    38  	"runtime"
    39  	"sort"
    40  	"strings"
    41  	"sync"
    42  	"text/template"
    43  	"time"
    44  
    45  	"github.com/fatih/color"
    46  	"github.com/go-logr/logr"
    47  	"github.com/pkg/errors"
    48  	"github.com/pmezard/go-difflib/difflib"
    49  	"golang.org/x/crypto/ssh"
    50  	corev1 "k8s.io/api/core/v1"
    51  	apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    52  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    53  	"k8s.io/apimachinery/pkg/api/resource"
    54  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    55  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    56  	apiruntime "k8s.io/apimachinery/pkg/runtime"
    57  	"k8s.io/apimachinery/pkg/runtime/schema"
    58  	k8sapitypes "k8s.io/apimachinery/pkg/types"
    59  	"k8s.io/apimachinery/pkg/util/duration"
    60  	"k8s.io/apimachinery/pkg/util/sets"
    61  	"k8s.io/cli-runtime/pkg/genericclioptions"
    62  	"k8s.io/client-go/dynamic"
    63  	"k8s.io/client-go/kubernetes"
    64  	"k8s.io/client-go/kubernetes/scheme"
    65  	"k8s.io/client-go/rest"
    66  	"k8s.io/klog/v2"
    67  	cmdget "k8s.io/kubectl/pkg/cmd/get"
    68  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    69  	"sigs.k8s.io/controller-runtime/pkg/client"
    70  	"sigs.k8s.io/kustomize/kyaml/yaml"
    71  
    72  	appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    73  	"github.com/1aal/kubeblocks/pkg/cli/testing"
    74  	"github.com/1aal/kubeblocks/pkg/cli/types"
    75  	"github.com/1aal/kubeblocks/pkg/configuration/core"
    76  	"github.com/1aal/kubeblocks/pkg/configuration/openapi"
    77  	cfgutil "github.com/1aal/kubeblocks/pkg/configuration/util"
    78  	"github.com/1aal/kubeblocks/pkg/constant"
    79  	viper "github.com/1aal/kubeblocks/pkg/viperx"
    80  )
    81  
    82  // CloseQuietly closes `io.Closer` quietly. Very handy and helpful for code
    83  // quality too.
    84  func CloseQuietly(d io.Closer) {
    85  	_ = d.Close()
    86  }
    87  
    88  // GetCliHomeDir returns kbcli home dir
    89  func GetCliHomeDir() (string, error) {
    90  	var cliHome string
    91  	if custom := os.Getenv(types.CliHomeEnv); custom != "" {
    92  		cliHome = custom
    93  	} else {
    94  		home, err := os.UserHomeDir()
    95  		if err != nil {
    96  			return "", err
    97  		}
    98  		cliHome = filepath.Join(home, types.CliDefaultHome)
    99  	}
   100  	if _, err := os.Stat(cliHome); err != nil && os.IsNotExist(err) {
   101  		if err = os.MkdirAll(cliHome, 0750); err != nil {
   102  			return "", errors.Wrap(err, "error when create kbcli home directory")
   103  		}
   104  	}
   105  	return cliHome, nil
   106  }
   107  
   108  // GetKubeconfigDir returns the kubeconfig directory.
   109  func GetKubeconfigDir() string {
   110  	var kubeconfigDir string
   111  	switch runtime.GOOS {
   112  	case types.GoosDarwin, types.GoosLinux:
   113  		kubeconfigDir = filepath.Join(os.Getenv("HOME"), ".kube")
   114  	case types.GoosWindows:
   115  		kubeconfigDir = filepath.Join(os.Getenv("USERPROFILE"), ".kube")
   116  	}
   117  	return kubeconfigDir
   118  }
   119  
   120  func ConfigPath(name string) string {
   121  	if len(name) == 0 {
   122  		return ""
   123  	}
   124  
   125  	return filepath.Join(GetKubeconfigDir(), name)
   126  }
   127  
   128  func RemoveConfig(name string) error {
   129  	if err := os.Remove(ConfigPath(name)); err != nil {
   130  		return err
   131  	}
   132  	return nil
   133  }
   134  
   135  func GetPublicIP() (string, error) {
   136  	resp, err := http.Get("https://ifconfig.me")
   137  	if err != nil {
   138  		return "", err
   139  	}
   140  	defer resp.Body.Close()
   141  	body, err := io.ReadAll(resp.Body)
   142  	if err != nil {
   143  		return "", err
   144  	}
   145  	return string(body), nil
   146  }
   147  
   148  // MakeSSHKeyPair makes a pair of public and private keys for SSH access.
   149  // Public key is encoded in the format for inclusion in an OpenSSH authorized_keys file.
   150  // Private Key generated is PEM encoded
   151  func MakeSSHKeyPair(pubKeyPath, privateKeyPath string) error {
   152  	if err := os.MkdirAll(path.Dir(pubKeyPath), os.FileMode(0700)); err != nil {
   153  		return err
   154  	}
   155  	if err := os.MkdirAll(path.Dir(privateKeyPath), os.FileMode(0700)); err != nil {
   156  		return err
   157  	}
   158  	privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
   159  	if err != nil {
   160  		return err
   161  	}
   162  
   163  	// generate and write private key as PEM
   164  	privateKeyFile, err := os.OpenFile(privateKeyPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
   165  	if err != nil {
   166  		return err
   167  	}
   168  	defer privateKeyFile.Close()
   169  
   170  	privateKeyPEM := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}
   171  	if err := pem.Encode(privateKeyFile, privateKeyPEM); err != nil {
   172  		return err
   173  	}
   174  
   175  	// generate and write public key
   176  	pub, err := ssh.NewPublicKey(&privateKey.PublicKey)
   177  	if err != nil {
   178  		return err
   179  	}
   180  	return os.WriteFile(pubKeyPath, ssh.MarshalAuthorizedKey(pub), 0655)
   181  }
   182  
   183  func PrintObjYAML(obj *unstructured.Unstructured) error {
   184  	data, err := yaml.Marshal(obj)
   185  	if err != nil {
   186  		return err
   187  	}
   188  	fmt.Println(string(data))
   189  	return nil
   190  }
   191  
   192  type RetryOptions struct {
   193  	MaxRetry int
   194  	Delay    time.Duration
   195  }
   196  
   197  func DoWithRetry(ctx context.Context, logger logr.Logger, operation func() error, options *RetryOptions) error {
   198  	err := operation()
   199  	for attempt := 0; err != nil && attempt < options.MaxRetry; attempt++ {
   200  		delay := time.Duration(int(math.Pow(2, float64(attempt)))) * time.Second
   201  		if options.Delay != 0 {
   202  			delay = options.Delay
   203  		}
   204  		logger.Info(fmt.Sprintf("Failed, retrying in %s ... (%d/%d). Error: %v", delay, attempt+1, options.MaxRetry, err))
   205  		select {
   206  		case <-time.After(delay):
   207  		case <-ctx.Done():
   208  			return err
   209  		}
   210  		err = operation()
   211  	}
   212  	return err
   213  }
   214  
   215  func PrintGoTemplate(wr io.Writer, tpl string, values interface{}) error {
   216  	tmpl, err := template.New("output").Parse(tpl)
   217  	if err != nil {
   218  		return err
   219  	}
   220  
   221  	err = tmpl.Execute(wr, values)
   222  	if err != nil {
   223  		return err
   224  	}
   225  	return nil
   226  }
   227  
   228  // SetKubeConfig sets KUBECONFIG environment
   229  func SetKubeConfig(cfg string) error {
   230  	return os.Setenv("KUBECONFIG", cfg)
   231  }
   232  
   233  var addToScheme sync.Once
   234  
   235  func NewFactory() cmdutil.Factory {
   236  	configFlags := NewConfigFlagNoWarnings()
   237  	// Add CRDs to the scheme. They are missing by default.
   238  	addToScheme.Do(func() {
   239  		if err := apiextv1.AddToScheme(scheme.Scheme); err != nil {
   240  			// This should never happen.
   241  			panic(err)
   242  		}
   243  	})
   244  	return cmdutil.NewFactory(configFlags)
   245  }
   246  
   247  // NewConfigFlagNoWarnings returns a ConfigFlags that disables warnings.
   248  func NewConfigFlagNoWarnings() *genericclioptions.ConfigFlags {
   249  	configFlags := genericclioptions.NewConfigFlags(true)
   250  	configFlags.WrapConfigFn = func(c *rest.Config) *rest.Config {
   251  		c.WarningHandler = rest.NoWarnings{}
   252  		return c
   253  	}
   254  	return configFlags
   255  }
   256  
   257  func GVRToString(gvr schema.GroupVersionResource) string {
   258  	return strings.Join([]string{gvr.Resource, gvr.Version, gvr.Group}, ".")
   259  }
   260  
   261  // GetNodeByName chooses node by name from a node array
   262  func GetNodeByName(nodes []*corev1.Node, name string) *corev1.Node {
   263  	for _, node := range nodes {
   264  		if node.Name == name {
   265  			return node
   266  		}
   267  	}
   268  	return nil
   269  }
   270  
   271  // ResourceIsEmpty checks if resource is empty or not
   272  func ResourceIsEmpty(res *resource.Quantity) bool {
   273  	resStr := res.String()
   274  	if resStr == "0" || resStr == "<nil>" {
   275  		return true
   276  	}
   277  	return false
   278  }
   279  
   280  func GetPodStatus(pods []corev1.Pod) (running, waiting, succeeded, failed int) {
   281  	for _, pod := range pods {
   282  		switch pod.Status.Phase {
   283  		case corev1.PodRunning:
   284  			running++
   285  		case corev1.PodPending:
   286  			waiting++
   287  		case corev1.PodSucceeded:
   288  			succeeded++
   289  		case corev1.PodFailed:
   290  			failed++
   291  		}
   292  	}
   293  	return
   294  }
   295  
   296  // OpenBrowser opens browser with url in different OS system
   297  func OpenBrowser(url string) error {
   298  	var err error
   299  	switch runtime.GOOS {
   300  	case "linux":
   301  		err = exec.Command("xdg-open", url).Start()
   302  	case "windows":
   303  		err = exec.Command("cmd", "/C", "start", url).Run()
   304  	case "darwin":
   305  		err = exec.Command("open", url).Start()
   306  	default:
   307  		err = fmt.Errorf("unsupported platform")
   308  	}
   309  	return err
   310  }
   311  
   312  func TimeFormat(t *metav1.Time) string {
   313  	return TimeFormatWithDuration(t, time.Minute)
   314  }
   315  
   316  // TimeFormatWithDuration formats time with specified precision
   317  func TimeFormatWithDuration(t *metav1.Time, duration time.Duration) string {
   318  	if t == nil || t.IsZero() {
   319  		return ""
   320  	}
   321  	return TimeTimeFormatWithDuration(t.Time, duration)
   322  }
   323  
   324  func TimeTimeFormat(t time.Time) string {
   325  	const layout = "Jan 02,2006 15:04 UTC-0700"
   326  	return t.Format(layout)
   327  }
   328  
   329  func timeLayout(precision time.Duration) string {
   330  	layout := "Jan 02,2006 15:04 UTC-0700"
   331  	switch precision {
   332  	case time.Second:
   333  		layout = "Jan 02,2006 15:04:05 UTC-0700"
   334  	case time.Millisecond:
   335  		layout = "Jan 02,2006 15:04:05.000 UTC-0700"
   336  	}
   337  	return layout
   338  }
   339  
   340  func TimeTimeFormatWithDuration(t time.Time, precision time.Duration) string {
   341  	layout := timeLayout(precision)
   342  	return t.Format(layout)
   343  }
   344  
   345  func TimeParse(t string, precision time.Duration) (time.Time, error) {
   346  	layout := timeLayout(precision)
   347  	return time.Parse(layout, t)
   348  }
   349  
   350  // GetHumanReadableDuration returns a succinct representation of the provided startTime and endTime
   351  // with limited precision for consumption by humans.
   352  func GetHumanReadableDuration(startTime metav1.Time, endTime metav1.Time) string {
   353  	if startTime.IsZero() {
   354  		return "<Unknown>"
   355  	}
   356  	if endTime.IsZero() {
   357  		endTime = metav1.NewTime(time.Now())
   358  	}
   359  	d := endTime.Sub(startTime.Time)
   360  	// if the
   361  	if d < time.Second {
   362  		d = time.Second
   363  	}
   364  	return duration.HumanDuration(d)
   365  }
   366  
   367  // CheckEmpty checks if string is empty, if yes, returns <none> for displaying
   368  func CheckEmpty(str string) string {
   369  	if len(str) == 0 {
   370  		return types.None
   371  	}
   372  	return str
   373  }
   374  
   375  // BuildLabelSelectorByNames builds the label selector by instance names, the label selector is
   376  // like "instance-key in (name1, name2)"
   377  func BuildLabelSelectorByNames(selector string, names []string) string {
   378  	if len(names) == 0 {
   379  		return selector
   380  	}
   381  
   382  	label := fmt.Sprintf("%s in (%s)", constant.AppInstanceLabelKey, strings.Join(names, ","))
   383  	if len(selector) == 0 {
   384  		return label
   385  	} else {
   386  		return selector + "," + label
   387  	}
   388  }
   389  
   390  // SortEventsByLastTimestamp sorts events by lastTimestamp
   391  func SortEventsByLastTimestamp(events *corev1.EventList, eventType string) *[]apiruntime.Object {
   392  	objs := make([]apiruntime.Object, 0, len(events.Items))
   393  	for i, e := range events.Items {
   394  		if eventType != "" && e.Type != eventType {
   395  			continue
   396  		}
   397  		objs = append(objs, &events.Items[i])
   398  	}
   399  	sorter := cmdget.NewRuntimeSort("{.lastTimestamp}", objs)
   400  	sort.Sort(sorter)
   401  	return &objs
   402  }
   403  
   404  func GetEventTimeStr(e *corev1.Event) string {
   405  	t := &e.CreationTimestamp
   406  	if !e.LastTimestamp.Time.IsZero() {
   407  		t = &e.LastTimestamp
   408  	}
   409  	return TimeFormat(t)
   410  }
   411  
   412  func GetEventObject(e *corev1.Event) string {
   413  	kind := e.InvolvedObject.Kind
   414  	if kind == "Pod" {
   415  		kind = "Instance"
   416  	}
   417  	return fmt.Sprintf("%s/%s", kind, e.InvolvedObject.Name)
   418  }
   419  
   420  // GetConfigTemplateList returns ConfigTemplate list used by the component.
   421  func GetConfigTemplateList(clusterName string, namespace string, cli dynamic.Interface, componentName string, reloadTpl bool) ([]appsv1alpha1.ComponentConfigSpec, error) {
   422  	var (
   423  		clusterObj        = appsv1alpha1.Cluster{}
   424  		clusterDefObj     = appsv1alpha1.ClusterDefinition{}
   425  		clusterVersionObj = appsv1alpha1.ClusterVersion{}
   426  	)
   427  
   428  	clusterKey := client.ObjectKey{
   429  		Namespace: namespace,
   430  		Name:      clusterName,
   431  	}
   432  	if err := GetResourceObjectFromGVR(types.ClusterGVR(), clusterKey, cli, &clusterObj); err != nil {
   433  		return nil, err
   434  	}
   435  	clusterDefKey := client.ObjectKey{
   436  		Namespace: "",
   437  		Name:      clusterObj.Spec.ClusterDefRef,
   438  	}
   439  	if err := GetResourceObjectFromGVR(types.ClusterDefGVR(), clusterDefKey, cli, &clusterDefObj); err != nil {
   440  		return nil, err
   441  	}
   442  	clusterVerKey := client.ObjectKey{
   443  		Namespace: "",
   444  		Name:      clusterObj.Spec.ClusterVersionRef,
   445  	}
   446  	if clusterVerKey.Name != "" {
   447  		if err := GetResourceObjectFromGVR(types.ClusterVersionGVR(), clusterVerKey, cli, &clusterVersionObj); err != nil {
   448  			return nil, err
   449  		}
   450  	}
   451  	return GetConfigTemplateListWithResource(clusterObj.Spec.ComponentSpecs, clusterDefObj.Spec.ComponentDefs, clusterVersionObj.Spec.ComponentVersions, componentName, reloadTpl)
   452  }
   453  
   454  func GetConfigTemplateListWithResource(cComponents []appsv1alpha1.ClusterComponentSpec,
   455  	dComponents []appsv1alpha1.ClusterComponentDefinition,
   456  	vComponents []appsv1alpha1.ClusterComponentVersion,
   457  	componentName string,
   458  	reloadTpl bool) ([]appsv1alpha1.ComponentConfigSpec, error) {
   459  
   460  	configSpecs, err := core.GetConfigTemplatesFromComponent(cComponents, dComponents, vComponents, componentName)
   461  	if err != nil {
   462  		return nil, err
   463  	}
   464  	if !reloadTpl || len(configSpecs) == 1 {
   465  		return configSpecs, nil
   466  	}
   467  
   468  	validConfigSpecs := make([]appsv1alpha1.ComponentConfigSpec, 0, len(configSpecs))
   469  	for _, configSpec := range configSpecs {
   470  		if configSpec.ConfigConstraintRef != "" && configSpec.TemplateRef != "" {
   471  			validConfigSpecs = append(validConfigSpecs, configSpec)
   472  		}
   473  	}
   474  	return validConfigSpecs, nil
   475  }
   476  
   477  // GetResourceObjectFromGVR queries the resource object using GVR.
   478  func GetResourceObjectFromGVR(gvr schema.GroupVersionResource, key client.ObjectKey, client dynamic.Interface, k8sObj interface{}) error {
   479  	unstructuredObj, err := client.
   480  		Resource(gvr).
   481  		Namespace(key.Namespace).
   482  		Get(context.TODO(), key.Name, metav1.GetOptions{})
   483  	if err != nil {
   484  		return core.WrapError(err, "failed to get resource[%v]", key)
   485  	}
   486  	return apiruntime.DefaultUnstructuredConverter.FromUnstructured(unstructuredObj.Object, k8sObj)
   487  }
   488  
   489  // GetComponentsFromClusterName returns name of component.
   490  func GetComponentsFromClusterName(key client.ObjectKey, cli dynamic.Interface) ([]string, error) {
   491  	clusterObj := appsv1alpha1.Cluster{}
   492  	clusterDefObj := appsv1alpha1.ClusterDefinition{}
   493  	if err := GetResourceObjectFromGVR(types.ClusterGVR(), key, cli, &clusterObj); err != nil {
   494  		return nil, err
   495  	}
   496  
   497  	if err := GetResourceObjectFromGVR(types.ClusterDefGVR(), client.ObjectKey{
   498  		Namespace: "",
   499  		Name:      clusterObj.Spec.ClusterDefRef,
   500  	}, cli, &clusterDefObj); err != nil {
   501  		return nil, err
   502  	}
   503  
   504  	return GetComponentsFromResource(clusterObj.Spec.ComponentSpecs, &clusterDefObj)
   505  }
   506  
   507  // GetComponentsFromResource returns name of component.
   508  func GetComponentsFromResource(componentSpecs []appsv1alpha1.ClusterComponentSpec, clusterDefObj *appsv1alpha1.ClusterDefinition) ([]string, error) {
   509  	filter := func(component *appsv1alpha1.ClusterComponentDefinition) bool {
   510  		if component != nil && len(componentSpecs) == 1 {
   511  			return true
   512  		}
   513  		return enableReconfiguring(component)
   514  	}
   515  	componentNames := make([]string, 0, len(componentSpecs))
   516  	for _, component := range componentSpecs {
   517  		cdComponent := clusterDefObj.GetComponentDefByName(component.ComponentDefRef)
   518  		if filter(cdComponent) {
   519  			componentNames = append(componentNames, component.Name)
   520  		}
   521  	}
   522  	return componentNames, nil
   523  }
   524  
   525  func enableReconfiguring(component *appsv1alpha1.ClusterComponentDefinition) bool {
   526  	if component == nil {
   527  		return false
   528  	}
   529  	for _, tpl := range component.ConfigSpecs {
   530  		if len(tpl.ConfigConstraintRef) > 0 && len(tpl.TemplateRef) > 0 {
   531  			return true
   532  		}
   533  	}
   534  	return false
   535  }
   536  
   537  // IsSupportReconfigureParams checks whether all updated parameters belong to config template parameters.
   538  func IsSupportReconfigureParams(tpl appsv1alpha1.ComponentConfigSpec, values map[string]*string, cli dynamic.Interface) (bool, error) {
   539  	var (
   540  		err              error
   541  		configConstraint = appsv1alpha1.ConfigConstraint{}
   542  	)
   543  
   544  	if err := GetResourceObjectFromGVR(types.ConfigConstraintGVR(), client.ObjectKey{
   545  		Namespace: "",
   546  		Name:      tpl.ConfigConstraintRef,
   547  	}, cli, &configConstraint); err != nil {
   548  		return false, err
   549  	}
   550  
   551  	if configConstraint.Spec.ConfigurationSchema == nil {
   552  		return true, nil
   553  	}
   554  
   555  	schema := configConstraint.Spec.ConfigurationSchema.DeepCopy()
   556  	if schema.Schema == nil {
   557  		schema.Schema, err = openapi.GenerateOpenAPISchema(schema.CUE, configConstraint.Spec.CfgSchemaTopLevelName)
   558  		if err != nil {
   559  			return false, err
   560  		}
   561  		if schema.Schema == nil {
   562  			return true, nil
   563  		}
   564  	}
   565  
   566  	schemaSpec := schema.Schema.Properties["spec"]
   567  	for key := range values {
   568  		if _, ok := schemaSpec.Properties[key]; !ok {
   569  			return false, nil
   570  		}
   571  	}
   572  	return true, nil
   573  }
   574  
   575  func ValidateParametersModified(tpl *appsv1alpha1.ComponentConfigSpec, parameters sets.Set[string], cli dynamic.Interface) (err error) {
   576  	cc := appsv1alpha1.ConfigConstraint{}
   577  	ccKey := client.ObjectKey{
   578  		Namespace: "",
   579  		Name:      tpl.ConfigConstraintRef,
   580  	}
   581  	if err = GetResourceObjectFromGVR(types.ConfigConstraintGVR(), ccKey, cli, &cc); err != nil {
   582  		return
   583  	}
   584  	return ValidateParametersModified2(parameters, cc.Spec)
   585  }
   586  
   587  func ValidateParametersModified2(parameters sets.Set[string], cc appsv1alpha1.ConfigConstraintSpec) error {
   588  	if len(cc.ImmutableParameters) == 0 {
   589  		return nil
   590  	}
   591  
   592  	immutableParameters := sets.New(cc.ImmutableParameters...)
   593  	uniqueParameters := immutableParameters.Intersection(parameters)
   594  	if uniqueParameters.Len() == 0 {
   595  		return nil
   596  	}
   597  	return core.MakeError("parameter[%v] is immutable, cannot be modified!", cfgutil.ToSet(uniqueParameters).AsSlice())
   598  }
   599  
   600  func GetIPLocation() (string, error) {
   601  	client := &http.Client{Timeout: 10 * time.Second}
   602  	req, err := http.NewRequest("GET", "https://ifconfig.io/country_code", nil)
   603  	if err != nil {
   604  		return "", err
   605  	}
   606  	resp, err := client.Do(req)
   607  	if err != nil {
   608  		return "", err
   609  	}
   610  	defer resp.Body.Close()
   611  	location, err := io.ReadAll(resp.Body)
   612  	if len(location) == 0 || err != nil {
   613  		return "", err
   614  	}
   615  
   616  	// remove last "\n"
   617  	return string(location[:len(location)-1]), nil
   618  }
   619  
   620  // GetHelmChartRepoURL gets helm chart repo, chooses one from GitHub and GitLab based on the IP location
   621  func GetHelmChartRepoURL() string {
   622  	if types.KubeBlocksChartURL == testing.KubeBlocksChartURL {
   623  		return testing.KubeBlocksChartURL
   624  	}
   625  
   626  	// if helm repo url is specified by config or environment, use it
   627  	url := viper.GetString(types.CfgKeyHelmRepoURL)
   628  	if url != "" {
   629  		klog.V(1).Infof("Using helm repo url set by config or environment: %s", url)
   630  		return url
   631  	}
   632  
   633  	// if helm repo url is not specified, choose one from GitHub and GitLab based on the IP location
   634  	// if location is CN, or we can not get location, use GitLab helm chart repo
   635  	repo := types.KubeBlocksChartURL
   636  	location, _ := GetIPLocation()
   637  	if location == "CN" || location == "" {
   638  		repo = types.GitLabHelmChartRepo
   639  	}
   640  	klog.V(1).Infof("Using helm repo url: %s", repo)
   641  	return repo
   642  }
   643  
   644  // GetKubeBlocksNamespace gets namespace of KubeBlocks installation, infer namespace from helm secrets
   645  func GetKubeBlocksNamespace(client kubernetes.Interface) (string, error) {
   646  	secrets, err := client.CoreV1().Secrets(metav1.NamespaceAll).List(context.TODO(), metav1.ListOptions{LabelSelector: types.KubeBlocksHelmLabel})
   647  	// if KubeBlocks is upgraded, there will be multiple secrets
   648  	if err == nil && len(secrets.Items) >= 1 {
   649  		return secrets.Items[0].Namespace, nil
   650  	}
   651  	return "", errors.New("failed to get KubeBlocks installation namespace")
   652  }
   653  
   654  // GetKubeBlocksNamespaceByDynamic gets namespace of KubeBlocks installation, infer namespace from helm secrets
   655  func GetKubeBlocksNamespaceByDynamic(dynamic dynamic.Interface) (string, error) {
   656  	list, err := dynamic.Resource(types.SecretGVR()).List(context.TODO(), metav1.ListOptions{LabelSelector: types.KubeBlocksHelmLabel})
   657  	if err == nil && len(list.Items) >= 1 {
   658  		return list.Items[0].GetNamespace(), nil
   659  	}
   660  	return "", errors.New("failed to get KubeBlocks installation namespace")
   661  }
   662  
   663  type ExposeType string
   664  
   665  const (
   666  	ExposeToVPC      ExposeType = "vpc"
   667  	ExposeToInternet ExposeType = "internet"
   668  
   669  	EnableValue  string = "true"
   670  	DisableValue string = "false"
   671  )
   672  
   673  var ProviderExposeAnnotations = map[K8sProvider]map[ExposeType]map[string]string{
   674  	EKSProvider: {
   675  		ExposeToVPC: map[string]string{
   676  			"service.beta.kubernetes.io/aws-load-balancer-type":     "nlb",
   677  			"service.beta.kubernetes.io/aws-load-balancer-internal": "true",
   678  		},
   679  		ExposeToInternet: map[string]string{
   680  			"service.beta.kubernetes.io/aws-load-balancer-type":     "nlb",
   681  			"service.beta.kubernetes.io/aws-load-balancer-internal": "false",
   682  		},
   683  	},
   684  	GKEProvider: {
   685  		ExposeToVPC: map[string]string{
   686  			"networking.gke.io/load-balancer-type": "Internal",
   687  		},
   688  		ExposeToInternet: map[string]string{},
   689  	},
   690  	AKSProvider: {
   691  		ExposeToVPC: map[string]string{
   692  			"service.beta.kubernetes.io/azure-load-balancer-internal": "true",
   693  		},
   694  		ExposeToInternet: map[string]string{
   695  			"service.beta.kubernetes.io/azure-load-balancer-internal": "false",
   696  		},
   697  	},
   698  	ACKProvider: {
   699  		ExposeToVPC: map[string]string{
   700  			"service.beta.kubernetes.io/alibaba-cloud-loadbalancer-address-type": "intranet",
   701  		},
   702  		ExposeToInternet: map[string]string{
   703  			"service.beta.kubernetes.io/alibaba-cloud-loadbalancer-address-type": "internet",
   704  		},
   705  	},
   706  	// TKE VPC LoadBalancer needs the subnet id, it's difficult for KB to get it, so we just support the internet on TKE now.
   707  	// reference: https://cloud.tencent.com/document/product/457/45487
   708  	TKEProvider: {
   709  		ExposeToInternet: map[string]string{},
   710  	},
   711  }
   712  
   713  func GetExposeAnnotations(provider K8sProvider, exposeType ExposeType) (map[string]string, error) {
   714  	exposeAnnotations, ok := ProviderExposeAnnotations[provider]
   715  	if !ok {
   716  		return nil, fmt.Errorf("unsupported provider: %s", provider)
   717  	}
   718  	annotations, ok := exposeAnnotations[exposeType]
   719  	if !ok {
   720  		return nil, fmt.Errorf("unsupported expose type: %s on provider %s", exposeType, provider)
   721  	}
   722  	return annotations, nil
   723  }
   724  
   725  // BuildAddonReleaseName returns the release name of addon, its f
   726  func BuildAddonReleaseName(addon string) string {
   727  	return fmt.Sprintf("%s-%s", types.AddonReleasePrefix, addon)
   728  }
   729  
   730  // CombineLabels combines labels into a string
   731  func CombineLabels(labels map[string]string) string {
   732  	var labelStr []string
   733  	for k, v := range labels {
   734  		labelStr = append(labelStr, fmt.Sprintf("%s=%s", k, v))
   735  	}
   736  
   737  	// sort labelStr to make sure the order is stable
   738  	sort.Strings(labelStr)
   739  
   740  	return strings.Join(labelStr, ",")
   741  }
   742  
   743  func BuildComponentNameLabels(prefix string, names []string) string {
   744  	return buildLabelSelectors(prefix, constant.KBAppComponentLabelKey, names)
   745  }
   746  
   747  // buildLabelSelectors builds the label selector by given label key, the label selector is
   748  // like "label-key in (name1, name2)"
   749  func buildLabelSelectors(prefix string, key string, names []string) string {
   750  	if len(names) == 0 {
   751  		return prefix
   752  	}
   753  
   754  	label := fmt.Sprintf("%s in (%s)", key, strings.Join(names, ","))
   755  	if len(prefix) == 0 {
   756  		return label
   757  	} else {
   758  		return prefix + "," + label
   759  	}
   760  }
   761  
   762  // NewOpsRequestForReconfiguring returns a new common OpsRequest for Reconfiguring operation
   763  func NewOpsRequestForReconfiguring(opsName, namespace, clusterName string) *appsv1alpha1.OpsRequest {
   764  	return &appsv1alpha1.OpsRequest{
   765  		TypeMeta: metav1.TypeMeta{
   766  			APIVersion: fmt.Sprintf("%s/%s", types.AppsAPIGroup, types.AppsAPIVersion),
   767  			Kind:       types.KindOps,
   768  		},
   769  		ObjectMeta: metav1.ObjectMeta{
   770  			Name:      opsName,
   771  			Namespace: namespace,
   772  		},
   773  		Spec: appsv1alpha1.OpsRequestSpec{
   774  			ClusterRef:  clusterName,
   775  			Type:        appsv1alpha1.ReconfiguringType,
   776  			Reconfigure: &appsv1alpha1.Reconfigure{},
   777  		},
   778  	}
   779  }
   780  func ConvertObjToUnstructured(obj any) (*unstructured.Unstructured, error) {
   781  	var (
   782  		contentBytes    []byte
   783  		err             error
   784  		unstructuredObj = &unstructured.Unstructured{}
   785  	)
   786  
   787  	if contentBytes, err = json.Marshal(obj); err != nil {
   788  		return nil, err
   789  	}
   790  	if err = json.Unmarshal(contentBytes, unstructuredObj); err != nil {
   791  		return nil, err
   792  	}
   793  	return unstructuredObj, nil
   794  }
   795  
   796  func CreateResourceIfAbsent(
   797  	dynamic dynamic.Interface,
   798  	gvr schema.GroupVersionResource,
   799  	namespace string,
   800  	unstructuredObj *unstructured.Unstructured) error {
   801  	objectName, isFound, err := unstructured.NestedString(unstructuredObj.Object, "metadata", "name")
   802  	if !isFound || err != nil {
   803  		return err
   804  	}
   805  	objectByte, err := json.Marshal(unstructuredObj)
   806  	if err != nil {
   807  		return err
   808  	}
   809  	if _, err = dynamic.Resource(gvr).Namespace(namespace).Patch(
   810  		context.TODO(), objectName, k8sapitypes.MergePatchType,
   811  		objectByte, metav1.PatchOptions{}); err != nil {
   812  		if apierrors.IsNotFound(err) {
   813  			if _, err = dynamic.Resource(gvr).Namespace(namespace).Create(
   814  				context.TODO(), unstructuredObj, metav1.CreateOptions{}); err != nil {
   815  				return err
   816  			}
   817  		} else {
   818  			return err
   819  		}
   820  	}
   821  	return nil
   822  }
   823  
   824  func BuildClusterDefinitionRefLabel(prefix string, clusterDef []string) string {
   825  	return buildLabelSelectors(prefix, constant.AppNameLabelKey, clusterDef)
   826  }
   827  
   828  // IsWindows returns true if the kbcli runtime situation is windows
   829  func IsWindows() bool {
   830  	return runtime.GOOS == types.GoosWindows
   831  }
   832  
   833  func GetUnifiedDiffString(original, edited string, from, to string, contextLine int) (string, error) {
   834  	if contextLine <= 0 {
   835  		contextLine = 3
   836  	}
   837  	diff := difflib.UnifiedDiff{
   838  		A:        difflib.SplitLines(original),
   839  		B:        difflib.SplitLines(edited),
   840  		FromFile: from,
   841  		ToFile:   to,
   842  		Context:  contextLine,
   843  	}
   844  	return difflib.GetUnifiedDiffString(diff)
   845  }
   846  
   847  func DisplayDiffWithColor(out io.Writer, diffText string) {
   848  	for _, line := range difflib.SplitLines(diffText) {
   849  		switch {
   850  		case strings.HasPrefix(line, "---"), strings.HasPrefix(line, "+++"):
   851  			line = color.HiYellowString(line)
   852  		case strings.HasPrefix(line, "@@"):
   853  			line = color.HiBlueString(line)
   854  		case strings.HasPrefix(line, "-"):
   855  			line = color.RedString(line)
   856  		case strings.HasPrefix(line, "+"):
   857  			line = color.GreenString(line)
   858  		}
   859  		fmt.Fprint(out, line)
   860  	}
   861  }
   862  
   863  // BuildTolerations toleration format: key=value:effect or key:effect,
   864  func BuildTolerations(raw []string) ([]interface{}, error) {
   865  	tolerations := make([]interface{}, 0)
   866  	for _, tolerationRaw := range raw {
   867  		for _, entries := range strings.Split(tolerationRaw, ",") {
   868  			toleration := make(map[string]interface{})
   869  			parts := strings.Split(entries, ":")
   870  			if len(parts) != 2 {
   871  				return tolerations, fmt.Errorf("invalid toleration %s", entries)
   872  			}
   873  			toleration["effect"] = parts[1]
   874  
   875  			partsKV := strings.Split(parts[0], "=")
   876  			switch len(partsKV) {
   877  			case 1:
   878  				toleration["operator"] = "Exists"
   879  				toleration["key"] = partsKV[0]
   880  			case 2:
   881  				toleration["operator"] = "Equal"
   882  				toleration["key"] = partsKV[0]
   883  				toleration["value"] = partsKV[1]
   884  			default:
   885  				return tolerations, fmt.Errorf("invalid toleration %s", entries)
   886  			}
   887  			tolerations = append(tolerations, toleration)
   888  		}
   889  	}
   890  	return tolerations, nil
   891  }
   892  
   893  // BuildNodeAffinity build node affinity from node labels
   894  func BuildNodeAffinity(nodeLabels map[string]string) *corev1.NodeAffinity {
   895  	var nodeAffinity *corev1.NodeAffinity
   896  
   897  	var matchExpressions []corev1.NodeSelectorRequirement
   898  	for key, value := range nodeLabels {
   899  		values := strings.Split(value, ",")
   900  		matchExpressions = append(matchExpressions, corev1.NodeSelectorRequirement{
   901  			Key:      key,
   902  			Operator: corev1.NodeSelectorOpIn,
   903  			Values:   values,
   904  		})
   905  	}
   906  	if len(matchExpressions) > 0 {
   907  		nodeSelectorTerm := corev1.NodeSelectorTerm{
   908  			MatchExpressions: matchExpressions,
   909  		}
   910  		nodeAffinity = &corev1.NodeAffinity{
   911  			PreferredDuringSchedulingIgnoredDuringExecution: []corev1.PreferredSchedulingTerm{
   912  				{
   913  					Preference: nodeSelectorTerm,
   914  				},
   915  			},
   916  		}
   917  	}
   918  
   919  	return nodeAffinity
   920  }
   921  
   922  // BuildPodAntiAffinity build pod anti affinity from topology keys
   923  func BuildPodAntiAffinity(podAntiAffinityStrategy string, topologyKeys []string) *corev1.PodAntiAffinity {
   924  	var podAntiAffinity *corev1.PodAntiAffinity
   925  	var podAffinityTerms []corev1.PodAffinityTerm
   926  	for _, topologyKey := range topologyKeys {
   927  		podAffinityTerms = append(podAffinityTerms, corev1.PodAffinityTerm{
   928  			TopologyKey: topologyKey,
   929  		})
   930  	}
   931  	if podAntiAffinityStrategy == string(appsv1alpha1.Required) {
   932  		podAntiAffinity = &corev1.PodAntiAffinity{
   933  			RequiredDuringSchedulingIgnoredDuringExecution: podAffinityTerms,
   934  		}
   935  	} else {
   936  		var weightedPodAffinityTerms []corev1.WeightedPodAffinityTerm
   937  		for _, podAffinityTerm := range podAffinityTerms {
   938  			weightedPodAffinityTerms = append(weightedPodAffinityTerms, corev1.WeightedPodAffinityTerm{
   939  				Weight:          100,
   940  				PodAffinityTerm: podAffinityTerm,
   941  			})
   942  		}
   943  		podAntiAffinity = &corev1.PodAntiAffinity{
   944  			PreferredDuringSchedulingIgnoredDuringExecution: weightedPodAffinityTerms,
   945  		}
   946  	}
   947  
   948  	return podAntiAffinity
   949  }
   950  
   951  // AddDirToPath add a dir to the PATH environment variable
   952  func AddDirToPath(dir string) error {
   953  	if dir == "" {
   954  		return fmt.Errorf("can't put empty dir into PATH")
   955  	}
   956  	p := strings.TrimSpace(os.Getenv("PATH"))
   957  	dir = strings.TrimSpace(dir)
   958  	if p == "" {
   959  		p = dir
   960  	} else {
   961  		p = dir + ":" + p
   962  	}
   963  	return os.Setenv("PATH", p)
   964  }
   965  
   966  func ListResourceByGVR(ctx context.Context, client dynamic.Interface, namespace string, gvrs []schema.GroupVersionResource, selector []metav1.ListOptions, allErrs *[]error) []*unstructured.UnstructuredList {
   967  	unstructuredList := make([]*unstructured.UnstructuredList, 0)
   968  	for _, gvr := range gvrs {
   969  		for _, labelSelector := range selector {
   970  			klog.V(1).Infof("listResourceByGVR: namespace=%s, gvr=%v, selector=%v", namespace, gvr, labelSelector)
   971  			resource, err := client.Resource(gvr).Namespace(namespace).List(ctx, labelSelector)
   972  			if err != nil {
   973  				AppendErrIgnoreNotFound(allErrs, err)
   974  				continue
   975  			}
   976  			unstructuredList = append(unstructuredList, resource)
   977  		}
   978  	}
   979  	return unstructuredList
   980  }
   981  
   982  func AppendErrIgnoreNotFound(allErrs *[]error, err error) {
   983  	if err == nil || apierrors.IsNotFound(err) {
   984  		return
   985  	}
   986  	*allErrs = append(*allErrs, err)
   987  }
   988  
   989  func WritePogStreamingLog(ctx context.Context, client kubernetes.Interface, pod *corev1.Pod, logOptions corev1.PodLogOptions, writer io.Writer) error {
   990  	request := client.CoreV1().Pods(pod.Namespace).GetLogs(pod.Name, &logOptions)
   991  	if data, err := request.DoRaw(ctx); err != nil {
   992  		return err
   993  	} else {
   994  		_, err := writer.Write(data)
   995  		return err
   996  	}
   997  }
   998  
   999  // RandRFC1123String generate a random string with length n, which fulfills RFC1123
  1000  func RandRFC1123String(n int) string {
  1001  	var letters = []rune("abcdefghijklmnopqrstuvwxyz0123456789")
  1002  	b := make([]rune, n)
  1003  	for i := range b {
  1004  		b[i] = letters[mrand.Intn(len(letters))]
  1005  	}
  1006  	return string(b)
  1007  }