github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/kubeblocks/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 kubeblocks
    21  
    22  import (
    23  	"context"
    24  	"fmt"
    25  	"io"
    26  	"sort"
    27  	"strings"
    28  
    29  	"github.com/Masterminds/semver/v3"
    30  	"github.com/jedib0t/go-pretty/v6/table"
    31  	"github.com/pkg/errors"
    32  	"golang.org/x/exp/slices"
    33  	"helm.sh/helm/v3/pkg/repo"
    34  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    35  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    36  	"k8s.io/apimachinery/pkg/runtime/schema"
    37  	"k8s.io/client-go/kubernetes"
    38  
    39  	extensionsv1alpha1 "github.com/1aal/kubeblocks/apis/extensions/v1alpha1"
    40  	"github.com/1aal/kubeblocks/pkg/cli/printer"
    41  	"github.com/1aal/kubeblocks/pkg/cli/types"
    42  	"github.com/1aal/kubeblocks/pkg/cli/util"
    43  	"github.com/1aal/kubeblocks/pkg/cli/util/helm"
    44  	"github.com/1aal/kubeblocks/pkg/cli/util/prompt"
    45  	"github.com/1aal/kubeblocks/pkg/constant"
    46  )
    47  
    48  func getGVRByCRD(crd *unstructured.Unstructured) (*schema.GroupVersionResource, error) {
    49  	group, _, err := unstructured.NestedString(crd.Object, "spec", "group")
    50  	if err != nil {
    51  		return nil, nil
    52  	}
    53  	return &schema.GroupVersionResource{
    54  		Group:    group,
    55  		Version:  types.AppsAPIVersion,
    56  		Resource: strings.Split(crd.GetName(), ".")[0],
    57  	}, nil
    58  }
    59  
    60  // check if KubeBlocks has been installed
    61  func checkIfKubeBlocksInstalled(client kubernetes.Interface) (bool, string, error) {
    62  	kbDeploys, err := client.AppsV1().Deployments(metav1.NamespaceAll).List(context.TODO(),
    63  		metav1.ListOptions{LabelSelector: "app.kubernetes.io/name=" + types.KubeBlocksChartName})
    64  	if err != nil {
    65  		return false, "", err
    66  	}
    67  
    68  	if len(kbDeploys.Items) == 0 {
    69  		return false, "", nil
    70  	}
    71  
    72  	var versions []string
    73  	for _, deploy := range kbDeploys.Items {
    74  		labels := deploy.GetLabels()
    75  		if labels == nil {
    76  			continue
    77  		}
    78  		if v, ok := labels["app.kubernetes.io/version"]; ok {
    79  			versions = append(versions, v)
    80  		}
    81  	}
    82  	return true, strings.Join(versions, " "), nil
    83  }
    84  
    85  func confirmUninstall(in io.Reader) error {
    86  	const confirmStr = "uninstall-kubeblocks"
    87  	_, err := prompt.NewPrompt(fmt.Sprintf("Please type \"%s\" to confirm:", confirmStr),
    88  		func(input string) error {
    89  			if input != confirmStr {
    90  				return fmt.Errorf("typed \"%s\" does not match \"%s\"", input, confirmStr)
    91  			}
    92  			return nil
    93  		}, in).Run()
    94  	return err
    95  }
    96  
    97  func getHelmChartVersions(chart string) ([]*semver.Version, error) {
    98  	errMsg := "failed to find the chart version"
    99  	// add repo, if exists, will update it
   100  	if err := helm.AddRepo(newHelmRepoEntry()); err != nil {
   101  		return nil, errors.Wrap(err, errMsg)
   102  	}
   103  
   104  	// get chart versions
   105  	versions, err := helm.GetChartVersions(chart)
   106  	if err != nil {
   107  		return nil, errors.Wrap(err, errMsg)
   108  	}
   109  	return versions, nil
   110  }
   111  
   112  // buildResourceLabelSelectors builds labelSelectors that can be used to get all
   113  // KubeBlocks resources and addons resources.
   114  // KubeBlocks has two types of resources: KubeBlocks resources and addon resources,
   115  // KubeBlocks resources are created by KubeBlocks itself, and addon resources are
   116  // created by addons.
   117  //
   118  // KubeBlocks resources are labeled with "app.kubernetes.io/instance=types.KubeBlocksChartName",
   119  // and most addon resources are labeled with "app.kubernetes.io/instance=<addon-prefix>-addon.Name",
   120  // but some addon resources are labeled with "release=<addon-prefix>-addon.Name".
   121  func buildResourceLabelSelectors(addons []*extensionsv1alpha1.Addon) []string {
   122  	var (
   123  		selectors []string
   124  		releases  []string
   125  		instances = []string{types.KubeBlocksChartName}
   126  	)
   127  
   128  	// releaseLabelAddons is a list of addons that use "release" label to label its resources
   129  	// TODO: use a better way to avoid hard code, maybe add unified label to all addons
   130  	releaseLabelAddons := []string{"prometheus"}
   131  	for _, addon := range addons {
   132  		addonReleaseName := fmt.Sprintf("%s-%s", types.AddonReleasePrefix, addon.Name)
   133  		if slices.Contains(releaseLabelAddons, addon.Name) {
   134  			releases = append(releases, addonReleaseName)
   135  		} else {
   136  			instances = append(instances, addonReleaseName)
   137  		}
   138  	}
   139  
   140  	selectors = append(selectors, util.BuildLabelSelectorByNames("", instances))
   141  	if len(releases) > 0 {
   142  		selectors = append(selectors, fmt.Sprintf("release in (%s)", strings.Join(releases, ",")))
   143  	}
   144  	return selectors
   145  }
   146  
   147  // buildAddonLabelSelector builds labelSelector that can be used to get all kubeBlocks resources,
   148  // including CRDs, addons (but not resources created by addons).
   149  // and it should be consistent with the labelSelectors defined in chart.
   150  // for example:
   151  // {{- define "kubeblocks.selectorLabels" -}}
   152  // app.kubernetes.io/name: {{ include "kubeblocks.name" . }}
   153  // app.kubernetes.io/instance: {{ .Release.Name }}
   154  // {{- end }}
   155  func buildKubeBlocksSelectorLabels() string {
   156  	return fmt.Sprintf("%s=%s,%s=%s",
   157  		constant.AppInstanceLabelKey, types.KubeBlocksReleaseName,
   158  		constant.AppNameLabelKey, types.KubeBlocksChartName)
   159  }
   160  
   161  // buildConfig builds labelSelector that can be used to get all configmaps that are used to store config templates.
   162  // and it should be consistent with the labelSelectors defined
   163  // in `configuration.updateConfigMapFinalizerImpl`.
   164  func buildConfigTypeSelectorLabels() string {
   165  	return fmt.Sprintf("%s=%s", constant.CMConfigurationTypeLabelKey, constant.ConfigTemplateType)
   166  }
   167  
   168  // printAddonMsg prints addon message when has failed addon or timeouts
   169  func printAddonMsg(out io.Writer, addons []*extensionsv1alpha1.Addon, install bool) {
   170  	var (
   171  		enablingAddons  []string
   172  		disablingAddons []string
   173  		failedAddons    []*extensionsv1alpha1.Addon
   174  	)
   175  
   176  	for _, addon := range addons {
   177  		switch addon.Status.Phase {
   178  		case extensionsv1alpha1.AddonEnabling:
   179  			enablingAddons = append(enablingAddons, addon.Name)
   180  		case extensionsv1alpha1.AddonDisabling:
   181  			disablingAddons = append(disablingAddons, addon.Name)
   182  		case extensionsv1alpha1.AddonFailed:
   183  			for _, c := range addon.Status.Conditions {
   184  				if c.Status == metav1.ConditionFalse {
   185  					failedAddons = append(failedAddons, addon)
   186  					break
   187  				}
   188  			}
   189  		}
   190  	}
   191  
   192  	// print failed addon messages
   193  	if len(failedAddons) > 0 {
   194  		printFailedAddonMsg(out, failedAddons)
   195  	}
   196  
   197  	// print enabling addon messages
   198  	if install && len(enablingAddons) > 0 {
   199  		fmt.Fprintf(out, "\nEnabling addons: %s\n", strings.Join(enablingAddons, ", "))
   200  		fmt.Fprintf(out, "Please wait for a while and try to run \"kbcli addon list\" to check addons status.\n")
   201  	}
   202  
   203  	if !install && len(disablingAddons) > 0 {
   204  		fmt.Fprintf(out, "\nDisabling addons: %s\n", strings.Join(disablingAddons, ", "))
   205  		fmt.Fprintf(out, "Please wait for a while and try to run \"kbcli addon list\" to check addons status.\n")
   206  	}
   207  }
   208  
   209  func printFailedAddonMsg(out io.Writer, addons []*extensionsv1alpha1.Addon) {
   210  	fmt.Fprintf(out, "\nFailed addons:\n")
   211  	tbl := printer.NewTablePrinter(out)
   212  	tbl.Tbl.SetColumnConfigs([]table.ColumnConfig{
   213  		{Number: 4, WidthMax: 120},
   214  	})
   215  	tbl.SetHeader("NAME", "TIME", "REASON", "MESSAGE")
   216  	for _, addon := range addons {
   217  		var times, reasons, messages []string
   218  		for _, c := range addon.Status.Conditions {
   219  			if c.Status != metav1.ConditionFalse {
   220  				continue
   221  			}
   222  			times = append(times, util.TimeFormat(&c.LastTransitionTime))
   223  			reasons = append(reasons, c.Reason)
   224  			messages = append(messages, c.Message)
   225  		}
   226  		tbl.AddRow(addon.Name, strings.Join(times, "\n"), strings.Join(reasons, "\n"), strings.Join(messages, "\n"))
   227  	}
   228  	tbl.Print()
   229  }
   230  
   231  func checkAddons(addons []*extensionsv1alpha1.Addon, install bool) *addonStatus {
   232  	status := &addonStatus{
   233  		allEnabled:  true,
   234  		allDisabled: true,
   235  		hasFailed:   false,
   236  		outputMsg:   "",
   237  	}
   238  
   239  	if len(addons) == 0 {
   240  		return status
   241  	}
   242  
   243  	all := make([]string, 0)
   244  	for _, addon := range addons {
   245  		s := string(addon.Status.Phase)
   246  		switch addon.Status.Phase {
   247  		case extensionsv1alpha1.AddonEnabled:
   248  			if install {
   249  				s = printer.BoldGreen("OK")
   250  			}
   251  			status.allDisabled = false
   252  		case extensionsv1alpha1.AddonDisabled:
   253  			if !install {
   254  				s = printer.BoldGreen("OK")
   255  			}
   256  			status.allEnabled = false
   257  		case extensionsv1alpha1.AddonFailed:
   258  			status.hasFailed = true
   259  			status.allEnabled = false
   260  			status.allDisabled = false
   261  		case extensionsv1alpha1.AddonDisabling:
   262  			status.allDisabled = false
   263  		case extensionsv1alpha1.AddonEnabling:
   264  			status.allEnabled = false
   265  		}
   266  		all = append(all, fmt.Sprintf("%-48s %s", addon.Name, s))
   267  	}
   268  	sort.Strings(all)
   269  	status.outputMsg = strings.Join(all, "\n  ")
   270  	return status
   271  }
   272  
   273  func newHelmRepoEntry() *repo.Entry {
   274  	return &repo.Entry{
   275  		Name: types.KubeBlocksChartName,
   276  		URL:  util.GetHelmChartRepoURL(),
   277  	}
   278  }