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 }