github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/kubeblocks/install.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 "bytes" 24 "context" 25 "encoding/json" 26 "fmt" 27 "sort" 28 "strings" 29 "time" 30 31 "github.com/pkg/errors" 32 "github.com/replicatedhq/troubleshoot/pkg/preflight" 33 "github.com/spf13/cobra" 34 "github.com/spf13/pflag" 35 "golang.org/x/exp/maps" 36 "helm.sh/helm/v3/pkg/cli/values" 37 apierrors "k8s.io/apimachinery/pkg/api/errors" 38 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 39 "k8s.io/apimachinery/pkg/runtime" 40 "k8s.io/apimachinery/pkg/util/wait" 41 "k8s.io/cli-runtime/pkg/genericiooptions" 42 "k8s.io/client-go/dynamic" 43 "k8s.io/client-go/kubernetes" 44 "k8s.io/klog/v2" 45 cmdutil "k8s.io/kubectl/pkg/cmd/util" 46 "k8s.io/kubectl/pkg/util/templates" 47 48 extensionsv1alpha1 "github.com/1aal/kubeblocks/apis/extensions/v1alpha1" 49 "github.com/1aal/kubeblocks/pkg/cli/spinner" 50 "github.com/1aal/kubeblocks/pkg/cli/types" 51 "github.com/1aal/kubeblocks/pkg/cli/util" 52 "github.com/1aal/kubeblocks/pkg/cli/util/breakingchange" 53 "github.com/1aal/kubeblocks/pkg/cli/util/helm" 54 "github.com/1aal/kubeblocks/version" 55 ) 56 57 const ( 58 kNodeAffinity = "affinity.nodeAffinity=%s" 59 kPodAntiAffinity = "affinity.podAntiAffinity=%s" 60 kTolerations = "tolerations=%s" 61 defaultTolerationsForInstallation = "kb-controller=true:NoSchedule" 62 ) 63 64 type Options struct { 65 genericiooptions.IOStreams 66 67 HelmCfg *helm.Config 68 69 // Namespace is the current namespace the command running in 70 Namespace string 71 Client kubernetes.Interface 72 Dynamic dynamic.Interface 73 Timeout time.Duration 74 Wait bool 75 } 76 77 type InstallOptions struct { 78 Options 79 OldVersion string 80 Version string 81 Quiet bool 82 CreateNamespace bool 83 Check bool 84 // autoApprove for KubeBlocks upgrade 85 autoApprove bool 86 ValueOpts values.Options 87 88 // ConfiguredOptions is the options that kubeblocks 89 PodAntiAffinity string 90 TopologyKeys []string 91 NodeLabels map[string]string 92 TolerationsRaw []string 93 } 94 95 type addonStatus struct { 96 allEnabled bool 97 allDisabled bool 98 hasFailed bool 99 outputMsg string 100 } 101 102 var ( 103 installExample = templates.Examples(` 104 # Install KubeBlocks, the default version is same with the kbcli version, the default namespace is kb-system 105 kbcli kubeblocks install 106 107 # Install KubeBlocks with specified version 108 kbcli kubeblocks install --version=0.4.0 109 110 # Install KubeBlocks with ignoring preflight checks 111 kbcli kubeblocks install --force 112 113 # Install KubeBlocks with specified namespace, if the namespace is not present, it will be created 114 kbcli kubeblocks install --namespace=my-namespace --create-namespace 115 116 # Install KubeBlocks with other settings, for example, set replicaCount to 3 117 kbcli kubeblocks install --set replicaCount=3`) 118 119 spinnerMsg = func(format string, a ...any) spinner.Option { 120 return spinner.WithMessage(fmt.Sprintf("%-50s", fmt.Sprintf(format, a...))) 121 } 122 ) 123 124 func newInstallCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 125 o := &InstallOptions{ 126 Options: Options{ 127 IOStreams: streams, 128 }, 129 } 130 131 p := &PreflightOptions{ 132 PreflightFlags: preflight.NewPreflightFlags(), 133 IOStreams: streams, 134 } 135 *p.Interactive = false 136 *p.Format = "kbcli" 137 138 cmd := &cobra.Command{ 139 Use: "install", 140 Short: "Install KubeBlocks.", 141 Args: cobra.NoArgs, 142 Example: installExample, 143 Run: func(cmd *cobra.Command, args []string) { 144 util.CheckErr(o.Complete(f, cmd)) 145 util.CheckErr(o.PreCheck()) 146 util.CheckErr(o.CompleteInstallOptions()) 147 util.CheckErr(p.Preflight(f, args, o.ValueOpts)) 148 util.CheckErr(o.Install()) 149 }, 150 } 151 152 cmd.Flags().StringVar(&o.Version, "version", version.DefaultKubeBlocksVersion, "KubeBlocks version") 153 cmd.Flags().BoolVar(&o.CreateNamespace, "create-namespace", false, "Create the namespace if not present") 154 cmd.Flags().BoolVar(&o.Check, "check", true, "Check kubernetes environment before installation") 155 cmd.Flags().DurationVar(&o.Timeout, "timeout", 300*time.Second, "Time to wait for installing KubeBlocks, such as --timeout=10m") 156 cmd.Flags().BoolVar(&o.Wait, "wait", true, "Wait for KubeBlocks to be ready, including all the auto installed add-ons. It will wait for a --timeout period") 157 cmd.Flags().BoolVar(&p.force, flagForce, p.force, "If present, just print fail item and continue with the following steps") 158 cmd.Flags().StringVar(&o.PodAntiAffinity, "pod-anti-affinity", "", "Pod anti-affinity type, one of: (Preferred, Required)") 159 cmd.Flags().StringArrayVar(&o.TopologyKeys, "topology-keys", nil, "Topology keys for affinity") 160 cmd.Flags().StringToStringVar(&o.NodeLabels, "node-labels", nil, "Node label selector") 161 cmd.Flags().StringSliceVar(&o.TolerationsRaw, "tolerations", nil, `Tolerations for Kubeblocks, such as '"dev=true:NoSchedule,large=true:NoSchedule"'`) 162 helm.AddValueOptionsFlags(cmd.Flags(), &o.ValueOpts) 163 164 return cmd 165 } 166 167 func (o *Options) Complete(f cmdutil.Factory, cmd *cobra.Command) error { 168 var err error 169 170 // default write log to file 171 if err = util.EnableLogToFile(cmd.Flags()); err != nil { 172 fmt.Fprintf(o.Out, "Failed to enable the log file %s", err.Error()) 173 } 174 175 if o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace(); err != nil { 176 return err 177 } 178 179 config, err := cmd.Flags().GetString("kubeconfig") 180 if err != nil { 181 return err 182 } 183 184 ctx, err := cmd.Flags().GetString("context") 185 if err != nil { 186 return err 187 } 188 189 // check whether --namespace is specified, if not, KubeBlocks will be installed 190 // to the kb-system namespace 191 var targetNamespace string 192 cmd.Flags().Visit(func(flag *pflag.Flag) { 193 if flag.Name == "namespace" { 194 targetNamespace = o.Namespace 195 } 196 }) 197 198 o.HelmCfg = helm.NewConfig(targetNamespace, config, ctx, klog.V(1).Enabled()) 199 if o.Dynamic, err = f.DynamicClient(); err != nil { 200 return err 201 } 202 203 o.Client, err = f.KubernetesClientSet() 204 return err 205 } 206 207 func (o *InstallOptions) PreCheck() error { 208 // check if KubeBlocks has been installed 209 v, err := util.GetVersionInfo(o.Client) 210 if err != nil { 211 return err 212 } 213 214 if v.KubeBlocks != "" { 215 return fmt.Errorf("KubeBlocks %s already exists, repeated installation is not supported", v.KubeBlocks) 216 } 217 218 // check whether the namespace exists 219 if err = o.checkNamespace(); err != nil { 220 return err 221 } 222 223 // check whether there are remained resource left by previous KubeBlocks installation, if yes, 224 // output the resource name 225 if err = o.checkRemainedResource(); err != nil { 226 return err 227 } 228 229 if err = o.checkVersion(v); err != nil { 230 return err 231 } 232 return nil 233 } 234 235 // CompleteInstallOptions complete options for real installation of kubeblocks 236 func (o *InstallOptions) CompleteInstallOptions() error { 237 // add pod anti-affinity 238 if o.PodAntiAffinity != "" || len(o.TopologyKeys) > 0 { 239 podAntiAffinityJSON, err := json.Marshal(util.BuildPodAntiAffinity(o.PodAntiAffinity, o.TopologyKeys)) 240 if err != nil { 241 return err 242 } 243 o.ValueOpts.JSONValues = append(o.ValueOpts.JSONValues, fmt.Sprintf(kPodAntiAffinity, podAntiAffinityJSON)) 244 } 245 246 // add node affinity 247 if len(o.NodeLabels) > 0 { 248 nodeLabelsJSON, err := json.Marshal(util.BuildNodeAffinity(o.NodeLabels)) 249 if err != nil { 250 return err 251 } 252 o.ValueOpts.JSONValues = append(o.ValueOpts.JSONValues, fmt.Sprintf(kNodeAffinity, string(nodeLabelsJSON))) 253 } 254 255 // add tolerations 256 // parse tolerations and add to values, the default tolerations are defined in var defaultTolerationsForInstallation 257 o.TolerationsRaw = append(o.TolerationsRaw, defaultTolerationsForInstallation) 258 tolerations, err := util.BuildTolerations(o.TolerationsRaw) 259 if err != nil { 260 return err 261 } 262 tolerationsJSON, err := json.Marshal(tolerations) 263 if err != nil { 264 return err 265 } 266 o.ValueOpts.JSONValues = append(o.ValueOpts.JSONValues, fmt.Sprintf(kTolerations, string(tolerationsJSON))) 267 return nil 268 } 269 270 func (o *InstallOptions) Install() error { 271 var err error 272 // add helm repo 273 s := spinner.New(o.Out, spinnerMsg("Add and update repo "+types.KubeBlocksRepoName)) 274 defer s.Fail() 275 // Add repo, if exists, will update it 276 if err = helm.AddRepo(newHelmRepoEntry()); err != nil { 277 return err 278 } 279 s.Success() 280 281 // install KubeBlocks 282 s = spinner.New(o.Out, spinnerMsg("Install KubeBlocks "+o.Version)) 283 defer s.Fail() 284 if err = o.installChart(); err != nil { 285 return err 286 } 287 s.Success() 288 289 // wait for auto-install addons to be ready 290 if err = o.waitAddonsEnabled(); err != nil { 291 fmt.Fprintf(o.Out, "Failed to wait for auto-install addons to be enabled, run \"kbcli kubeblocks status\" to check the status\n") 292 return err 293 } 294 295 if !o.Quiet { 296 msg := fmt.Sprintf("\nKubeBlocks %s installed to namespace %s SUCCESSFULLY!\n", o.Version, o.HelmCfg.Namespace()) 297 if !o.Wait { 298 msg = fmt.Sprintf(` 299 KubeBlocks %s is installing to namespace %s. 300 You can check the KubeBlocks status by running "kbcli kubeblocks status" 301 `, o.Version, o.HelmCfg.Namespace()) 302 } 303 fmt.Fprint(o.Out, msg) 304 o.printNotes() 305 } 306 return nil 307 } 308 309 // waitAddonsEnabled waits for auto-install addons status to be enabled 310 func (o *InstallOptions) waitAddonsEnabled() error { 311 if !o.Wait { 312 return nil 313 } 314 315 addons := make(map[string]*extensionsv1alpha1.Addon) 316 fetchAddons := func() error { 317 objs, err := o.Dynamic.Resource(types.AddonGVR()).List(context.TODO(), metav1.ListOptions{ 318 LabelSelector: buildKubeBlocksSelectorLabels(), 319 }) 320 if err != nil && !apierrors.IsNotFound(err) { 321 return err 322 } 323 if objs == nil || len(objs.Items) == 0 { 324 klog.V(1).Info("No Addons found") 325 return nil 326 } 327 328 for _, obj := range objs.Items { 329 addon := &extensionsv1alpha1.Addon{} 330 if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, addon); err != nil { 331 return err 332 } 333 334 if addon.Status.ObservedGeneration == 0 { 335 klog.V(1).Infof("Addon %s is not observed yet", addon.Name) 336 continue 337 } 338 339 // addon should be auto installed, check its status 340 if addon.Spec.InstallSpec.GetEnabled() { 341 addons[addon.Name] = addon 342 if addon.Status.Phase != extensionsv1alpha1.AddonEnabled { 343 klog.V(1).Infof("Addon %s is not enabled yet, status %s", addon.Name, addon.Status.Phase) 344 } 345 if addon.Status.Phase == extensionsv1alpha1.AddonFailed { 346 klog.V(1).Infof("Addon %s failed:", addon.Name) 347 for _, c := range addon.Status.Conditions { 348 klog.V(1).Infof(" %s: %s", c.Reason, c.Message) 349 } 350 } 351 } 352 } 353 return nil 354 } 355 356 suffixMsg := func(msg string) string { 357 return fmt.Sprintf("%-50s", msg) 358 } 359 360 // create spinner 361 msg := "" 362 header := "Wait for addons to be enabled" 363 failedErr := errors.New("some addons are failed to be enabled") 364 s := spinner.New(o.Out, spinnerMsg(header)) 365 var ( 366 err error 367 spinnerDone = func() { 368 s.SetFinalMsg(msg) 369 s.Done("") 370 fmt.Fprintln(o.Out) 371 } 372 ) 373 // wait all addons to be enabled, or timeout 374 if err = wait.PollImmediate(5*time.Second, o.Timeout, func() (bool, error) { 375 if err = fetchAddons(); err != nil || len(addons) == 0 { 376 return false, err 377 } 378 status := checkAddons(maps.Values(addons), true) 379 msg = suffixMsg(fmt.Sprintf("%s\n %s", header, status.outputMsg)) 380 s.SetMessage(msg) 381 if status.allEnabled { 382 spinnerDone() 383 return true, nil 384 } else if status.hasFailed { 385 return false, failedErr 386 } 387 return false, nil 388 }); err != nil { 389 spinnerDone() 390 printAddonMsg(o.Out, maps.Values(addons), true) 391 return err 392 } 393 394 return nil 395 } 396 397 func (o *InstallOptions) checkVersion(v util.Version) error { 398 if !o.Check { 399 return nil 400 } 401 402 // check installing version exists 403 if exists, err := versionExists(o.Version); !exists { 404 if err != nil { 405 klog.V(1).Infof(err.Error()) 406 } 407 return errors.Wrapf(err, "version %s does not exist, please use \"kbcli kubeblocks list-versions --devel\" to show the available versions", o.Version) 408 } 409 410 versionErr := fmt.Errorf("failed to get kubernetes version") 411 k8sVersionStr := v.Kubernetes 412 if k8sVersionStr == "" { 413 return versionErr 414 } 415 416 semVer := util.GetK8sSemVer(k8sVersionStr) 417 if len(semVer) == 0 { 418 return versionErr 419 } 420 421 // output kubernetes version 422 fmt.Fprintf(o.Out, "Kubernetes version %s\n", ""+semVer) 423 424 // disable or enable some features according to the kubernetes environment 425 provider, err := util.GetK8sProvider(k8sVersionStr, o.Client) 426 if err != nil { 427 return fmt.Errorf("failed to get kubernetes provider: %v", err) 428 } 429 if provider.IsCloud() { 430 fmt.Fprintf(o.Out, "Kubernetes provider %s\n", provider) 431 } 432 433 // check kbcli version, now do nothing 434 fmt.Fprintf(o.Out, "kbcli version %s\n", v.Cli) 435 436 return nil 437 } 438 439 func (o *InstallOptions) checkNamespace() error { 440 // target namespace is not specified, use default namespace 441 if o.HelmCfg.Namespace() == "" { 442 o.HelmCfg.SetNamespace(types.DefaultNamespace) 443 o.CreateNamespace = true 444 fmt.Fprintf(o.Out, "KubeBlocks will be installed to namespace \"%s\"\n", o.HelmCfg.Namespace()) 445 } 446 447 // check if namespace exists 448 if !o.CreateNamespace { 449 _, err := o.Client.CoreV1().Namespaces().Get(context.TODO(), o.Namespace, metav1.GetOptions{}) 450 return err 451 } 452 return nil 453 } 454 455 func (o *InstallOptions) checkRemainedResource() error { 456 if !o.Check { 457 return nil 458 } 459 460 ns, _ := util.GetKubeBlocksNamespace(o.Client) 461 if ns == "" { 462 ns = o.Namespace 463 } 464 465 // Now, we only check whether there are resources left by KubeBlocks, ignore 466 // the addon resources. 467 objs, err := getKBObjects(o.Dynamic, ns, nil) 468 if err != nil { 469 fmt.Fprintf(o.ErrOut, "Failed to get resources left by KubeBlocks before: %s\n", err.Error()) 470 } 471 472 res := getRemainedResource(objs) 473 if len(res) == 0 { 474 return nil 475 } 476 477 // output remained resource 478 var keys []string 479 for k := range res { 480 keys = append(keys, k) 481 } 482 sort.Strings(keys) 483 resStr := &bytes.Buffer{} 484 for _, k := range keys { 485 resStr.WriteString(fmt.Sprintf(" %s: %s\n", k, strings.Join(res[k], ","))) 486 } 487 return fmt.Errorf("there are resources left by previous KubeBlocks version, try to run \"kbcli kubeblocks uninstall\" to clean up\n%s", resStr.String()) 488 } 489 490 func (o *InstallOptions) installChart() error { 491 _, err := o.buildChart().Install(o.HelmCfg) 492 return err 493 } 494 495 func (o *InstallOptions) printNotes() { 496 fmt.Fprintf(o.Out, ` 497 -> Basic commands for cluster: 498 kbcli cluster create -h # help information about creating a database cluster 499 kbcli cluster list # list all database clusters 500 kbcli cluster describe <cluster name> # get cluster information 501 502 -> Uninstall KubeBlocks: 503 kbcli kubeblocks uninstall 504 `) 505 } 506 507 func (o *InstallOptions) buildChart() *helm.InstallOpts { 508 return &helm.InstallOpts{ 509 Name: types.KubeBlocksChartName, 510 Chart: types.KubeBlocksChartName + "/" + types.KubeBlocksChartName, 511 Wait: o.Wait, 512 Version: o.Version, 513 Namespace: o.HelmCfg.Namespace(), 514 ValueOpts: &o.ValueOpts, 515 TryTimes: 2, 516 CreateNamespace: o.CreateNamespace, 517 Timeout: o.Timeout, 518 Atomic: false, 519 Upgrader: breakingchange.Upgrader{ 520 FromVersion: o.OldVersion, 521 ToVersion: o.Version, 522 Dynamic: o.Dynamic, 523 }, 524 } 525 } 526 527 func versionExists(version string) (bool, error) { 528 if version == "" { 529 return true, nil 530 } 531 532 allVers, err := getHelmChartVersions(types.KubeBlocksChartName) 533 if err != nil { 534 return false, err 535 } 536 537 for _, v := range allVers { 538 if v.String() == version { 539 return true, nil 540 } 541 } 542 return false, nil 543 }