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 }