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  }