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 }