github.com/oam-dev/kubevela@v1.9.11/references/cli/install.go (about)

     1  /*
     2  Copyright 2021 The KubeVela Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8  	http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package cli
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"time"
    23  
    24  	"cuelang.org/go/pkg/strings"
    25  	"github.com/hashicorp/go-version"
    26  	"github.com/pkg/errors"
    27  	"github.com/spf13/cobra"
    28  	"helm.sh/helm/v3/pkg/chart"
    29  	"helm.sh/helm/v3/pkg/strvals"
    30  	corev1 "k8s.io/api/core/v1"
    31  	apierror "k8s.io/apimachinery/pkg/api/errors"
    32  	apitypes "k8s.io/apimachinery/pkg/types"
    33  	"k8s.io/client-go/kubernetes"
    34  	"k8s.io/client-go/rest"
    35  	"k8s.io/klog/v2"
    36  	"sigs.k8s.io/controller-runtime/pkg/client"
    37  
    38  	"github.com/kubevela/pkg/util/k8s"
    39  
    40  	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
    41  	"github.com/oam-dev/kubevela/apis/types"
    42  	"github.com/oam-dev/kubevela/pkg/utils/apply"
    43  	"github.com/oam-dev/kubevela/pkg/utils/common"
    44  	"github.com/oam-dev/kubevela/pkg/utils/helm"
    45  	"github.com/oam-dev/kubevela/pkg/utils/util"
    46  	innerVersion "github.com/oam-dev/kubevela/version"
    47  )
    48  
    49  const defaultConstraint = ">= 1.19"
    50  
    51  const (
    52  	// LegacyKubeVelaInstallerHelmRepoURL is used for kubevela version < v1.9.0
    53  	LegacyKubeVelaInstallerHelmRepoURL = "https://charts.kubevela.net/core/"
    54  	// KubeVelaInstallerHelmRepoURL is used for kubevela version >= v1.9.0
    55  	KubeVelaInstallerHelmRepoURL = "https://kubevela.github.io/charts/"
    56  )
    57  
    58  // kubeVelaReleaseName release name
    59  const kubeVelaReleaseName = "kubevela"
    60  
    61  // kubeVelaChartName the name of veal core chart
    62  const kubeVelaChartName = "vela-core"
    63  
    64  // InstallArgs the args for install command
    65  type InstallArgs struct {
    66  	userInput     *UserInput
    67  	helmHelper    *helm.Helper
    68  	Args          common.Args
    69  	Values        []string
    70  	Namespace     string
    71  	Version       string
    72  	ChartFilePath string
    73  	Detail        bool
    74  	ReuseValues   bool
    75  }
    76  
    77  // NewInstallCommand creates `install` command to install vela core
    78  func NewInstallCommand(c common.Args, order string, ioStreams util.IOStreams) *cobra.Command {
    79  	installArgs := &InstallArgs{Args: c, userInput: NewUserInput(), helmHelper: helm.NewHelper()}
    80  	cmd := &cobra.Command{
    81  		Use:   "install",
    82  		Short: "Installs or Upgrades Kubevela control plane on a Kubernetes cluster.",
    83  		Long:  "The Kubevela CLI allows installing Kubevela on any Kubernetes derivative to which your kube config is pointing to.",
    84  		Args:  cobra.ExactArgs(0),
    85  		PreRunE: func(cmd *cobra.Command, args []string) error {
    86  			// CheckRequirements
    87  			ioStreams.Info("Check Requirements ...")
    88  			restConfig, err := c.GetConfig()
    89  			if err != nil {
    90  				return errors.Wrapf(err, "failed to get kube config, You can set KUBECONFIG env or make file ~/.kube/config")
    91  			}
    92  			if isNewerVersion, serverVersion, err := checkKubeServerVersion(restConfig); err != nil {
    93  				ioStreams.Error(err.Error())
    94  				ioStreams.Error("This is not recommended and could have negative impacts on the stability of KubeVela - use at your own risk.")
    95  
    96  				userConfirmation := installArgs.userInput.AskBool("Do you want to continue?", &UserInputOptions{assumeYes})
    97  				if !userConfirmation {
    98  					return fmt.Errorf("stopping installation")
    99  				}
   100  			} else if isNewerVersion {
   101  				ioStreams.Errorf("The Kubernetes server version(%s) is higher than the one officially supported(%s).\n", serverVersion, defaultConstraint)
   102  				ioStreams.Error("This is not recommended and could have negative impacts on the stability of KubeVela - use at your own risk.")
   103  				userInput := NewUserInput()
   104  				userConfirmation := userInput.AskBool("Do you want to continue?", &UserInputOptions{assumeYes})
   105  				if !userConfirmation {
   106  					return fmt.Errorf("stopping installation")
   107  				}
   108  			}
   109  			return nil
   110  		},
   111  		RunE: func(cmd *cobra.Command, args []string) error {
   112  			v, err := version.NewVersion(installArgs.Version)
   113  			if err != nil {
   114  				return err
   115  			}
   116  			// Step1: Download Helm Chart
   117  			ioStreams.Info("Installing KubeVela Core ...")
   118  			if installArgs.ChartFilePath == "" {
   119  				installArgs.ChartFilePath = getKubeVelaHelmChartRepoURL(v)
   120  			}
   121  			chart, err := installArgs.helmHelper.LoadCharts(installArgs.ChartFilePath, nil)
   122  			if err != nil {
   123  				return fmt.Errorf("loading the helm chart of kubeVela control plane failure, %w", err)
   124  			}
   125  			ioStreams.Infof("Helm Chart used for KubeVela control plane installation: %s \n", installArgs.ChartFilePath)
   126  
   127  			// Step2: Prepare namespace
   128  			restConfig, err := c.GetConfig()
   129  			if err != nil {
   130  				return fmt.Errorf("get kube config failure: %w", err)
   131  			}
   132  			kubeClient, err := c.GetClient()
   133  			if err != nil {
   134  				return fmt.Errorf("create kube client failure: %w", err)
   135  			}
   136  			ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
   137  			defer cancel()
   138  			var namespace corev1.Namespace
   139  			var namespaceExists = true
   140  			if err := kubeClient.Get(ctx, apitypes.NamespacedName{Name: installArgs.Namespace}, &namespace); err != nil {
   141  				if !apierror.IsNotFound(err) {
   142  					return fmt.Errorf("failed to check if namespace %s already exists: %w", installArgs.Namespace, err)
   143  				}
   144  				namespaceExists = false
   145  			}
   146  			if namespaceExists {
   147  				fmt.Printf("Existing KubeVela installation found in namespace %s\n\n", installArgs.Namespace)
   148  				userConfirmation := installArgs.userInput.AskBool("Do you want to overwrite this installation?", &UserInputOptions{assumeYes})
   149  				if !userConfirmation {
   150  					return fmt.Errorf("stopping installation")
   151  				}
   152  			} else {
   153  				namespace.Name = installArgs.Namespace
   154  				if err := kubeClient.Create(ctx, &namespace); err != nil {
   155  					return fmt.Errorf("failed to create kubeVela namespace %s: %w", installArgs.Namespace, err)
   156  				}
   157  			}
   158  
   159  			if err := checkExistStepDefinitions(ctx, kubeClient, namespace.Name); err != nil {
   160  				return err
   161  			}
   162  			if err := checkExistViews(ctx, kubeClient, namespace.Name); err != nil {
   163  				return err
   164  			}
   165  
   166  			// Step3: Prepare the values for chart
   167  			imageTag := installArgs.Version
   168  			if !strings.HasPrefix(imageTag, "v") {
   169  				imageTag = "v" + imageTag
   170  			}
   171  			var values = map[string]interface{}{
   172  				"image": map[string]interface{}{
   173  					"tag":        imageTag,
   174  					"pullPolicy": "IfNotPresent",
   175  				},
   176  			}
   177  			if len(installArgs.Values) > 0 {
   178  				for _, value := range installArgs.Values {
   179  					if err := strvals.ParseInto(value, values); err != nil {
   180  						return errors.Wrap(err, "failed parsing --set data")
   181  					}
   182  				}
   183  			}
   184  			// Step4: apply new CRDs
   185  			if err := upgradeCRDs(cmd.Context(), kubeClient, chart); err != nil {
   186  				return fmt.Errorf("upgrade CRD failure %w", err)
   187  			}
   188  			// Step5: Install or upgrade helm release
   189  			release, err := installArgs.helmHelper.UpgradeChart(chart, kubeVelaReleaseName, installArgs.Namespace, values,
   190  				helm.UpgradeChartOptions{
   191  					Config:      restConfig,
   192  					Detail:      installArgs.Detail,
   193  					Logging:     ioStreams,
   194  					Wait:        true,
   195  					ReuseValues: installArgs.ReuseValues,
   196  				})
   197  			if err != nil {
   198  				msg := fmt.Sprintf("Could not install KubeVela control plane installation: %s", err.Error())
   199  				return errors.New(msg)
   200  			}
   201  
   202  			err = waitKubeVelaControllerRunning(kubeClient, installArgs.Namespace, release.Manifest)
   203  			if err != nil {
   204  				msg := fmt.Sprintf("Could not complete KubeVela control plane installation: %s \nFor troubleshooting, please check the status of the kubevela deployment by executing the following command: \n\nkubectl get pods -n %s\n", err.Error(), installArgs.Namespace)
   205  				return errors.New(msg)
   206  			}
   207  			ioStreams.Info()
   208  			ioStreams.Info("KubeVela control plane has been successfully set up on your cluster.")
   209  			ioStreams.Info("If you want to enable dashboard, please run \"vela addon enable velaux\"")
   210  			return nil
   211  		},
   212  		Annotations: map[string]string{
   213  			types.TagCommandOrder: order,
   214  			types.TagCommandType:  types.TypeSystem,
   215  		},
   216  	}
   217  
   218  	cmd.Flags().StringArrayVarP(&installArgs.Values, "set", "", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")
   219  	cmd.Flags().StringVarP(&installArgs.Namespace, "namespace", "n", "vela-system", "namespace scope for installing KubeVela Core")
   220  	cmd.Flags().StringVarP(&installArgs.Version, "version", "v", innerVersion.VelaVersion, "")
   221  	cmd.Flags().BoolVarP(&installArgs.Detail, "detail", "d", true, "show detail log of installation")
   222  	cmd.Flags().BoolVarP(&installArgs.ReuseValues, "reuse", "r", true, "will re-use the user's last supplied values.")
   223  	cmd.Flags().StringVarP(&installArgs.ChartFilePath, "file", "f", "", "custom the chart path of KubeVela control plane")
   224  	return cmd
   225  }
   226  
   227  func checkKubeServerVersion(config *rest.Config) (bool, string, error) {
   228  	// get kubernetes cluster api version
   229  	client, err := kubernetes.NewForConfig(config)
   230  	if err != nil {
   231  		return false, "", err
   232  	}
   233  	// check version
   234  	serverVersion, err := client.ServerVersion()
   235  	if err != nil {
   236  		return false, "", fmt.Errorf("get kubernetes api version failure %w", err)
   237  	}
   238  	vStr := fmt.Sprintf("%s.%s", serverVersion.Major, strings.Replace(serverVersion.Minor, "+", "", 1))
   239  	currentVersion, err := version.NewVersion(vStr)
   240  	if err != nil {
   241  		return false, "", err
   242  	}
   243  	hConstraints, err := version.NewConstraint(defaultConstraint)
   244  	if err != nil {
   245  		return false, "", err
   246  	}
   247  	isNewerVersion, allConstraintsValid := checkIsNewVersion(hConstraints, currentVersion)
   248  
   249  	if allConstraintsValid {
   250  		return false, vStr, nil
   251  	}
   252  	if isNewerVersion {
   253  		return true, vStr, nil
   254  	}
   255  
   256  	return false, vStr, fmt.Errorf("the kubernetes server version '%s' doesn't satisfy constraints '%s'", serverVersion, defaultConstraint)
   257  }
   258  
   259  // checkIsNewVersion checks if the provided version is higher than all constraints and if all constraints are valid
   260  func checkIsNewVersion(hConstraints version.Constraints, serverVersion *version.Version) (bool, bool) {
   261  	isNewerVersion := false
   262  	allConstraintsValid := true
   263  	for _, constraint := range hConstraints {
   264  		validConstraint := constraint.Check(serverVersion)
   265  		if !validConstraint {
   266  			allConstraintsValid = false
   267  			constraintVersionString := getConstraintVersion(constraint.String())
   268  			constraintVersion, err := version.NewVersion(constraintVersionString)
   269  			if err != nil {
   270  				return false, false
   271  			}
   272  			if serverVersion.GreaterThan(constraintVersion) {
   273  				isNewerVersion = true
   274  			} else {
   275  				return false, false
   276  			}
   277  		}
   278  	}
   279  	return isNewerVersion, allConstraintsValid
   280  }
   281  
   282  // getConstraintVersion returns the version of a constraint without leading spaces, <, >, =
   283  func getConstraintVersion(constraint string) string {
   284  	for index, character := range constraint {
   285  		if character != '<' && character != '>' && character != ' ' && character != '=' {
   286  			return constraint[index:]
   287  		}
   288  	}
   289  	return constraint
   290  }
   291  
   292  func getKubeVelaHelmChartRepoURL(ver *version.Version) string {
   293  	// Determine use legacy repo or new one.
   294  	useLegacy := innerVersion.ShouldUseLegacyHelmRepo(ver)
   295  	helmRepo := KubeVelaInstallerHelmRepoURL
   296  	if useLegacy {
   297  		helmRepo = LegacyKubeVelaInstallerHelmRepoURL
   298  	}
   299  	return helmRepo + kubeVelaChartName + "-" + ver.String() + ".tgz"
   300  }
   301  
   302  func waitKubeVelaControllerRunning(kubeClient client.Client, namespace, manifest string) error {
   303  	deployments := helm.GetDeploymentsFromManifest(manifest)
   304  	spinner := newTrackingSpinnerWithDelay("Waiting KubeVela control plane running ...", 1*time.Second)
   305  	spinner.Start()
   306  	defer spinner.Stop()
   307  	trackInterval := 5 * time.Second
   308  	timeout := 600 * time.Second
   309  	start := time.Now()
   310  	ctx := context.Background()
   311  	for {
   312  		timeConsumed := int(time.Since(start).Seconds())
   313  		var readyCount = 0
   314  		for i, d := range deployments {
   315  			err := kubeClient.Get(ctx, apitypes.NamespacedName{Name: d.Name, Namespace: namespace}, deployments[i])
   316  			if err != nil {
   317  				return client.IgnoreNotFound(err)
   318  			}
   319  			if deployments[i].Status.ReadyReplicas != deployments[i].Status.Replicas {
   320  				applySpinnerNewSuffix(spinner, fmt.Sprintf("Waiting deployment %s ready. (timeout %d/%d seconds)...", deployments[i].Name, timeConsumed, int(timeout.Seconds())))
   321  			} else {
   322  				readyCount++
   323  			}
   324  		}
   325  		if readyCount >= len(deployments) {
   326  			return nil
   327  		}
   328  		if timeConsumed > int(timeout.Seconds()) {
   329  			return errors.Errorf("Enabling timeout, please run \"kubectl get pod -n vela-system\" to check the status")
   330  		}
   331  		time.Sleep(trackInterval)
   332  	}
   333  }
   334  
   335  func upgradeCRDs(ctx context.Context, kubeClient client.Client, chart *chart.Chart) error {
   336  	crds := helm.GetCRDFromChart(chart)
   337  	applyHelper := apply.NewAPIApplicator(kubeClient)
   338  	for _, crd := range crds {
   339  		if err := applyHelper.Apply(ctx, crd, apply.DisableUpdateAnnotation()); err != nil {
   340  			return err
   341  		}
   342  	}
   343  	return nil
   344  }
   345  
   346  func checkExistStepDefinitions(ctx context.Context, kubeClient client.Client, namespace string) error {
   347  	legacyDefs := []string{"apply-deployment", "apply-terraform-config", "apply-terraform-provider", "clean-jobs", "request", "vela-cli"}
   348  	for _, name := range legacyDefs {
   349  		def := &v1beta1.WorkflowStepDefinition{}
   350  		if err := kubeClient.Get(ctx, apitypes.NamespacedName{Namespace: namespace, Name: name}, def); err == nil {
   351  			if err := takeOverResourcesForHelm(ctx, kubeClient, def, namespace); err != nil {
   352  				return fmt.Errorf("failed to update the %s workflow step definition: %w", name, err)
   353  			}
   354  			klog.Infof("successfully tack over the %s workflow step definition", name)
   355  		}
   356  	}
   357  	return nil
   358  }
   359  
   360  func checkExistViews(ctx context.Context, kubeClient client.Client, namespace string) error {
   361  	legacyViews := []string{"component-pod-view", "component-service-view"}
   362  	for _, name := range legacyViews {
   363  		cm := &corev1.ConfigMap{}
   364  		if err := kubeClient.Get(ctx, apitypes.NamespacedName{Namespace: namespace, Name: name}, cm); err == nil {
   365  			if err := takeOverResourcesForHelm(ctx, kubeClient, cm, namespace); err != nil {
   366  				return fmt.Errorf("failed to update the %s view: %w", name, err)
   367  			}
   368  			klog.Infof("successfully tack over the %s view", name)
   369  		}
   370  	}
   371  	return nil
   372  }
   373  
   374  func takeOverResourcesForHelm(ctx context.Context, kubeClient client.Client, obj client.Object, namespace string) error {
   375  	anno := obj.GetAnnotations()
   376  	if anno != nil && anno["meta.helm.sh/release-name"] == kubeVelaReleaseName {
   377  		return nil
   378  	}
   379  	if err := k8s.AddLabel(obj, "app.kubernetes.io/managed-by", "Helm"); err != nil {
   380  		return err
   381  	}
   382  	if err := k8s.AddAnnotation(obj, "meta.helm.sh/release-name", kubeVelaReleaseName); err != nil {
   383  		return err
   384  	}
   385  	if err := k8s.AddAnnotation(obj, "meta.helm.sh/release-namespace", namespace); err != nil {
   386  		return err
   387  	}
   388  	return kubeClient.Update(ctx, obj)
   389  }