github.com/oam-dev/kubevela@v1.9.11/references/cli/addon.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 "encoding/json" 22 "fmt" 23 "os" 24 "path/filepath" 25 "sort" 26 "strings" 27 "time" 28 29 "github.com/fatih/color" 30 "github.com/getkin/kin-openapi/openapi3" 31 "github.com/gosuri/uitable" 32 "github.com/pkg/errors" 33 "github.com/spf13/cobra" 34 "helm.sh/helm/v3/pkg/strvals" 35 types2 "k8s.io/apimachinery/pkg/types" 36 "k8s.io/client-go/discovery" 37 "k8s.io/client-go/rest" 38 "sigs.k8s.io/controller-runtime/pkg/client" 39 40 common2 "github.com/oam-dev/kubevela/apis/core.oam.dev/common" 41 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" 42 "github.com/oam-dev/kubevela/apis/types" 43 pkgaddon "github.com/oam-dev/kubevela/pkg/addon" 44 "github.com/oam-dev/kubevela/pkg/oam" 45 addonutil "github.com/oam-dev/kubevela/pkg/utils/addon" 46 "github.com/oam-dev/kubevela/pkg/utils/apply" 47 "github.com/oam-dev/kubevela/pkg/utils/common" 48 cmdutil "github.com/oam-dev/kubevela/pkg/utils/util" 49 ) 50 51 const ( 52 statusEnabled = "enabled" 53 statusDisabled = "disabled" 54 statusSuspend = "suspend" 55 ) 56 57 var enabledAddonColor = color.New(color.Bold, color.FgGreen) 58 59 var ( 60 forceDisable bool 61 addonRegistry string 62 addonVersion string 63 addonClusters string 64 verboseStatus bool 65 skipValidate bool 66 overrideDefs bool 67 dryRun bool 68 yes2all bool 69 ) 70 71 // NewAddonCommand create `addon` command 72 func NewAddonCommand(c common.Args, order string, ioStreams cmdutil.IOStreams) *cobra.Command { 73 cmd := &cobra.Command{ 74 Use: "addon", 75 Short: "Manage addons for extension.", 76 Long: "Manage addons for extension.", 77 Annotations: map[string]string{ 78 types.TagCommandOrder: order, 79 types.TagCommandType: types.TypeApp, 80 }, 81 } 82 cmd.AddCommand( 83 NewAddonListCommand(c), 84 NewAddonEnableCommand(c, ioStreams), 85 NewAddonDisableCommand(c, ioStreams), 86 NewAddonStatusCommand(c, ioStreams), 87 NewAddonRegistryCommand(c, ioStreams), 88 NewAddonUpgradeCommand(c, ioStreams), 89 NewAddonPackageCommand(c), 90 NewAddonInitCommand(), 91 NewAddonPushCommand(c), 92 ) 93 return cmd 94 } 95 96 // NewAddonListCommand create addon list command 97 func NewAddonListCommand(c common.Args) *cobra.Command { 98 cmd := &cobra.Command{ 99 Use: "list", 100 Aliases: []string{"ls"}, 101 Short: "List addons", 102 Long: "List addons in KubeVela", 103 Example: ` List addon by: 104 vela addon ls 105 List addons in a specific registry, useful to reveal addons with duplicated names: 106 vela addon ls --registry <registry-name> 107 `, 108 RunE: func(cmd *cobra.Command, args []string) error { 109 k8sClient, err := c.GetClient() 110 if err != nil { 111 return err 112 } 113 table, err := listAddons(context.Background(), k8sClient, addonRegistry) 114 if err != nil { 115 return err 116 } 117 fmt.Println(table.String()) 118 return nil 119 }, 120 } 121 cmd.Flags().StringVarP(&addonRegistry, "registry", "r", "", "specify the registry name to list") 122 return cmd 123 } 124 125 // NewAddonEnableCommand create addon enable command 126 func NewAddonEnableCommand(c common.Args, ioStream cmdutil.IOStreams) *cobra.Command { 127 ctx := context.Background() 128 cmd := &cobra.Command{ 129 Use: "enable", 130 Aliases: []string{"install"}, 131 Short: "enable an addon", 132 Long: "enable an addon in cluster.", 133 Example: ` Enable addon by: 134 vela addon enable <addon-name> 135 Enable addon with specify version: 136 vela addon enable <addon-name> --version <addon-version> 137 Enable addon for specific clusters, (local means control plane): 138 vela addon enable <addon-name> --clusters={local,cluster1,cluster2} 139 Enable addon locally: 140 vela addon enable <your-local-addon-path> 141 Enable addon with specified args (the args should be defined in addon's parameters): 142 vela addon enable <addon-name> <my-parameter-of-addon>=<my-value> 143 Enable addon with specified registry: 144 vela addon enable <registryName>/<addonName> 145 `, 146 RunE: func(cmd *cobra.Command, args []string) error { 147 var additionalInfo string 148 if len(args) < 1 { 149 return fmt.Errorf("must specify addon name") 150 } 151 addonArgs, err := parseAddonArgsToMap(args[1:]) 152 if err != nil { 153 return err 154 } 155 clusterArgs := transClusters(addonClusters) 156 if len(clusterArgs) != 0 { 157 addonArgs[types.ClustersArg] = clusterArgs 158 } 159 config, err := c.GetConfig() 160 if err != nil { 161 return err 162 } 163 k8sClient, err := c.GetClient() 164 if err != nil { 165 return err 166 } 167 dc, err := c.GetDiscoveryClient() 168 if err != nil { 169 return err 170 } 171 addonOrDir := args[0] 172 var name = addonOrDir 173 // inject runtime info 174 addonArgs[pkgaddon.InstallerRuntimeOption] = map[string]interface{}{ 175 "upgrade": false, 176 } 177 var addonName string 178 if file, err := os.Stat(addonOrDir); err == nil { 179 if !file.IsDir() { 180 return fmt.Errorf("%s is not addon dir", addonOrDir) 181 } 182 ioStream.Infof(color.New(color.FgYellow).Sprintf("enabling addon by local dir: %s \n", addonOrDir)) 183 // args[0] is a local path install with local dir, use base dir name as addonName 184 abs, err := filepath.Abs(addonOrDir) 185 if err != nil { 186 return errors.Wrapf(err, "directory %s is invalid", addonOrDir) 187 } 188 addonName = filepath.Base(abs) 189 if !yes2all { 190 if err := checkUninstallFromClusters(ctx, k8sClient, addonName, addonArgs); err != nil { 191 return err 192 } 193 } 194 additionalInfo, err = enableAddonByLocal(ctx, addonName, addonOrDir, k8sClient, dc, config, addonArgs) 195 if err != nil { 196 return err 197 } 198 } else { 199 if filepath.IsAbs(addonOrDir) || strings.HasPrefix(addonOrDir, ".") || strings.HasSuffix(addonOrDir, "/") { 200 return fmt.Errorf("addon directory %s not found in local file system", addonOrDir) 201 } 202 _, addonName, err = splitSpecifyRegistry(name) 203 if err != nil { 204 return fmt.Errorf("failed to split addonName and addonRegistry: %w", err) 205 } 206 if !yes2all { 207 if err := checkUninstallFromClusters(ctx, k8sClient, addonName, addonArgs); err != nil { 208 return err 209 } 210 } 211 additionalInfo, err = enableAddon(ctx, k8sClient, dc, config, name, addonVersion, addonArgs) 212 if err != nil { 213 return err 214 } 215 } 216 if dryRun { 217 return nil 218 } 219 fmt.Printf("Addon %s enabled successfully.\n", addonName) 220 AdditionalEndpointPrinter(ctx, c, k8sClient, addonName, additionalInfo, false) 221 return nil 222 }, 223 } 224 225 cmd.Flags().StringVarP(&addonVersion, "version", "v", "", "specify the addon version to enable") 226 cmd.Flags().StringVarP(&addonClusters, types.ClustersArg, "c", "", "specify the runtime-clusters to enable") 227 cmd.Flags().BoolVarP(&skipValidate, "skip-version-validating", "s", false, "skip validating system version requirement") 228 cmd.Flags().BoolVarP(&overrideDefs, "override-definitions", "", false, "override existing definitions if conflict with those contained in this addon") 229 cmd.Flags().BoolVarP(&dryRun, FlagDryRun, "", false, "render all yaml files out without real execute it") 230 cmd.Flags().BoolVarP(&yes2all, "yes", "y", false, "all checks will be skipped and the default answer is yes for all validation check.") 231 return cmd 232 } 233 234 // AdditionalEndpointPrinter will print endpoints 235 func AdditionalEndpointPrinter(ctx context.Context, c common.Args, _ client.Client, name, info string, _ bool) { 236 err := printAppEndpoints(ctx, addonutil.Addon2AppName(name), types.DefaultKubeVelaNS, Filter{}, c, true) 237 if err != nil { 238 fmt.Println("Get application endpoints error:", err) 239 return 240 } 241 if len(info) > 0 { 242 fmt.Println(info) 243 } 244 } 245 246 // NewAddonUpgradeCommand create addon upgrade command 247 func NewAddonUpgradeCommand(c common.Args, ioStream cmdutil.IOStreams) *cobra.Command { 248 ctx := context.Background() 249 cmd := &cobra.Command{ 250 Use: "upgrade", 251 Short: "upgrade an addon", 252 Long: "upgrade an addon in cluster.", 253 Example: ` 254 Upgrade addon by: 255 vela addon upgrade <addon-name> 256 Upgrade addon with specify version: 257 vela addon upgrade <addon-name> --version <addon-version> 258 Upgrade addon for specific clusters, (local means control plane): 259 vela addon upgrade <addon-name> --clusters={local,cluster1,cluster2} 260 Upgrade addon locally: 261 vela addon upgrade <your-local-addon-path> 262 Upgrade addon with specified args (the args should be defined in addon's parameters): 263 vela addon upgrade <addon-name> <my-parameter-of-addon>=<my-value> 264 The specified args will be merged with legacy args, what user specified in 'vela addon enable', and non-empty legacy arg will be overridden by 265 non-empty new arg 266 `, 267 RunE: func(cmd *cobra.Command, args []string) error { 268 if len(args) < 1 { 269 return fmt.Errorf("must specify addon name") 270 } 271 config, err := c.GetConfig() 272 if err != nil { 273 return err 274 } 275 k8sClient, err := c.GetClient() 276 if err != nil { 277 return err 278 } 279 dc, err := c.GetDiscoveryClient() 280 if err != nil { 281 return err 282 } 283 addonInputArgs, err := parseAddonArgsToMap(args[1:]) 284 if err != nil { 285 return err 286 } 287 clusterArgs := transClusters(addonClusters) 288 if len(clusterArgs) != 0 { 289 addonInputArgs[types.ClustersArg] = clusterArgs 290 } 291 addonOrDir := args[0] 292 293 // inject runtime info 294 addonInputArgs[pkgaddon.InstallerRuntimeOption] = map[string]interface{}{ 295 "upgrade": true, 296 } 297 298 var name, additionalInfo string 299 if file, err := os.Stat(addonOrDir); err == nil { 300 if !file.IsDir() { 301 return fmt.Errorf("%s is not addon dir", addonOrDir) 302 } 303 ioStream.Infof(color.New(color.FgYellow).Sprintf("enabling addon by local dir: %s \n", addonOrDir)) 304 // args[0] is a local path install with local dir 305 abs, err := filepath.Abs(addonOrDir) 306 if err != nil { 307 return errors.Wrapf(err, "cannot open directory %s", addonOrDir) 308 } 309 name = filepath.Base(abs) 310 _, err = pkgaddon.FetchAddonRelatedApp(context.Background(), k8sClient, name) 311 if err != nil { 312 return errors.Wrapf(err, "cannot fetch addon related addon %s", name) 313 } 314 addonArgs, err := pkgaddon.MergeAddonInstallArgs(ctx, k8sClient, name, addonInputArgs) 315 if err != nil { 316 return err 317 } 318 additionalInfo, err = enableAddonByLocal(ctx, name, addonOrDir, k8sClient, dc, config, addonArgs) 319 if err != nil { 320 return err 321 } 322 } else { 323 if filepath.IsAbs(addonOrDir) || strings.HasPrefix(addonOrDir, ".") || strings.HasSuffix(addonOrDir, "/") { 324 return fmt.Errorf("addon directory %s not found in local", addonOrDir) 325 } 326 name = addonOrDir 327 _, err = pkgaddon.FetchAddonRelatedApp(context.Background(), k8sClient, addonOrDir) 328 if err != nil { 329 return errors.Wrapf(err, "cannot fetch addon related addon %s", addonOrDir) 330 } 331 addonArgs, err := pkgaddon.MergeAddonInstallArgs(ctx, k8sClient, name, addonInputArgs) 332 if err != nil { 333 return err 334 } 335 additionalInfo, err = enableAddon(ctx, k8sClient, dc, config, addonOrDir, addonVersion, addonArgs) 336 if err != nil { 337 return err 338 } 339 } 340 341 fmt.Printf("Addon %s enabled successfully.\n", name) 342 AdditionalEndpointPrinter(ctx, c, k8sClient, name, additionalInfo, true) 343 return nil 344 }, 345 } 346 cmd.Flags().StringVarP(&addonVersion, "version", "v", "", "specify the addon version to upgrade") 347 cmd.Flags().StringVarP(&addonClusters, types.ClustersArg, "c", "", "specify the runtime-clusters to upgrade") 348 cmd.Flags().BoolVarP(&skipValidate, "skip-version-validating", "s", false, "skip validating system version requirement") 349 cmd.Flags().BoolVarP(&overrideDefs, "override-definitions", "", false, "override existing definitions if conflict with those contained in this addon") 350 return cmd 351 } 352 353 func parseAddonArgsToMap(args []string) (map[string]interface{}, error) { 354 res := map[string]interface{}{} 355 for _, arg := range args { 356 if err := strvals.ParseInto(arg, res); err != nil { 357 return nil, err 358 } 359 } 360 return res, nil 361 } 362 363 // NewAddonDisableCommand create addon disable command 364 func NewAddonDisableCommand(c common.Args, _ cmdutil.IOStreams) *cobra.Command { 365 cmd := &cobra.Command{ 366 Use: "disable", 367 Aliases: []string{"uninstall"}, 368 Short: "disable an addon", 369 Long: "disable an addon in cluster.", 370 Example: "vela addon disable <addon-name>", 371 RunE: func(cmd *cobra.Command, args []string) error { 372 if len(args) < 1 { 373 return fmt.Errorf("must specify addon name") 374 } 375 name := args[0] 376 k8sClient, err := c.GetClient() 377 if err != nil { 378 return err 379 } 380 config, err := c.GetConfig() 381 if err != nil { 382 return err 383 } 384 err = disableAddon(k8sClient, name, config, forceDisable) 385 if err != nil { 386 return err 387 } 388 fmt.Printf("Successfully disable addon:%s\n", name) 389 return nil 390 }, 391 } 392 cmd.Flags().BoolVarP(&forceDisable, "force", "f", false, "skip checking if applications are still using this addon") 393 return cmd 394 } 395 396 // NewAddonStatusCommand create addon status command 397 func NewAddonStatusCommand(c common.Args, ioStream cmdutil.IOStreams) *cobra.Command { 398 cmd := &cobra.Command{ 399 Use: "status", 400 Short: "get an addon's status.", 401 Long: "get an addon's status from cluster.", 402 Example: "vela addon status <addon-name>", 403 RunE: func(cmd *cobra.Command, args []string) error { 404 if len(args) < 1 { 405 return fmt.Errorf("must specify addon name") 406 } 407 name := args[0] 408 err := statusAddon(name, ioStream, cmd, c) 409 if err != nil { 410 return err 411 } 412 return nil 413 }, 414 } 415 cmd.Flags().BoolVarP(&verboseStatus, "verbose", "v", false, "show addon descriptions and parameters in addition to status") 416 return cmd 417 } 418 419 // NewAddonInitCommand creates an addon scaffold 420 func NewAddonInitCommand() *cobra.Command { 421 var path string 422 initCmd := pkgaddon.InitCmd{} 423 424 cmd := &cobra.Command{ 425 Use: "init", 426 Short: "create an addon scaffold", 427 Long: "Create an addon scaffold for quick starting.", 428 Example: ` Store the scaffold in a different directory: 429 vela addon init mongodb -p path/to/addon 430 431 Add a Helm component: 432 vela addon init mongodb --helm-repo https://marketplace.azurecr.io/helm/v1/repo --chart mongodb --chart-version 12.1.16 433 434 Add resources from URL using ref-objects component 435 vela addon init my-addon --url https://domain.com/resource.yaml 436 437 Use --no-samples options to skip creating sample files 438 vela addon init my-addon --no-sample 439 440 You can combine all the options together.`, 441 RunE: func(cmd *cobra.Command, args []string) error { 442 if len(args) < 1 { 443 return fmt.Errorf("an addon name is required") 444 } 445 446 addonName := args[0] 447 448 // Scaffold will be created in ./addonName, unless the user specifies a path 449 // validity of addon names will be checked later 450 addonPath := addonName 451 if len(path) > 0 { 452 addonPath = path 453 } 454 455 if addonName == "" || addonPath == "" { 456 return fmt.Errorf("addon name or path should not be empty") 457 } 458 459 initCmd.AddonName = addonName 460 initCmd.Path = addonPath 461 462 return initCmd.CreateScaffold() 463 }, 464 } 465 466 f := cmd.Flags() 467 f.StringVar(&initCmd.HelmRepoURL, "helm-repo", "", "URL that points to a Helm repo") 468 f.StringVar(&initCmd.HelmChartName, "chart", "", "Helm Chart name") 469 f.StringVar(&initCmd.HelmChartVersion, "chart-version", "", "version of the Chart") 470 f.StringVarP(&path, "path", "p", "", "path to the addon directory (default is ./<addon-name>)") 471 f.StringArrayVarP(&initCmd.RefObjURLs, "url", "u", []string{}, "add URL resources using ref-object component") 472 f.BoolVarP(&initCmd.NoSamples, "no-samples", "", false, "do not generate sample files") 473 f.BoolVarP(&initCmd.Overwrite, "force", "f", false, "overwrite existing addon files") 474 475 return cmd 476 } 477 478 // NewAddonPushCommand pushes an addon dir/package to a ChartMuseum 479 func NewAddonPushCommand(c common.Args) *cobra.Command { 480 p := &pkgaddon.PushCmd{} 481 cmd := &cobra.Command{ 482 Use: "push", 483 Short: "uploads an addon package to ChartMuseum", 484 Long: `Uploads an addon package to ChartMuseum. 485 486 Two arguments are needed <addon directory/package> and <name/URL of ChartMuseum>. 487 488 The first argument <addon directory/package> can be: 489 - your conventional addon directory (containing metadata.yaml). We will package it for you. 490 - packaged addon (.tgz) generated by 'vela addon package' command 491 492 The second argument <name/URL of ChartMuseum> can be: 493 - registry name (helm type). You can add your ChartMuseum registry using 'vela addon registry add'. 494 - ChartMuseum URL, e.g. http://localhost:8080`, 495 Example: `# Push the addon in directory <your-addon> to a ChartMuseum registry named <localcm> 496 $ vela addon push your-addon localcm 497 498 # Push packaged addon mongo-1.0.0.tgz to a ChartMuseum registry at http://localhost:8080 499 $ vela addon push mongo-1.0.0.tgz http://localhost:8080 500 501 # Force push, overwriting existing ones 502 $ vela addon push your-addon localcm -f 503 504 # If you already written your own Chart.yaml and don't want us to generate it for you: 505 $ vela addon push your-addon localcm --keep-chartmeta 506 # Note: when using .tgz packages, we will always keep the original Chart.yaml 507 508 # In addition to cli flags, you can also use environment variables 509 $ HELM_REPO_USERNAME=name HELM_REPO_PASSWORD=pswd vela addon push mongo-1.0.0.tgz http://localhost:8080`, 510 RunE: func(cmd *cobra.Command, args []string) error { 511 if len(args) != 2 { 512 return fmt.Errorf("two arguments are needed: addon directory/package, name/URL of Chart repository") 513 } 514 515 c, err := c.GetClient() 516 if err != nil { 517 return err 518 } 519 520 p.Client = c 521 p.Out = cmd.OutOrStdout() 522 p.ChartName = args[0] 523 p.RepoName = args[1] 524 p.SetFieldsFromEnv() 525 526 return p.Push(context.Background()) 527 }, 528 } 529 530 f := cmd.Flags() 531 f.StringVarP(&p.ChartVersion, "version", "v", "", "override chart version pre-push") 532 f.StringVarP(&p.AppVersion, "app-version", "a", "", "override app version pre-push") 533 f.StringVarP(&p.Username, "username", "u", "", "override HTTP basic auth username [$HELM_REPO_USERNAME]") 534 f.StringVarP(&p.Password, "password", "p", "", "override HTTP basic auth password [$HELM_REPO_PASSWORD]") 535 f.StringVarP(&p.AccessToken, "access-token", "", "", "send token in Authorization header [$HELM_REPO_ACCESS_TOKEN]") 536 f.StringVarP(&p.AuthHeader, "auth-header", "", "", "alternative header to use for token auth [$HELM_REPO_AUTH_HEADER]") 537 f.StringVarP(&p.ContextPath, "context-path", "", "", "ChartMuseum context path [$HELM_REPO_CONTEXT_PATH]") 538 f.StringVarP(&p.CaFile, "ca-file", "", "", "verify certificates of HTTPS-enabled servers using this CA bundle [$HELM_REPO_CA_FILE]") 539 f.StringVarP(&p.CertFile, "cert-file", "", "", "identify HTTPS client using this SSL certificate file [$HELM_REPO_CERT_FILE]") 540 f.StringVarP(&p.KeyFile, "key-file", "", "", "identify HTTPS client using this SSL key file [$HELM_REPO_KEY_FILE]") 541 f.BoolVarP(&p.InsecureSkipVerify, "insecure", "", false, "connect to server with an insecure way by skipping certificate verification [$HELM_REPO_INSECURE]") 542 f.BoolVarP(&p.ForceUpload, "force", "f", false, "force upload even if chart version exists") 543 f.BoolVarP(&p.UseHTTP, "use-http", "", false, "use HTTP") 544 f.BoolVarP(&p.KeepChartMetadata, "keep-chartmeta", "", false, "do not update Chart.yaml automatically according to addon metadata (only when addon dir provided)") 545 f.Int64VarP(&p.Timeout, "timeout", "t", 30, "The duration (in seconds) vela cli will wait to get response from ChartMuseum") 546 547 return cmd 548 } 549 550 func enableAddon(ctx context.Context, k8sClient client.Client, dc *discovery.DiscoveryClient, config *rest.Config, name string, version string, args map[string]interface{}) (string, error) { 551 var err error 552 var additionalInfo string 553 registryDS := pkgaddon.NewRegistryDataStore(k8sClient) 554 registries, err := registryDS.ListRegistries(ctx) 555 if err != nil { 556 return "", err 557 } 558 registryName, addonName, err := splitSpecifyRegistry(name) 559 if err != nil { 560 return "", err 561 } 562 if len(registryName) != 0 { 563 foundRegistry := false 564 for _, registry := range registries { 565 if registry.Name == registryName { 566 foundRegistry = true 567 } 568 } 569 if !foundRegistry { 570 return "", fmt.Errorf("specified registry %s not exist", registryName) 571 } 572 } 573 for i, registry := range registries { 574 opts := addonOptions() 575 if len(registryName) != 0 && registryName != registry.Name { 576 continue 577 } 578 additionalInfo, err = pkgaddon.EnableAddon(ctx, addonName, version, k8sClient, dc, apply.NewAPIApplicator(k8sClient), config, registry, args, nil, pkgaddon.FilterDependencyRegistries(i, registries), opts...) 579 if errors.Is(err, pkgaddon.ErrNotExist) || errors.Is(err, pkgaddon.ErrFetch) { 580 continue 581 } 582 if unMatchErr := new(pkgaddon.VersionUnMatchError); errors.As(err, unMatchErr) { 583 // Get available version of the addon 584 availableVersion, err := unMatchErr.GetAvailableVersion() 585 if err != nil { 586 return "", err 587 } 588 input := NewUserInput() 589 if input.AskBool(unMatchErr.Error(), &UserInputOptions{AssumeYes: false}) { 590 return pkgaddon.EnableAddon(ctx, addonName, availableVersion, k8sClient, dc, apply.NewAPIApplicator(k8sClient), config, registry, args, nil, pkgaddon.FilterDependencyRegistries(i, registries)) 591 } 592 // The user does not agree to use the version provided by us 593 return "", fmt.Errorf("you can try another version by command: \"vela addon enable %s --version <version> \" ", addonName) 594 } 595 if err != nil { 596 return "", err 597 } 598 if err = waitApplicationRunning(k8sClient, addonName); err != nil { 599 return "", err 600 } 601 return additionalInfo, nil 602 } 603 if len(registryName) != 0 { 604 return "", fmt.Errorf("addon: %s not found in registry %s", addonName, registryName) 605 } 606 return "", fmt.Errorf("addon: %s not found in all candidate registries", addonName) 607 } 608 609 func addonOptions() []pkgaddon.InstallOption { 610 var opts []pkgaddon.InstallOption 611 if skipValidate || yes2all { 612 opts = append(opts, pkgaddon.SkipValidateVersion) 613 } 614 if overrideDefs || yes2all { 615 opts = append(opts, pkgaddon.OverrideDefinitions) 616 } 617 if dryRun { 618 opts = append(opts, pkgaddon.DryRunAddon) 619 } 620 return opts 621 } 622 623 // enableAddonByLocal enable addon in local dir and return the addon name 624 func enableAddonByLocal(ctx context.Context, name string, dir string, k8sClient client.Client, dc *discovery.DiscoveryClient, config *rest.Config, args map[string]interface{}) (string, error) { 625 opts := addonOptions() 626 info, err := pkgaddon.EnableAddonByLocalDir(ctx, name, dir, k8sClient, dc, apply.NewAPIApplicator(k8sClient), config, args, opts...) 627 if err != nil { 628 return "", err 629 } 630 if err = waitApplicationRunning(k8sClient, name); err != nil { 631 return "", err 632 } 633 return info, nil 634 } 635 636 func disableAddon(client client.Client, name string, config *rest.Config, force bool) error { 637 if err := pkgaddon.DisableAddon(context.Background(), client, name, config, force); err != nil { 638 return err 639 } 640 return nil 641 } 642 643 func statusAddon(name string, ioStreams cmdutil.IOStreams, cmd *cobra.Command, c common.Args) error { 644 k8sClient, err := c.GetClient() 645 if err != nil { 646 return err 647 } 648 649 statusString, status, err := generateAddonInfo(k8sClient, name) 650 if err != nil { 651 return err 652 } 653 654 fmt.Print(statusString) 655 656 if status.AddonPhase != statusEnabled && status.AddonPhase != statusDisabled { 657 fmt.Printf("diagnose addon info from application %s", addonutil.Addon2AppName(name)) 658 err := printAppStatus(context.Background(), k8sClient, ioStreams, addonutil.Addon2AppName(name), types.DefaultKubeVelaNS, cmd, c, false) 659 if err != nil { 660 return err 661 } 662 } 663 return nil 664 } 665 666 func addonNotExist(err error) bool { 667 if errors.Is(err, pkgaddon.ErrNotExist) || errors.Is(err, pkgaddon.ErrRegistryNotExist) { 668 return true 669 } 670 if strings.Contains(err.Error(), "not found") { 671 return true 672 } 673 return false 674 } 675 676 // generateAddonInfo will get addon status, description, version, dependencies (and whether they are installed), 677 // and parameters (and their current values). 678 // The first return value is the formatted string for printing. 679 // The second return value is just for diagnostic purposes, as it is needed in statusAddon to print diagnostic info. 680 func generateAddonInfo(c client.Client, name string) (string, pkgaddon.Status, error) { 681 var res string 682 var phase string 683 var installed bool 684 var addonPackage *pkgaddon.WholeAddonPackage 685 686 // Check current addon status 687 status, err := pkgaddon.GetAddonStatus(context.Background(), c, name) 688 if err != nil { 689 return res, status, err 690 } 691 692 // Get addon install package 693 if verboseStatus || status.AddonPhase == statusDisabled { 694 // We need the metadata to get descriptions about parameters 695 addonPackages, err := pkgaddon.FindAddonPackagesDetailFromRegistry(context.Background(), c, []string{name}, nil) 696 // If the state of addon is not disabled, we don't check the error, because it could be installed from local. 697 if status.AddonPhase == statusDisabled && err != nil { 698 if addonNotExist(err) { 699 return "", pkgaddon.Status{}, fmt.Errorf("addon '%s' not found in cluster or any registry", name) 700 } 701 return "", pkgaddon.Status{}, err 702 } 703 if len(addonPackages) != 0 { 704 addonPackage = addonPackages[0] 705 if status.InstalledRegistry != "" { 706 for _, ap := range addonPackages { 707 if ap.RegistryName == status.InstalledRegistry { 708 addonPackage = ap 709 break 710 } 711 } 712 } 713 } 714 } 715 716 switch status.AddonPhase { 717 case statusEnabled: 718 installed = true 719 c := color.New(color.FgGreen) 720 phase = c.Sprintf("%s", status.AddonPhase) 721 case statusSuspend: 722 installed = true 723 c := color.New(color.FgRed) 724 phase = c.Sprintf("%s", status.AddonPhase) 725 case statusDisabled: 726 c := color.New(color.Faint) 727 phase = c.Sprintf("%s", status.AddonPhase) 728 // If the addon is 729 // 1. disabled, 730 // 2. does not exist in the registry, 731 // 3. verbose is on (when off, it is not possible to know whether the addon is in registry or not), 732 // means the addon does not exist at all. 733 // So, no need to go further, we return error message saying that we can't find it. 734 if addonPackage == nil && verboseStatus { 735 return res, pkgaddon.Status{}, fmt.Errorf("addon %s is not found in registries nor locally installed", name) 736 } 737 default: 738 c := color.New(color.Faint) 739 phase = c.Sprintf("%s", status.AddonPhase) 740 } 741 742 // Addon name 743 res += color.New(color.Bold).Sprintf("%s", name) 744 res += fmt.Sprintf(": %s ", phase) 745 if installed { 746 res += fmt.Sprintf("(%s)", status.InstalledVersion) 747 } 748 res += "\n" 749 750 // Description 751 // Skip this if addon is installed from local sources. 752 // Description is fetched from the Internet, which is not useful for local sources. 753 if status.InstalledRegistry != pkgaddon.LocalAddonRegistryName && addonPackage != nil { 754 res += fmt.Sprintln(addonPackage.Description) 755 } 756 757 // Installed Clusters 758 if len(status.Clusters) != 0 { 759 res += color.New(color.FgHiBlue).Sprint("==> ") + color.New(color.Bold).Sprintln("Installed Clusters") 760 var ic []string 761 for c := range status.Clusters { 762 ic = append(ic, c) 763 } 764 sort.Strings(ic) 765 res += fmt.Sprintln(ic) 766 } 767 768 // Registry name 769 registryName := status.InstalledRegistry 770 // Disabled addons will have empty InstalledRegistry, so if the addon exists in the registry, we use the registry name. 771 if registryName == "" && addonPackage != nil { 772 registryName = addonPackage.RegistryName 773 } 774 if registryName != "" { 775 res += color.New(color.FgHiBlue).Sprint("==> ") + color.New(color.Bold).Sprintln("Registry Name") 776 res += fmt.Sprintln(registryName) 777 } 778 779 // If the addon is installed from local sources, or does not exist at all, stop here! 780 // The following information is fetched from the Internet, which is not useful for local sources. 781 if registryName == pkgaddon.LocalAddonRegistryName || registryName == "" || addonPackage == nil { 782 return res, status, nil 783 } 784 785 // Available Versions 786 res += color.New(color.FgHiBlue).Sprint("==> ") + color.New(color.Bold).Sprintln("Available Versions") 787 res += genAvailableVersionInfo(addonPackage.AvailableVersions, status.InstalledVersion, 8) 788 res += "\n" 789 790 // Dependencies 791 dependenciesString, allInstalled := generateDependencyString(c, addonPackage.Dependencies) 792 res += color.New(color.FgHiBlue).Sprint("==> ") + color.New(color.Bold).Sprint("Dependencies ") 793 if allInstalled { 794 res += color.GreenString("✔") 795 } else { 796 res += color.RedString("✘") 797 } 798 res += "\n" 799 res += dependenciesString 800 res += "\n" 801 802 // Parameters 803 parameterString := generateParameterString(status, addonPackage) 804 if len(parameterString) != 0 { 805 res += color.New(color.FgHiBlue).Sprint("==> ") + color.New(color.Bold).Sprintln("Parameters") 806 res += parameterString 807 } 808 809 return res, status, nil 810 } 811 812 func generateParameterString(status pkgaddon.Status, addonPackage *pkgaddon.WholeAddonPackage) string { 813 ret := "" 814 815 if addonPackage.APISchema == nil { 816 return ret 817 } 818 ret = printSchema(addonPackage.APISchema, status.Parameters, 0) 819 820 return ret 821 } 822 823 func convertInterface2StringList(l []interface{}) []string { 824 var strl []string 825 for _, s := range l { 826 str, ok := s.(string) 827 if !ok { 828 continue 829 } 830 strl = append(strl, str) 831 } 832 return strl 833 } 834 835 // printSchema prints the parameters in an addon recursively to a string 836 // Deeper the parameter is nested, more the indentations. 837 func printSchema(ref *openapi3.Schema, currentParams map[string]interface{}, indent int) string { 838 ret := "" 839 840 if ref == nil { 841 return ret 842 } 843 844 addIndent := func(n int) string { 845 r := "" 846 for i := 0; i < n; i++ { 847 r += "\t" 848 } 849 return r 850 } 851 852 // Required parameters 853 required := make(map[string]bool) 854 for _, k := range ref.Required { 855 required[k] = true 856 } 857 858 for propKey, propValue := range ref.Properties { 859 desc := propValue.Value.Description 860 defaultValue := propValue.Value.Default 861 if defaultValue == nil { 862 defaultValue = "" 863 } 864 required := required[propKey] 865 866 // Extra indentation on nested objects 867 addedIndent := addIndent(indent) 868 869 var currentValue string 870 thisParam, hasParam := currentParams[propKey] 871 if hasParam { 872 currentValue = fmt.Sprintf("%#v", thisParam) 873 switch thisParam.(type) { 874 case int: 875 case int64: 876 case int32: 877 case float32: 878 case float64: 879 case string: 880 case bool: 881 default: 882 if js, err := json.MarshalIndent(thisParam, "", " "); err == nil { 883 currentValue = strings.ReplaceAll(string(js), "\n", "\n\t "+addedIndent) 884 } 885 } 886 } 887 888 // Header: addon: description 889 ret += addedIndent 890 ret += color.New(color.FgCyan).Sprintf("-> ") 891 ret += color.New(color.Bold).Sprint(propKey) + ": " 892 ret += fmt.Sprintf("%s\n", desc) 893 894 // Show current value 895 if currentValue != "" { 896 ret += addedIndent 897 ret += "\tcurrent value: " + color.New(color.FgGreen).Sprintf("%s\n", currentValue) 898 } 899 900 // Show required or not 901 if required { 902 ret += addedIndent 903 ret += "\trequired: " 904 ret += color.GreenString("✔\n") 905 } 906 // Show Enum options 907 if len(propValue.Value.Enum) > 0 { 908 ret += addedIndent 909 ret += "\toptions: \"" + strings.Join(convertInterface2StringList(propValue.Value.Enum), "\", \"") + "\"\n" 910 } 911 // Show default value 912 if defaultValue != "" && currentValue == "" { 913 ret += addedIndent 914 ret += "\tdefault: " + fmt.Sprintf("%#v\n", defaultValue) 915 } 916 917 // Object type param, we will get inside the object. 918 // To show what's inside nested objects. 919 if propValue.Value.Type == "object" { 920 nestedParam := make(map[string]interface{}) 921 if hasParam { 922 nestedParam = currentParams[propKey].(map[string]interface{}) 923 } 924 ret += printSchema(propValue.Value, nestedParam, indent+1) 925 } 926 } 927 928 return ret 929 } 930 931 func generateDependencyString(c client.Client, dependencies []*pkgaddon.Dependency) (string, bool) { 932 if len(dependencies) == 0 { 933 return "[]", true 934 } 935 936 ret := "[" 937 allDependenciesInstalled := true 938 939 for idx, d := range dependencies { 940 name := d.Name 941 942 // Checks if the dependency is enabled, and mark it 943 status, err := pkgaddon.GetAddonStatus(context.Background(), c, name) 944 if err != nil { 945 continue 946 } 947 948 var enabledString string 949 switch status.AddonPhase { 950 case statusEnabled: 951 enabledString = color.GreenString("✔") 952 case statusSuspend: 953 enabledString = color.RedString("✔") 954 default: 955 enabledString = color.RedString("✘") 956 allDependenciesInstalled = false 957 } 958 ret += fmt.Sprintf("%s %s", name, enabledString) 959 960 if idx != len(dependencies)-1 { 961 ret += ", " 962 } 963 } 964 965 ret += "]" 966 967 return ret, allDependenciesInstalled 968 } 969 970 func listAddons(ctx context.Context, clt client.Client, registry string) (*uitable.Table, error) { 971 var addons []*pkgaddon.UIData 972 var err error 973 registryDS := pkgaddon.NewRegistryDataStore(clt) 974 registries, err := registryDS.ListRegistries(ctx) 975 if err != nil { 976 return nil, err 977 } 978 979 for _, r := range registries { 980 if registry != "" && r.Name != registry { 981 continue 982 } 983 var addonList []*pkgaddon.UIData 984 var err error 985 if !pkgaddon.IsVersionRegistry(r) { 986 meta, err := r.ListAddonMeta() 987 if err != nil { 988 continue 989 } 990 addonList, err = r.ListUIData(meta, pkgaddon.CLIMetaOptions) 991 if err != nil { 992 continue 993 } 994 } else { 995 versionedRegistry := pkgaddon.BuildVersionedRegistry(r.Name, r.Helm.URL, &common.HTTPOption{ 996 Username: r.Helm.Username, 997 Password: r.Helm.Password, 998 InsecureSkipTLS: r.Helm.InsecureSkipTLS, 999 }) 1000 addonList, err = versionedRegistry.ListAddon() 1001 if err != nil { 1002 continue 1003 } 1004 } 1005 addons = mergeAddons(addons, addonList) 1006 } 1007 1008 table := uitable.New() 1009 table.AddRow("NAME", "REGISTRY", "DESCRIPTION", "AVAILABLE-VERSIONS", "STATUS") 1010 1011 // get locally installed addons first 1012 locallyInstalledAddons := map[string]bool{} 1013 appList := v1beta1.ApplicationList{} 1014 if err := clt.List(ctx, &appList, client.MatchingLabels{oam.LabelAddonRegistry: pkgaddon.LocalAddonRegistryName}); err != nil { 1015 return table, err 1016 } 1017 for _, app := range appList.Items { 1018 labels := app.GetLabels() 1019 addonName := labels[oam.LabelAddonName] 1020 addonVersion := labels[oam.LabelAddonVersion] 1021 table.AddRow(enabledAddonColor.Sprintf("%s", addonName), app.GetLabels()[oam.LabelAddonRegistry], "", genAvailableVersionInfo([]string{addonVersion}, addonVersion, 3), enabledAddonColor.Sprintf("%s", statusEnabled)) 1022 locallyInstalledAddons[addonName] = true 1023 } 1024 1025 for _, addon := range addons { 1026 // if the addon with same name has already installed locally, display the registry one as not installed 1027 if locallyInstalledAddons[addon.Name] { 1028 table.AddRow(addon.Name, addon.RegistryName, limitStringLength(addon.Description, 60), genAvailableVersionInfo(addon.AvailableVersions, "", 3), "-") 1029 continue 1030 } 1031 status, err := pkgaddon.GetAddonStatus(ctx, clt, addon.Name) 1032 if err != nil { 1033 return table, err 1034 } 1035 statusRow := status.AddonPhase 1036 name := addon.Name 1037 if len(status.InstalledVersion) != 0 { 1038 statusRow = enabledAddonColor.Sprintf("%s (%s)", statusRow, status.InstalledVersion) 1039 name = enabledAddonColor.Sprintf("%s", name) 1040 } 1041 if statusRow == statusDisabled { 1042 statusRow = "-" 1043 } 1044 table.AddRow(name, addon.RegistryName, limitStringLength(addon.Description, 60), genAvailableVersionInfo(addon.AvailableVersions, status.InstalledVersion, 3), statusRow) 1045 } 1046 1047 return table, nil 1048 } 1049 1050 func waitApplicationRunning(k8sClient client.Client, addonName string) error { 1051 if dryRun { 1052 return nil 1053 } 1054 trackInterval := 5 * time.Second 1055 timeout := 600 * time.Second 1056 start := time.Now() 1057 ctx := context.Background() 1058 var app v1beta1.Application 1059 spinner := newTrackingSpinnerWithDelay("Waiting addon running ...", 1*time.Second) 1060 spinner.Start() 1061 defer spinner.Stop() 1062 1063 for { 1064 err := k8sClient.Get(ctx, types2.NamespacedName{Name: addonutil.Addon2AppName(addonName), Namespace: types.DefaultKubeVelaNS}, &app) 1065 if err != nil { 1066 return client.IgnoreNotFound(err) 1067 } 1068 1069 phase := app.Status.Phase 1070 if app.Generation > app.Status.ObservedGeneration { 1071 phase = common2.ApplicationStarting 1072 } else { 1073 switch app.Status.Phase { 1074 case common2.ApplicationRunning: 1075 return nil 1076 case common2.ApplicationWorkflowSuspending: 1077 fmt.Printf("Enabling suspend, please run \"vela workflow resume %s -n vela-system\" to continue", addonutil.Addon2AppName(addonName)) 1078 return nil 1079 case common2.ApplicationWorkflowTerminated, common2.ApplicationWorkflowFailed: 1080 return errors.Errorf("Enabling failed, please run \"vela status %s -n vela-system\" to check the status of the addon", addonutil.Addon2AppName(addonName)) 1081 default: 1082 } 1083 } 1084 1085 timeConsumed := int(time.Since(start).Seconds()) 1086 applySpinnerNewSuffix(spinner, fmt.Sprintf("Waiting addon application running. It is now in phase: %s (timeout %d/%d seconds)...", 1087 phase, timeConsumed, int(timeout.Seconds()))) 1088 if timeConsumed > int(timeout.Seconds()) { 1089 return errors.Errorf("Enabling timeout, please run \"vela status %s -n vela-system\" to check the status of the addon", addonutil.Addon2AppName(addonName)) 1090 } 1091 time.Sleep(trackInterval) 1092 } 1093 1094 } 1095 1096 // generate the available version 1097 // this func put the installed version as the first version and keep the origin order 1098 // print ... if available version too much 1099 func genAvailableVersionInfo(versions []string, installedVersion string, limit int) string { 1100 var v []string 1101 1102 // put installed-version as the first version and keep the origin order 1103 if len(installedVersion) != 0 { 1104 for i, version := range versions { 1105 if version == installedVersion { 1106 v = append(v, version) 1107 versions = append(versions[:i], versions[i+1:]...) 1108 } 1109 } 1110 } 1111 v = append(v, versions...) 1112 1113 res := "[" 1114 var count int 1115 for _, version := range v { 1116 if count == limit { 1117 // just show newest 3 versions 1118 res += "..." 1119 break 1120 } 1121 if version == installedVersion { 1122 res += enabledAddonColor.Sprintf("%s", version) 1123 } else { 1124 res += version 1125 } 1126 res += ", " 1127 count++ 1128 } 1129 res = strings.TrimSuffix(res, ", ") 1130 res += "]" 1131 return res 1132 } 1133 1134 // limitStringLength limits the length of the string, and add ... if it is too long 1135 func limitStringLength(str string, length int) string { 1136 if length <= 0 { 1137 return str 1138 } 1139 if len(str) > length { 1140 return str[:length] + "..." 1141 } 1142 return str 1143 } 1144 1145 // TransAddonName will turn addon's name from xxx/yyy to xxx-yyy 1146 func TransAddonName(name string) string { 1147 return strings.ReplaceAll(name, "/", "-") 1148 } 1149 1150 func mergeAddons(a1, a2 []*pkgaddon.UIData) []*pkgaddon.UIData { 1151 for _, item := range a2 { 1152 if hasAddon(a1, item.Name) { 1153 continue 1154 } 1155 a1 = append(a1, item) 1156 } 1157 return a1 1158 } 1159 1160 func hasAddon(addons []*pkgaddon.UIData, name string) bool { 1161 for _, addon := range addons { 1162 if addon.Name == name { 1163 return true 1164 } 1165 } 1166 return false 1167 } 1168 1169 func transClusters(cstr string) []interface{} { 1170 if len(cstr) == 0 { 1171 return nil 1172 } 1173 cstr = strings.TrimPrefix(strings.TrimSuffix(cstr, "}"), "{") 1174 var clusterL []interface{} 1175 clusterList := strings.Split(cstr, ",") 1176 for _, v := range clusterList { 1177 clusterL = append(clusterL, strings.TrimSpace(v)) 1178 } 1179 return clusterL 1180 } 1181 1182 // NewAddonPackageCommand create addon package command 1183 func NewAddonPackageCommand(_ common.Args) *cobra.Command { 1184 cmd := &cobra.Command{ 1185 Use: "package", 1186 Short: "package an addon directory", 1187 Long: "package an addon directory into a helm chart archive.", 1188 Example: "vela addon package <addon directory>", 1189 RunE: func(cmd *cobra.Command, args []string) error { 1190 if len(args) < 1 { 1191 return fmt.Errorf("must specify addon directory path") 1192 } 1193 addonDict, err := filepath.Abs(args[0]) 1194 if err != nil { 1195 return err 1196 } 1197 1198 archive, err := pkgaddon.PackageAddon(addonDict) 1199 if err != nil { 1200 return errors.Wrapf(err, "fail to package %s into helm chart archive", addonDict) 1201 } 1202 1203 fmt.Printf("Successfully package addon to: %s\n", archive) 1204 return nil 1205 }, 1206 } 1207 return cmd 1208 } 1209 1210 func splitSpecifyRegistry(name string) (string, string, error) { 1211 res := strings.Split(name, "/") 1212 switch len(res) { 1213 case 2: 1214 return res[0], res[1], nil 1215 case 1: 1216 return "", res[0], nil 1217 default: 1218 return "", "", fmt.Errorf("invalid addon name, you should specify name only <addonName> or with registry as prefix <registryName>/<addonName>") 1219 } 1220 } 1221 1222 func checkUninstallFromClusters(ctx context.Context, k8sClient client.Client, addonName string, args map[string]interface{}) error { 1223 status, err := pkgaddon.GetAddonStatus(ctx, k8sClient, addonName) 1224 if err != nil { 1225 return fmt.Errorf("failed to check addon status: %w", err) 1226 } 1227 if status.AddonPhase == statusDisabled { 1228 return nil 1229 } 1230 if _, ok := args["clusters"]; !ok { 1231 return nil 1232 } 1233 cList, ok := args["clusters"].([]interface{}) 1234 if !ok { 1235 return fmt.Errorf("clusters parameter must be a list of string") 1236 } 1237 1238 clusters := map[string]struct{}{} 1239 for _, c := range cList { 1240 clusterName := c.(string) 1241 clusters[clusterName] = struct{}{} 1242 } 1243 var disableClusters, installedClusters []string 1244 for c := range status.Clusters { 1245 if _, ok := clusters[c]; !ok { 1246 disableClusters = append(disableClusters, c) 1247 } 1248 installedClusters = append(installedClusters, c) 1249 } 1250 if len(disableClusters) == 0 { 1251 return nil 1252 } 1253 fmt.Println(color.New(color.FgRed).Sprintf("'%s' addon was currently installed on clusters %s, but this operation will uninstall from these clusters: %s \n", addonName, generateClustersInfo(installedClusters), generateClustersInfo(disableClusters))) 1254 input := NewUserInput() 1255 if !input.AskBool("Do you want to continue?", &UserInputOptions{AssumeYes: false}) { 1256 return fmt.Errorf("operation abort") 1257 } 1258 return nil 1259 } 1260 1261 func generateClustersInfo(clusters []string) string { 1262 ret := "[" 1263 for i, cluster := range clusters { 1264 ret += cluster 1265 if i < len(clusters)-1 { 1266 ret += "," 1267 } 1268 } 1269 ret += "]" 1270 return ret 1271 }