2022-02-09 11:48:38 +08:00
/ *
Copyright 2021 The KubeVela Authors .
Licensed under the Apache License , Version 2.0 ( the "License" ) ;
you may not use this file except in compliance with the License .
You may obtain a copy of the License at
http : //www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing , software
distributed under the License is distributed on an "AS IS" BASIS ,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
See the License for the specific language governing permissions and
limitations under the License .
* /
package cli
import (
"context"
"fmt"
"time"
"cuelang.org/go/pkg/strings"
"github.com/hashicorp/go-version"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/strvals"
corev1 "k8s.io/api/core/v1"
apierror "k8s.io/apimachinery/pkg/api/errors"
apitypes "k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
2023-01-05 19:13:19 +08:00
"k8s.io/klog/v2"
2022-02-09 11:48:38 +08:00
"sigs.k8s.io/controller-runtime/pkg/client"
2023-01-05 19:13:19 +08:00
"github.com/kubevela/pkg/util/k8s"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
2022-02-09 11:48:38 +08:00
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/utils/apply"
"github.com/oam-dev/kubevela/pkg/utils/common"
"github.com/oam-dev/kubevela/pkg/utils/helm"
"github.com/oam-dev/kubevela/pkg/utils/util"
innerVersion "github.com/oam-dev/kubevela/version"
)
2023-05-16 11:57:40 +08:00
const defaultConstraint = ">= 1.19"
2022-02-09 11:48:38 +08:00
2023-07-04 12:15:30 +08:00
const (
// LegacyKubeVelaInstallerHelmRepoURL is used for kubevela version < v1.9.0
LegacyKubeVelaInstallerHelmRepoURL = "https://charts.kubevela.net/core/"
// KubeVelaInstallerHelmRepoURL is used for kubevela version >= v1.9.0
KubeVelaInstallerHelmRepoURL = "https://kubevela.github.io/charts/"
)
2022-02-09 11:48:38 +08:00
// kubeVelaReleaseName release name
const kubeVelaReleaseName = "kubevela"
// kubeVelaChartName the name of veal core chart
const kubeVelaChartName = "vela-core"
// InstallArgs the args for install command
type InstallArgs struct {
userInput * UserInput
helmHelper * helm . Helper
Args common . Args
Values [ ] string
Namespace string
Version string
ChartFilePath string
Detail bool
ReuseValues bool
}
// NewInstallCommand creates `install` command to install vela core
func NewInstallCommand ( c common . Args , order string , ioStreams util . IOStreams ) * cobra . Command {
installArgs := & InstallArgs { Args : c , userInput : NewUserInput ( ) , helmHelper : helm . NewHelper ( ) }
cmd := & cobra . Command {
Use : "install" ,
Short : "Installs or Upgrades Kubevela control plane on a Kubernetes cluster." ,
Long : "The Kubevela CLI allows installing Kubevela on any Kubernetes derivative to which your kube config is pointing to." ,
Args : cobra . ExactArgs ( 0 ) ,
PreRunE : func ( cmd * cobra . Command , args [ ] string ) error {
// CheckRequirements
ioStreams . Info ( "Check Requirements ..." )
restConfig , err := c . GetConfig ( )
if err != nil {
return errors . Wrapf ( err , "failed to get kube config, You can set KUBECONFIG env or make file ~/.kube/config" )
}
if isNewerVersion , serverVersion , err := checkKubeServerVersion ( restConfig ) ; err != nil {
ioStreams . Error ( err . Error ( ) )
ioStreams . Error ( "This is not recommended and could have negative impacts on the stability of KubeVela - use at your own risk." )
userConfirmation := installArgs . userInput . AskBool ( "Do you want to continue?" , & UserInputOptions { assumeYes } )
if ! userConfirmation {
return fmt . Errorf ( "stopping installation" )
}
} else if isNewerVersion {
ioStreams . Errorf ( "The Kubernetes server version(%s) is higher than the one officially supported(%s).\n" , serverVersion , defaultConstraint )
ioStreams . Error ( "This is not recommended and could have negative impacts on the stability of KubeVela - use at your own risk." )
userInput := NewUserInput ( )
userConfirmation := userInput . AskBool ( "Do you want to continue?" , & UserInputOptions { assumeYes } )
if ! userConfirmation {
return fmt . Errorf ( "stopping installation" )
}
}
return nil
} ,
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
2023-07-04 12:15:30 +08:00
v , err := version . NewVersion ( installArgs . Version )
if err != nil {
return err
}
2022-02-09 11:48:38 +08:00
// Step1: Download Helm Chart
ioStreams . Info ( "Installing KubeVela Core ..." )
if installArgs . ChartFilePath == "" {
2023-07-04 12:15:30 +08:00
installArgs . ChartFilePath = getKubeVelaHelmChartRepoURL ( v )
2022-02-09 11:48:38 +08:00
}
2022-04-12 14:33:44 +08:00
chart , err := installArgs . helmHelper . LoadCharts ( installArgs . ChartFilePath , nil )
2022-02-09 11:48:38 +08:00
if err != nil {
2023-05-16 11:57:40 +08:00
return fmt . Errorf ( "loading the helm chart of kubeVela control plane failure, %w" , err )
2022-02-09 11:48:38 +08:00
}
ioStreams . Infof ( "Helm Chart used for KubeVela control plane installation: %s \n" , installArgs . ChartFilePath )
// Step2: Prepare namespace
restConfig , err := c . GetConfig ( )
if err != nil {
return fmt . Errorf ( "get kube config failure: %w" , err )
}
kubeClient , err := c . GetClient ( )
if err != nil {
return fmt . Errorf ( "create kube client failure: %w" , err )
}
ctx , cancel := context . WithTimeout ( context . Background ( ) , time . Second * 10 )
defer cancel ( )
var namespace corev1 . Namespace
var namespaceExists = true
if err := kubeClient . Get ( ctx , apitypes . NamespacedName { Name : installArgs . Namespace } , & namespace ) ; err != nil {
if ! apierror . IsNotFound ( err ) {
return fmt . Errorf ( "failed to check if namespace %s already exists: %w" , installArgs . Namespace , err )
}
namespaceExists = false
}
if namespaceExists {
fmt . Printf ( "Existing KubeVela installation found in namespace %s\n\n" , installArgs . Namespace )
userConfirmation := installArgs . userInput . AskBool ( "Do you want to overwrite this installation?" , & UserInputOptions { assumeYes } )
if ! userConfirmation {
return fmt . Errorf ( "stopping installation" )
}
} else {
namespace . Name = installArgs . Namespace
if err := kubeClient . Create ( ctx , & namespace ) ; err != nil {
return fmt . Errorf ( "failed to create kubeVela namespace %s: %w" , installArgs . Namespace , err )
}
}
2023-01-05 19:13:19 +08:00
if err := checkExistStepDefinitions ( ctx , kubeClient , namespace . Name ) ; err != nil {
return err
}
if err := checkExistViews ( ctx , kubeClient , namespace . Name ) ; err != nil {
return err
}
2022-02-09 11:48:38 +08:00
// Step3: Prepare the values for chart
imageTag := installArgs . Version
if ! strings . HasPrefix ( imageTag , "v" ) {
imageTag = "v" + imageTag
}
var values = map [ string ] interface { } {
"image" : map [ string ] interface { } {
"tag" : imageTag ,
"pullPolicy" : "IfNotPresent" ,
} ,
}
if len ( installArgs . Values ) > 0 {
for _ , value := range installArgs . Values {
if err := strvals . ParseInto ( value , values ) ; err != nil {
return errors . Wrap ( err , "failed parsing --set data" )
}
}
}
// Step4: apply new CRDs
if err := upgradeCRDs ( cmd . Context ( ) , kubeClient , chart ) ; err != nil {
2022-08-08 17:57:48 +08:00
return fmt . Errorf ( "upgrade CRD failure %w" , err )
2022-02-09 11:48:38 +08:00
}
// Step5: Install or upgrade helm release
release , err := installArgs . helmHelper . UpgradeChart ( chart , kubeVelaReleaseName , installArgs . Namespace , values ,
helm . UpgradeChartOptions {
Config : restConfig ,
Detail : installArgs . Detail ,
Logging : ioStreams ,
Wait : true ,
ReuseValues : installArgs . ReuseValues ,
} )
if err != nil {
msg := fmt . Sprintf ( "Could not install KubeVela control plane installation: %s" , err . Error ( ) )
return errors . New ( msg )
}
err = waitKubeVelaControllerRunning ( kubeClient , installArgs . Namespace , release . Manifest )
if err != nil {
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 )
return errors . New ( msg )
}
ioStreams . Info ( )
ioStreams . Info ( "KubeVela control plane has been successfully set up on your cluster." )
ioStreams . Info ( "If you want to enable dashboard, please run \"vela addon enable velaux\"" )
return nil
} ,
Annotations : map [ string ] string {
types . TagCommandOrder : order ,
types . TagCommandType : types . TypeSystem ,
} ,
}
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)" )
cmd . Flags ( ) . StringVarP ( & installArgs . Namespace , "namespace" , "n" , "vela-system" , "namespace scope for installing KubeVela Core" )
cmd . Flags ( ) . StringVarP ( & installArgs . Version , "version" , "v" , innerVersion . VelaVersion , "" )
cmd . Flags ( ) . BoolVarP ( & installArgs . Detail , "detail" , "d" , true , "show detail log of installation" )
cmd . Flags ( ) . BoolVarP ( & installArgs . ReuseValues , "reuse" , "r" , true , "will re-use the user's last supplied values." )
cmd . Flags ( ) . StringVarP ( & installArgs . ChartFilePath , "file" , "f" , "" , "custom the chart path of KubeVela control plane" )
return cmd
}
func checkKubeServerVersion ( config * rest . Config ) ( bool , string , error ) {
// get kubernetes cluster api version
client , err := kubernetes . NewForConfig ( config )
if err != nil {
return false , "" , err
}
// check version
serverVersion , err := client . ServerVersion ( )
if err != nil {
return false , "" , fmt . Errorf ( "get kubernetes api version failure %w" , err )
}
vStr := fmt . Sprintf ( "%s.%s" , serverVersion . Major , strings . Replace ( serverVersion . Minor , "+" , "" , 1 ) )
currentVersion , err := version . NewVersion ( vStr )
if err != nil {
return false , "" , err
}
hConstraints , err := version . NewConstraint ( defaultConstraint )
if err != nil {
return false , "" , err
}
isNewerVersion , allConstraintsValid := checkIsNewVersion ( hConstraints , currentVersion )
if allConstraintsValid {
return false , vStr , nil
}
if isNewerVersion {
return true , vStr , nil
}
return false , vStr , fmt . Errorf ( "the kubernetes server version '%s' doesn't satisfy constraints '%s'" , serverVersion , defaultConstraint )
}
// checkIsNewVersion checks if the provided version is higher than all constraints and if all constraints are valid
func checkIsNewVersion ( hConstraints version . Constraints , serverVersion * version . Version ) ( bool , bool ) {
isNewerVersion := false
allConstraintsValid := true
for _ , constraint := range hConstraints {
validConstraint := constraint . Check ( serverVersion )
if ! validConstraint {
allConstraintsValid = false
constraintVersionString := getConstraintVersion ( constraint . String ( ) )
constraintVersion , err := version . NewVersion ( constraintVersionString )
if err != nil {
return false , false
}
if serverVersion . GreaterThan ( constraintVersion ) {
isNewerVersion = true
} else {
return false , false
}
}
}
return isNewerVersion , allConstraintsValid
}
// getConstraintVersion returns the version of a constraint without leading spaces, <, >, =
func getConstraintVersion ( constraint string ) string {
for index , character := range constraint {
if character != '<' && character != '>' && character != ' ' && character != '=' {
return constraint [ index : ]
}
}
return constraint
}
2023-07-04 12:15:30 +08:00
func getKubeVelaHelmChartRepoURL ( ver * version . Version ) string {
// Determine use legacy repo or new one.
useLegacy := innerVersion . ShouldUseLegacyHelmRepo ( ver )
helmRepo := KubeVelaInstallerHelmRepoURL
if useLegacy {
helmRepo = LegacyKubeVelaInstallerHelmRepoURL
2022-02-09 11:48:38 +08:00
}
2023-07-04 12:15:30 +08:00
return helmRepo + kubeVelaChartName + "-" + ver . String ( ) + ".tgz"
2022-02-09 11:48:38 +08:00
}
func waitKubeVelaControllerRunning ( kubeClient client . Client , namespace , manifest string ) error {
deployments := helm . GetDeploymentsFromManifest ( manifest )
spinner := newTrackingSpinnerWithDelay ( "Waiting KubeVela control plane running ..." , 1 * time . Second )
spinner . Start ( )
defer spinner . Stop ( )
trackInterval := 5 * time . Second
timeout := 600 * time . Second
start := time . Now ( )
ctx := context . Background ( )
for {
timeConsumed := int ( time . Since ( start ) . Seconds ( ) )
var readyCount = 0
for i , d := range deployments {
err := kubeClient . Get ( ctx , apitypes . NamespacedName { Name : d . Name , Namespace : namespace } , deployments [ i ] )
if err != nil {
return client . IgnoreNotFound ( err )
}
if deployments [ i ] . Status . ReadyReplicas != deployments [ i ] . Status . Replicas {
applySpinnerNewSuffix ( spinner , fmt . Sprintf ( "Waiting deployment %s ready. (timeout %d/%d seconds)..." , deployments [ i ] . Name , timeConsumed , int ( timeout . Seconds ( ) ) ) )
} else {
readyCount ++
}
}
if readyCount >= len ( deployments ) {
return nil
}
if timeConsumed > int ( timeout . Seconds ( ) ) {
return errors . Errorf ( "Enabling timeout, please run \"kubectl get pod -n vela-system\" to check the status" )
}
time . Sleep ( trackInterval )
}
}
func upgradeCRDs ( ctx context . Context , kubeClient client . Client , chart * chart . Chart ) error {
crds := helm . GetCRDFromChart ( chart )
2022-02-14 10:33:50 +08:00
applyHelper := apply . NewAPIApplicator ( kubeClient )
2022-02-09 11:48:38 +08:00
for _ , crd := range crds {
2022-02-14 10:33:50 +08:00
if err := applyHelper . Apply ( ctx , crd , apply . DisableUpdateAnnotation ( ) ) ; err != nil {
2022-02-09 11:48:38 +08:00
return err
}
}
return nil
}
2023-01-05 19:13:19 +08:00
func checkExistStepDefinitions ( ctx context . Context , kubeClient client . Client , namespace string ) error {
legacyDefs := [ ] string { "apply-deployment" , "apply-terraform-config" , "apply-terraform-provider" , "clean-jobs" , "request" , "vela-cli" }
for _ , name := range legacyDefs {
def := & v1beta1 . WorkflowStepDefinition { }
if err := kubeClient . Get ( ctx , apitypes . NamespacedName { Namespace : namespace , Name : name } , def ) ; err == nil {
if err := takeOverResourcesForHelm ( ctx , kubeClient , def , namespace ) ; err != nil {
return fmt . Errorf ( "failed to update the %s workflow step definition: %w" , name , err )
}
klog . Infof ( "successfully tack over the %s workflow step definition" , name )
}
}
return nil
}
func checkExistViews ( ctx context . Context , kubeClient client . Client , namespace string ) error {
legacyViews := [ ] string { "component-pod-view" , "component-service-view" }
for _ , name := range legacyViews {
cm := & corev1 . ConfigMap { }
if err := kubeClient . Get ( ctx , apitypes . NamespacedName { Namespace : namespace , Name : name } , cm ) ; err == nil {
if err := takeOverResourcesForHelm ( ctx , kubeClient , cm , namespace ) ; err != nil {
return fmt . Errorf ( "failed to update the %s view: %w" , name , err )
}
klog . Infof ( "successfully tack over the %s view" , name )
}
}
return nil
}
func takeOverResourcesForHelm ( ctx context . Context , kubeClient client . Client , obj client . Object , namespace string ) error {
anno := obj . GetAnnotations ( )
if anno != nil && anno [ "meta.helm.sh/release-name" ] == kubeVelaReleaseName {
return nil
}
if err := k8s . AddLabel ( obj , "app.kubernetes.io/managed-by" , "Helm" ) ; err != nil {
return err
}
if err := k8s . AddAnnotation ( obj , "meta.helm.sh/release-name" , kubeVelaReleaseName ) ; err != nil {
return err
}
if err := k8s . AddAnnotation ( obj , "meta.helm.sh/release-namespace" , namespace ) ; err != nil {
return err
}
return kubeClient . Update ( ctx , obj )
}