From 0b6ae1f01badfe6d2459bbc705985f4d30767686 Mon Sep 17 00:00:00 2001 From: Sakala Venkata Krishna Rohit Date: Tue, 3 Dec 2024 11:59:06 -0800 Subject: [PATCH] Remove app, catalog, multiclusterapp commands (#405) Ref: https://github.com/rancher/rancher/issues/48252 --- cmd/app.go | 1463 ----------------------------------- cmd/app_test.go | 23 - cmd/catalog.go | 311 -------- cmd/common.go | 75 -- cmd/multiclusterapp.go | 1334 -------------------------------- cmd/multiclusterapp_test.go | 122 --- cmd/wait.go | 2 +- go.mod | 3 +- go.sum | 2 - main.go | 3 - 10 files changed, 2 insertions(+), 3336 deletions(-) delete mode 100644 cmd/app.go delete mode 100644 cmd/app_test.go delete mode 100644 cmd/catalog.go delete mode 100644 cmd/multiclusterapp.go delete mode 100644 cmd/multiclusterapp_test.go diff --git a/cmd/app.go b/cmd/app.go deleted file mode 100644 index 42430e7e..00000000 --- a/cmd/app.go +++ /dev/null @@ -1,1463 +0,0 @@ -package cmd - -import ( - "bufio" - "encoding/base64" - "encoding/json" - "fmt" - "net/http" - "net/url" - "os" - "path/filepath" - "sort" - "strings" - "time" - - gover "github.com/hashicorp/go-version" - "github.com/pkg/errors" - "github.com/rancher/cli/cliclient" - "github.com/rancher/norman/clientbase" - clusterClient "github.com/rancher/rancher/pkg/client/generated/cluster/v3" - managementClient "github.com/rancher/rancher/pkg/client/generated/management/v3" - projectClient "github.com/rancher/rancher/pkg/client/generated/project/v3" - "github.com/sirupsen/logrus" - "github.com/urfave/cli" - "gopkg.in/yaml.v2" -) - -const ( - installAppDescription = ` -Install an app template in the current Rancher server. This defaults to the newest version of the app template. -Specify a version using '--version' if required. -The app will be installed into a new namespace unless '--namespace' is specified. - -Example: - # Install the redis template without any options - $ rancher app install redis appFoo - - # Block cli until installation has finished or encountered an error. Use after app install. - $ rancher wait - - # Install the local redis template folder without any options - $ rancher app install ./redis appFoo - - # Install the redis template and specify an answers file location - $ rancher app install --answers /example/answers.yaml redis appFoo - - # Install the redis template and set multiple answers and the version to install - $ rancher app install --set foo=bar --set-string baz=bunk --version 1.0.1 redis appFoo - - # Install the redis template and specify the namespace for the app - $ rancher app install --namespace bar redis appFoo -` - upgradeAppDescription = ` -Upgrade an existing app to a newer version via app template or app version in the current Rancher server. - -Example: - # Upgrade the 'appFoo' app to latest version without any options - $ rancher app upgrade appFoo latest - - # Upgrade the 'appFoo' app by local template folder without any options - $ rancher app upgrade appFoo ./redis - - # Upgrade the 'appFoo' app and set multiple answers and the 0.2.0 version to install - $ rancher app upgrade --set foo=bar --set-string baz=bunk appFoo 0.2.0 -` -) - -type AppData struct { - ID string - App projectClient.App - Catalog string - Template string - Version string -} - -type TemplateData struct { - ID string - Template managementClient.Template - Category string -} - -type VersionData struct { - Current string - Version string -} - -type revision struct { - Current string - Name string - Created time.Time - Human string - Catalog string - Template string - Version string -} - -type chartVersion struct { - chartMetadata `yaml:",inline"` - Dir string `json:"-" yaml:"-"` - URLs []string `json:"urls" yaml:"urls"` - Digest string `json:"digest,omitempty" yaml:"digest,omitempty"` -} - -type chartMetadata struct { - Name string `json:"name,omitempty" yaml:"name,omitempty"` - Sources []string `json:"sources,omitempty" yaml:"sources,omitempty"` - Version string `json:"version,omitempty" yaml:"version,omitempty"` - KubeVersion string `json:"kubeVersion,omitempty" yaml:"kubeVersion,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Keywords []string `json:"keywords,omitempty" yaml:"keywords,omitempty"` - Icon string `json:"icon,omitempty" yaml:"icon,omitempty"` -} - -type revSlice []revision - -func (s revSlice) Less(i, j int) bool { return s[i].Created.After(s[j].Created) } -func (s revSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } -func (s revSlice) Len() int { return len(s) } - -func AppCommand() cli.Command { - appLsFlags := []cli.Flag{ - formatFlag, - cli.BoolFlag{ - Name: "quiet,q", - Usage: "Only display IDs", - }, - } - - return cli.Command{ - Name: "apps", - Aliases: []string{"app"}, - Usage: "Operations with apps. Uses helm. Flags prepended with \"helm\" can also be accurately described by helm documentation.", - Action: defaultAction(appLs), - Flags: appLsFlags, - Subcommands: []cli.Command{ - { - Name: "ls", - Usage: "List apps", - Description: "\nList all apps in the current Rancher server", - ArgsUsage: "None", - Action: appLs, - Flags: appLsFlags, - }, - { - Name: "delete", - Usage: "Delete an app", - Action: appDelete, - ArgsUsage: "[APP_NAME/APP_ID]", - }, - { - Name: "install", - Usage: "Install an app template", - Description: installAppDescription, - Action: templateInstall, - ArgsUsage: "[TEMPLATE_NAME/TEMPLATE_PATH, APP_NAME]", - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "answers,a", - Usage: "Path to an answers file, the format of the file is a map with key:value. This supports JSON and YAML.", - }, - cli.StringFlag{ - Name: "values", - Usage: "Path to a helm values file.", - }, - cli.StringFlag{ - Name: "namespace,n", - Usage: "Namespace to install the app into", - }, - cli.StringSliceFlag{ - Name: "set", - Usage: "Set answers for the template, can be used multiple times. Example: --set foo=bar", - }, - cli.StringSliceFlag{ - Name: "set-string", - Usage: "Set string answers for the template (Skips Helm's type conversion), can be used multiple times. Example: --set-string foo=bar", - }, - cli.StringFlag{ - Name: "version", - Usage: "Version of the template to use", - }, - cli.BoolFlag{ - Name: "no-prompt", - Usage: "Suppress asking questions and use the default values when required answers are not provided", - }, - cli.IntFlag{ - Name: "helm-timeout", - Usage: "Amount of time for helm to wait for k8s commands (default is 300 secs). Example: --helm-timeout 600", - Value: 300, - }, - cli.BoolFlag{ - Name: "helm-wait", - Usage: "Helm will wait for as long as timeout value, for installed resources to be ready (pods, PVCs, deployments, etc.). Example: --helm-wait", - }, - }, - }, - { - Name: "rollback", - Usage: "Rollback an app to a previous version", - Action: appRollback, - ArgsUsage: "[APP_NAME/APP_ID, REVISION_ID/REVISION_NAME]", - Flags: []cli.Flag{ - cli.BoolFlag{ - Name: "show-revisions,r", - Usage: "Show revisions available to rollback to", - }, - cli.BoolFlag{ - Name: "force,f", - Usage: "Force rollback, deletes and recreates resources if needed during rollback. (default is false)", - }, - }, - }, - { - Name: "upgrade", - Usage: "Upgrade an existing app to a newer version", - Description: upgradeAppDescription, - Action: appUpgrade, - ArgsUsage: "[APP_NAME/APP_ID VERSION/TEMPLATE_PATH]", - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "answers,a", - Usage: "Path to an answers file, the format of the file is a map with key:value. Supports JSON and YAML", - }, - cli.StringFlag{ - Name: "values", - Usage: "Path to a helm values file.", - }, - cli.StringSliceFlag{ - Name: "set", - Usage: "Set answers for the template, can be used multiple times. Example: --set foo=bar", - }, - cli.StringSliceFlag{ - Name: "set-string", - Usage: "Set string answers for the template (Skips Helm's type conversion), can be used multiple times. Example: --set-string foo=bar", - }, - cli.BoolFlag{ - Name: "show-versions,v", - Usage: "Display versions available to upgrade to", - }, - cli.BoolFlag{ - Name: "reset", - Usage: "Reset all catalog app answers", - }, - cli.BoolFlag{ - Name: "force,f", - Usage: "Force upgrade, deletes and recreates resources if needed during upgrade. (default is false)", - }, - }, - }, - { - Name: "list-templates", - Aliases: []string{"lt"}, - Usage: "List templates available for installation", - Description: "\nList all app templates in the current Rancher server", - ArgsUsage: "None", - Action: templateLs, - Flags: []cli.Flag{ - formatFlag, - cli.StringFlag{ - Name: "catalog", - Usage: "Specify the catalog to list templates for", - }, - }, - }, - { - Name: "show-template", - Aliases: []string{"st"}, - Usage: "Show versions available to install for an app template", - Description: "\nShow all available versions of an app template", - ArgsUsage: "[TEMPLATE_ID]", - Action: templateShow, - }, - { - Name: "show-app", - Aliases: []string{"sa"}, - Usage: "Show an app's available versions and revisions", - ArgsUsage: "[APP_NAME/APP_ID]", - Action: showApp, - Flags: []cli.Flag{ - formatFlag, - }, - }, - { - Name: "show-notes", - Usage: "Show contents of apps notes.txt", - Action: appNotes, - ArgsUsage: "[APP_NAME/APP_ID]", - }, - }, - } -} - -func appLs(ctx *cli.Context) error { - c, err := GetClient(ctx) - if err != nil { - return err - } - - collection, err := c.ProjectClient.App.List(defaultListOpts(ctx)) - if err != nil { - return err - } - - writer := NewTableWriter([][]string{ - {"ID", "ID"}, - {"NAME", "App.Name"}, - {"STATE", "App.State"}, - {"CATALOG", "Catalog"}, - {"TEMPLATE", "Template"}, - {"VERSION", "Version"}, - }, ctx) - - defer writer.Close() - - for _, item := range collection.Data { - appExternalID := item.ExternalID - appTemplateFiles := make(map[string]string) - if appExternalID == "" { - // add namespace prefix to AppRevisionID to create a Rancher API style ID - appRevisionID := strings.Replace(item.ID, item.Name, item.AppRevisionID, -1) - - appRevision, err := c.ProjectClient.AppRevision.ByID(appRevisionID) - if err != nil { - return err - } - - if appRevision.Status != nil { - appTemplateFiles = appRevision.Status.Files - } - } - - parsedInfo, err := parseTemplateInfo(appExternalID, appTemplateFiles) - if err != nil { - return err - } - - appData := &AppData{ - ID: item.ID, - App: item, - Catalog: parsedInfo["catalog"], - Template: parsedInfo["template"], - Version: parsedInfo["version"], - } - writer.Write(appData) - } - return writer.Err() - -} - -func parseTemplateInfo(appExternalID string, appTemplateFiles map[string]string) (map[string]string, error) { - if appExternalID != "" { - parsedExternal, parseErr := parseExternalID(appExternalID) - if parseErr != nil { - return nil, errors.Wrap(parseErr, "failed to parse ExternalID from app") - } - - return parsedExternal, nil - } - - for fileName, fileContent := range appTemplateFiles { - if strings.HasSuffix(fileName, "/Chart.yaml") || strings.HasSuffix(fileName, "/Chart.yml") { - content, decodeErr := base64.StdEncoding.DecodeString(fileContent) - if decodeErr != nil { - return nil, errors.Wrap(decodeErr, "failed to decode Chart.yaml from app") - } - - version := &chartVersion{} - unmarshalErr := yaml.Unmarshal(content, version) - if unmarshalErr != nil { - return nil, errors.Wrap(unmarshalErr, "failed to parse Chart.yaml from app") - } - - return map[string]string{ - "catalog": "local directory", - "template": version.Name, - "version": version.Version, - }, nil - } - } - - return nil, errors.New("can't parse info from app") -} - -func appDelete(ctx *cli.Context) error { - if ctx.NArg() == 0 { - return cli.ShowSubcommandHelp(ctx) - } - - c, err := GetClient(ctx) - if err != nil { - return err - } - - for _, arg := range ctx.Args() { - resource, err := Lookup(c, arg, "app") - if err != nil { - return err - } - - app, err := c.ProjectClient.App.ByID(resource.ID) - if err != nil { - return err - } - - err = c.ProjectClient.App.Delete(app) - if err != nil { - return err - } - } - - return nil - -} - -func appUpgrade(ctx *cli.Context) error { - c, err := GetClient(ctx) - if err != nil { - return err - } - - if ctx.Bool("show-versions") { - return outputVersions(ctx, c) - } - - if ctx.NArg() < 2 { - return cli.ShowSubcommandHelp(ctx) - } - - appName := ctx.Args().First() - appVersionOrLocalTemplatePath := ctx.Args().Get(1) - - resource, err := Lookup(c, appName, "app") - if err != nil { - return err - } - - app, err := c.ProjectClient.App.ByID(resource.ID) - if err != nil { - return err - } - - answers := app.Answers - answersSetString := app.AnswersSetString - values := app.ValuesYaml - answers, answersSetString, err = processAnswerUpdates(ctx, answers, answersSetString) - if err != nil { - return err - } - values, err = processValueUpgrades(ctx, values) - if err != nil { - return err - } - - force := ctx.Bool("force") - - au := &projectClient.AppUpgradeConfig{ - Answers: answers, - AnswersSetString: answersSetString, - ForceUpgrade: force, - ValuesYaml: values, - } - - if resolveTemplatePath(appVersionOrLocalTemplatePath) { - // if it is a path, upgrade install charts locally - localTemplatePath := appVersionOrLocalTemplatePath - _, files, err := walkTemplateDirectory(localTemplatePath) - if err != nil { - return err - } - - au.Files = files - } else { - appVersion := appVersionOrLocalTemplatePath - externalID, err := updateExternalIDVersion(app.ExternalID, appVersion) - if err != nil { - return err - } - - filter := defaultListOpts(ctx) - filter.Filters["externalId"] = externalID - - template, err := c.ManagementClient.TemplateVersion.List(filter) - if err != nil { - return err - } - if len(template.Data) == 0 { - return fmt.Errorf("version %s is not valid", appVersion) - } - - au.ExternalID = template.Data[0].ExternalID - } - - return c.ProjectClient.App.ActionUpgrade(app, au) -} - -func updateExternalIDVersion(externalID string, version string) (string, error) { - u, err := url.Parse(externalID) - if err != nil { - return "", err - } - - oldVersionQuery := fmt.Sprintf("version=%s", u.Query().Get("version")) - newVersionQuery := fmt.Sprintf("version=%s", version) - return strings.Replace(externalID, oldVersionQuery, newVersionQuery, 1), nil -} - -func appRollback(ctx *cli.Context) error { - c, err := GetClient(ctx) - if err != nil { - return err - } - - if ctx.Bool("show-revisions") { - return outputRevisions(ctx, c) - } - - if ctx.NArg() < 2 { - return cli.ShowSubcommandHelp(ctx) - } - - force := ctx.Bool("force") - - resource, err := Lookup(c, ctx.Args().First(), "app") - if err != nil { - return err - } - - app, err := c.ProjectClient.App.ByID(resource.ID) - if err != nil { - return err - } - - revisionResource, err := Lookup(c, ctx.Args().Get(1), "appRevision") - if err != nil { - return err - } - - revision, err := c.ProjectClient.AppRevision.ByID(revisionResource.ID) - if err != nil { - return err - } - - rr := &projectClient.RollbackRevision{ - ForceUpgrade: force, - RevisionID: revision.Name, - } - - return c.ProjectClient.App.ActionRollback(app, rr) -} - -func templateLs(ctx *cli.Context) error { - c, err := GetClient(ctx) - if err != nil { - return err - } - - filter := defaultListOpts(ctx) - if ctx.String("app") != "" { - resource, err := Lookup(c, ctx.String("app"), "app") - if err != nil { - return err - } - filter.Filters["appId"] = resource.ID - } - - collection, err := c.ManagementClient.Template.List(filter) - if err != nil { - return err - } - - writer := NewTableWriter([][]string{ - {"ID", "ID"}, - {"NAME", "Template.Name"}, - {"CATEGORY", "Category"}, - }, ctx) - - defer writer.Close() - - for _, item := range collection.Data { - writer.Write(&TemplateData{ - ID: item.ID, - Template: item, - Category: strings.Join(item.Categories, ","), - }) - } - - return writer.Err() -} - -func templateShow(ctx *cli.Context) error { - if ctx.NArg() == 0 { - return cli.ShowSubcommandHelp(ctx) - } - - c, err := GetClient(ctx) - if err != nil { - return err - } - - resource, err := Lookup(c, ctx.Args().First(), "template") - if err != nil { - return err - } - - template, err := getFilteredTemplate(ctx, c, resource.ID) - if err != nil { - return err - } - - sortedVersions, err := sortTemplateVersions(template) - if err != nil { - return err - } - - if len(sortedVersions) == 0 { - fmt.Println("No app versions available to install for this version of Rancher server") - } - - for _, version := range sortedVersions { - fmt.Println(version) - } - - return nil -} - -func templateInstall(ctx *cli.Context) error { - if ctx.NArg() == 0 { - return cli.ShowSubcommandHelp(ctx) - } - templateName := ctx.Args().First() - appName := ctx.Args().Get(1) - - c, err := GetClient(ctx) - if err != nil { - return err - } - - app := &projectClient.App{ - Name: appName, - } - if resolveTemplatePath(templateName) { - // if it is a path, install charts locally - chartName, files, err := walkTemplateDirectory(templateName) - if err != nil { - return err - } - answers, answersSetString, err := processAnswerInstall(ctx, nil, nil, nil, false, false) - if err != nil { - return err - } - values, err := processValueInstall(ctx, nil, "") - if err != nil { - return err - } - - app.Files = files - app.Answers = answers - app.AnswersSetString = answersSetString - app.ValuesYaml = values - namespace := ctx.String("namespace") - if namespace == "" { - namespace = chartName + "-" + RandomLetters(5) - } - err = createNamespace(c, namespace) - if err != nil { - return err - } - app.TargetNamespace = namespace - } else { - resource, err := Lookup(c, templateName, "template") - if err != nil { - return err - } - - template, err := getFilteredTemplate(ctx, c, resource.ID) - if err != nil { - return err - } - - latestVersion, err := getTemplateLatestVersion(template) - if err != nil { - return err - } - - templateVersionID := templateVersionIDFromVersionLink(template.VersionLinks[latestVersion]) - userVersion := ctx.String("version") - if userVersion != "" { - if link, ok := template.VersionLinks[userVersion]; ok { - templateVersionID = templateVersionIDFromVersionLink(link) - } else { - return fmt.Errorf( - "version %s for template %s is invalid, run 'rancher app show-template %s' for a list of versions", - userVersion, - templateName, - templateName, - ) - } - } - - templateVersion, err := c.ManagementClient.TemplateVersion.ByID(templateVersionID) - if err != nil { - return err - } - - interactive := !ctx.Bool("no-prompt") - answers, answersSetString, err := processAnswerInstall(ctx, templateVersion, nil, nil, interactive, false) - if err != nil { - return err - } - values, err := processValueInstall(ctx, templateVersion, "") - if err != nil { - return err - } - namespace := ctx.String("namespace") - if namespace == "" { - namespace = template.Name + "-" + RandomLetters(5) - } - err = createNamespace(c, namespace) - if err != nil { - return err - } - app.Answers = answers - app.AnswersSetString = answersSetString - app.ValuesYaml = values - app.ExternalID = templateVersion.ExternalID - app.TargetNamespace = namespace - } - - app.Wait = ctx.Bool("helm-wait") - app.Timeout = ctx.Int64("helm-timeout") - - madeApp, err := c.ProjectClient.App.Create(app) - if err != nil { - return err - } - - fmt.Printf("run \"app show-notes %s\" to view app notes once app is ready\n", madeApp.Name) - - return nil -} - -// appNotes prints notes from app's notes.txt file -func appNotes(ctx *cli.Context) error { - c, err := GetClient(ctx) - if err != nil { - return err - } - - if ctx.NArg() < 1 { - return cli.ShowSubcommandHelp(ctx) - } - - resource, err := Lookup(c, ctx.Args().First(), "app") - if err != nil { - return err - } - - app, err := c.ProjectClient.App.ByID(resource.ID) - if err != nil { - return err - } - - if len(app.Notes) > 0 { - fmt.Println(app.Notes) - } else { - fmt.Println("no notes to print") - } - - return nil -} - -func resolveTemplatePath(templateName string) bool { - return templateName == "." || strings.Contains(templateName, "\\\\") || strings.Contains(templateName, "/") -} - -func walkTemplateDirectory(templatePath string) (string, map[string]string, error) { - templateAbsPath, parsedErr := filepath.Abs(templatePath) - if parsedErr != nil { - return "", nil, parsedErr - } - if _, statErr := os.Stat(templateAbsPath); statErr != nil { - return "", nil, statErr - } - - var ( - chartName string - files = make(map[string]string) - err error - ) - err = filepath.Walk(templateAbsPath, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if info.IsDir() { - return nil - } - - if !strings.EqualFold(info.Name(), "Chart.yaml") { - return nil - } - version := &chartVersion{} - content, err := os.ReadFile(path) - if err != nil { - return err - } - - rootDir := filepath.Dir(path) - if err := yaml.Unmarshal(content, version); err != nil { - return err - } - chartName = version.Name - err = filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if info.IsDir() { - return nil - } - content, err := os.ReadFile(path) - if err != nil { - return err - } - if len(content) > 0 { - key := filepath.Join(chartName, strings.TrimPrefix(path, rootDir+"/")) - files[key] = base64.StdEncoding.EncodeToString(content) - } - return nil - }) - if err != nil { - return err - } - - return filepath.SkipDir - }) - - return chartName, files, err -} - -func showApp(ctx *cli.Context) error { - if ctx.NArg() == 0 { - return cli.ShowSubcommandHelp(ctx) - } - - c, err := GetClient(ctx) - if err != nil { - return err - } - - err = outputRevisions(ctx, c) - if err != nil { - return err - } - - fmt.Println() - - err = outputVersions(ctx, c) - if err != nil { - return err - } - return nil -} - -func outputVersions(ctx *cli.Context, c *cliclient.MasterClient) error { - if ctx.NArg() == 0 { - return cli.ShowSubcommandHelp(ctx) - } - - resource, err := Lookup(c, ctx.Args().First(), "app") - if err != nil { - return err - } - - app, err := c.ProjectClient.App.ByID(resource.ID) - if err != nil { - return err - } - - externalID := app.ExternalID - if externalID == "" { - // local folder app doesn't show any version information - return nil - } - - externalInfo, err := parseExternalID(externalID) - if err != nil { - return err - } - - template, err := getFilteredTemplate(ctx, c, "cattle-global-data:"+externalInfo["catalog"]+"-"+externalInfo["template"]) - if err != nil { - return err - } - - sortedVersions, err := sortTemplateVersions(template) - if err != nil { - return err - } - - if len(sortedVersions) == 0 { - fmt.Println("No app versions available to install for this version of Rancher server") - return nil - } - - writer := NewTableWriter([][]string{ - {"CURRENT", "Current"}, - {"VERSION", "Version"}, - }, ctx) - - defer writer.Close() - - for _, version := range sortedVersions { - var current string - if version.String() == externalInfo["version"] { - current = "*" - } - writer.Write(&VersionData{ - Current: current, - Version: version.String(), - }) - } - return writer.Err() -} - -func outputRevisions(ctx *cli.Context, c *cliclient.MasterClient) error { - if ctx.NArg() == 0 { - return cli.ShowSubcommandHelp(ctx) - } - - resource, err := Lookup(c, ctx.Args().First(), "app") - if err != nil { - return err - } - - app, err := c.ProjectClient.App.ByID(resource.ID) - if err != nil { - return err - } - - revisions := &projectClient.AppRevisionCollection{} - err = c.ProjectClient.GetLink(*resource, "revision", revisions) - if err != nil { - return err - } - - var sorted revSlice - for _, rev := range revisions.Data { - parsedTime, err := time.Parse(time.RFC3339, rev.Created) - if err != nil { - return err - } - - parsedInfo, err := parseTemplateInfo(rev.Status.ExternalID, rev.Status.Files) - if err != nil { - return err - } - - reversionData := revision{ - Name: rev.Name, - Created: parsedTime, - Catalog: parsedInfo["catalog"], - Template: parsedInfo["template"], - Version: parsedInfo["version"], - } - sorted = append(sorted, reversionData) - } - - sort.Sort(sorted) - - writer := NewTableWriter([][]string{ - {"CURRENT", "Current"}, - {"REVISION", "Name"}, - {"CATALOG", "Catalog"}, - {"TEMPLATE", "Template"}, - {"VERSION", "Version"}, - {"CREATED", "Human"}, - }, ctx) - - defer writer.Close() - - for _, rev := range sorted { - if rev.Name == app.AppRevisionID { - rev.Current = "*" - } - rev.Human = rev.Created.Format("02 Jan 2006 15:04:05 MST") - - writer.Write(rev) - } - return writer.Err() -} - -func templateVersionIDFromVersionLink(s string) string { - pieces := strings.Split(s, "/") - return pieces[len(pieces)-1] -} - -// parseExternalID gives back a map with the keys catalog, template and version -func parseExternalID(e string) (map[string]string, error) { - parsed := make(map[string]string) - u, err := url.Parse(e) - if err != nil { - return parsed, err - } - q := u.Query() - for key, value := range q { - if len(value) > 0 { - parsed[key] = value[0] - } - } - return parsed, nil -} - -// getFilteredTemplate uses the rancherVersion in the template request to get the -// filtered template with incompatable versions dropped -func getFilteredTemplate(ctx *cli.Context, c *cliclient.MasterClient, templateID string) (*managementClient.Template, error) { - ver, err := getRancherServerVersion(c) - if err != nil { - return nil, err - } - - filter := defaultListOpts(ctx) - filter.Filters["id"] = templateID - filter.Filters["rancherVersion"] = ver - - template, err := c.ManagementClient.Template.List(filter) - if err != nil { - return nil, err - } - - if len(template.Data) == 0 { - return nil, fmt.Errorf("template %v not found", templateID) - } - return &template.Data[0], nil -} - -// getTemplateLatestVersion returns the newest version of the template -func getTemplateLatestVersion(template *managementClient.Template) (string, error) { - if len(template.VersionLinks) == 0 { - return "", errors.New("no versions found for this template (the chart you are trying to install may be intentionally hidden or deprecated for your Rancher version)") - } - sorted, err := sortTemplateVersions(template) - if err != nil { - return "", err - } - - return sorted[len(sorted)-1].String(), nil -} - -func sortTemplateVersions(template *managementClient.Template) ([]*gover.Version, error) { - var versions []*gover.Version - for key := range template.VersionLinks { - v, err := gover.NewVersion(key) - if err != nil { - return nil, err - } - versions = append(versions, v) - } - - sort.Sort(gover.Collection(versions)) - return versions, nil -} - -// createNamespace checks if a namespace exists and creates it if needed -func createNamespace(c *cliclient.MasterClient, n string) error { - filter := defaultListOpts(nil) - filter.Filters["name"] = n - namespaces, err := c.ClusterClient.Namespace.List(filter) - if err != nil { - return err - } - - if len(namespaces.Data) == 0 { - newNamespace := &clusterClient.Namespace{ - Name: n, - ProjectID: c.UserConfig.Project, - } - - ns, err := c.ClusterClient.Namespace.Create(newNamespace) - if err != nil { - return err - } - - nsID := ns.ID - startTime := time.Now() - for { - logrus.Debugf("Namespace create wait - Name: %s, State: %s, Transitioning: %s", ns.Name, ns.State, ns.Transitioning) - if time.Since(startTime)/time.Second > 30 { - return fmt.Errorf("timed out waiting for new namespace %s", ns.Name) - } - ns, err = c.ClusterClient.Namespace.ByID(nsID) - if err != nil { - if e, ok := err.(*clientbase.APIError); ok && e.StatusCode == http.StatusForbidden { - //the new namespace is created successfully but cannot be got when RBAC rules are not ready. - continue - } - return err - } - - if ns.State == "active" { - break - } - - time.Sleep(500 * time.Millisecond) - } - } else { - if namespaces.Data[0].ProjectID != c.UserConfig.Project { - return fmt.Errorf("namespace %s already exists", n) - } - } - return nil -} - -// processValueInstall creates a map of the values file and fills in missing entries with defaults -func processValueInstall(ctx *cli.Context, tv *managementClient.TemplateVersion, existingValues string) (string, error) { - values, err := processValues(ctx, existingValues) - if err != nil { - return existingValues, err - } - // add default values if entries missing from map - err = fillInDefaultAnswers(tv, values) - if err != nil { - return existingValues, err - } - - // change map back into string to be consistent with ui - existingValues, err = parseMapToYamlString(values) - if err != nil { - return existingValues, err - } - return existingValues, nil -} - -// processValueUpgrades creates map from existing values and applies updates -func processValueUpgrades(ctx *cli.Context, existingValues string) (string, error) { - values, err := processValues(ctx, existingValues) - if err != nil { - return existingValues, err - } - // change map back into string to be consistent with ui - existingValues, err = parseMapToYamlString(values) - if err != nil { - return existingValues, err - } - return existingValues, nil -} - -// processValues creates a map of the values file -func processValues(ctx *cli.Context, existingValues string) (map[string]interface{}, error) { - var err error - values := make(map[string]interface{}) - if existingValues != "" { - // parse values into map to ensure previous values are considered on update - values, err = createValuesMap([]byte(existingValues)) - if err != nil { - return values, err - } - } - if ctx.String("values") != "" { - // if values file passed in, overwrite defaults with new key value pair - values, err = parseFile(ctx.String("values")) - if err != nil { - return values, err - } - } - return values, nil -} - -// processAnswerInstall adds answers to given map, and prompts users to answers chart questions if interactive is true -func processAnswerInstall( - ctx *cli.Context, - tv *managementClient.TemplateVersion, - answers, - answersSetString map[string]string, - interactive bool, - multicluster bool, -) (map[string]string, map[string]string, error) { - var err error - answers, answersSetString, err = processAnswerUpdates(ctx, answers, answersSetString) - if err != nil { - return answers, answersSetString, err - } - // interactive occurs before adding defaults to ensure all questions are asked - if interactive { - // answers to questions will be added to map - err := askQuestions(tv, answers) - if err != nil { - return answers, answersSetString, err - } - } - if multicluster && !interactive { - // add default values if answers missing from map - err = fillInDefaultAnswersStringMap(tv, answers) - if err != nil { - return answers, answersSetString, err - } - } - return answers, answersSetString, nil -} - -func processAnswerUpdates(ctx *cli.Context, answers, answersSetString map[string]string) (map[string]string, map[string]string, error) { - logrus.Println("ok") - if answers == nil || ctx.Bool("reset") { - // this would not be possible without returning a map - answers = make(map[string]string) - } - if answersSetString == nil || ctx.Bool("reset") { - // this would not be possible without returning a map - answersSetString = make(map[string]string) - } - if ctx.String("answers") != "" { - err := parseAnswersFile(ctx.String("answers"), answers) - if err != nil { - return answers, answersSetString, err - } - } - for _, answer := range ctx.StringSlice("set") { - parts := strings.SplitN(answer, "=", 2) - if len(parts) == 2 { - answers[parts[0]] = parts[1] - } - } - for _, answer := range ctx.StringSlice("set-string") { - parts := strings.SplitN(answer, "=", 2) - logrus.Printf("%v\n", parts) - if len(parts) == 2 { - answersSetString[parts[0]] = parts[1] - } - } - return answers, answersSetString, nil -} - -// parseMapToYamlString create yaml string from answers map -func parseMapToYamlString(answerMap map[string]interface{}) (string, error) { - yamlFileString, err := yaml.Marshal(answerMap) - if err != nil { - return "", err - } - return string(yamlFileString), nil -} - -func parseAnswersFile(location string, answers map[string]string) error { - holder, err := parseFile(location) - if err != nil { - return err - } - for key, value := range holder { - switch value.(type) { - case nil: - answers[key] = "" - default: - answers[key] = fmt.Sprintf("%v", value) - } - } - return nil -} - -func parseFile(location string) (map[string]interface{}, error) { - bytes, err := os.ReadFile(location) - if err != nil { - return nil, err - } - return createValuesMap(bytes) -} - -func createValuesMap(bytes []byte) (map[string]interface{}, error) { - values := make(map[string]interface{}) - if hasPrefix(bytes, []byte("{")) { - // this is the check that "readFileReturnJSON" uses to differentiate between JSON and YAML - if err := json.Unmarshal(bytes, &values); err != nil { - return nil, err - } - } else { - if err := yaml.Unmarshal(bytes, &values); err != nil { - return nil, err - } - } - return values, nil -} - -func askQuestions(tv *managementClient.TemplateVersion, answers map[string]string) error { - var asked bool - var attempts int - if tv == nil { - return nil - } - for { - attempts++ - for _, question := range tv.Questions { - if _, ok := answers[question.Variable]; !ok && checkShowIfStringMap(question.ShowIf, answers) { - asked = true - answers[question.Variable] = askQuestion(question) - if checkShowSubquestionIfStringMap(question, answers) { - for _, subQuestion := range question.Subquestions { - // only ask the question if there is not an answer and it passes the ShowIf check - if _, ok := answers[subQuestion.Variable]; !ok && checkShowIfStringMap(subQuestion.ShowIf, answers) { - answers[subQuestion.Variable] = askSubQuestion(subQuestion) - } - } - } - } - } - if !asked { - return nil - } else if attempts >= 10 { - return errors.New("attempted questions 10 times") - } - asked = false - } -} - -func askQuestion(q managementClient.Question) string { - if len(q.Description) > 0 { - fmt.Printf("\nDescription: %s\n", q.Description) - } - - if len(q.Options) > 0 { - options := strings.Join(q.Options, ", ") - fmt.Printf("Accepted Options: %s\n", options) - } - - fmt.Printf("Name: %s\nVariable Name: %s\nDefault:[%s]\nEnter answer or 'return' for default:", q.Label, q.Variable, q.Default) - - answer, err := bufio.NewReader(os.Stdin).ReadString('\n') - if err != nil { - return "" - } - - answer = strings.TrimSpace(answer) - if answer == "" { - answer = q.Default - } - - return answer -} - -func askSubQuestion(q managementClient.SubQuestion) string { - if len(q.Description) > 0 { - fmt.Printf("\nDescription: %s\n", q.Description) - } - - if len(q.Options) > 0 { - options := strings.Join(q.Options, ", ") - fmt.Printf("Accepted Options: %s\n", options) - } - - fmt.Printf("Name: %s\nVariable Name: %s\nDefault:[%s]\nEnter answer or 'return' for default:", q.Label, q.Variable, q.Default) - - answer, err := bufio.NewReader(os.Stdin).ReadString('\n') - if err != nil { - return "" - } - - answer = strings.TrimSpace(answer) - if answer == "" { - answer = q.Default - } - - return answer -} - -// fillInDefaultAnswers parses through questions and creates an answer map with default answers if missing from map -func fillInDefaultAnswers(tv *managementClient.TemplateVersion, answers map[string]interface{}) error { - if tv == nil { - return nil - } - for _, question := range tv.Questions { - if _, ok := answers[question.Variable]; !ok && checkShowIf(question.ShowIf, answers) { - answers[question.Variable] = question.Default - if checkShowSubquestionIf(question, answers) { - for _, subQuestion := range question.Subquestions { - // set the sub-question if the showIf check passes - if _, ok := answers[subQuestion.Variable]; !ok && checkShowIf(subQuestion.ShowIf, answers) { - answers[subQuestion.Variable] = subQuestion.Default - } - } - } - } - } - if answers == nil { - return errors.New("could not generate default answers") - } - return nil -} - -// checkShowIf uses the ShowIf field to determine if a question should be asked -// this field comes in the format = where key is a question id and value is the answer -func checkShowIf(s string, answers map[string]interface{}) bool { - // No ShowIf so always ask the question - if len(s) == 0 { - return true - } - - pieces := strings.Split(s, "=") - if len(pieces) != 2 { - return false - } - - //if the key exists and the val matches the expression ask the question - if val, ok := answers[pieces[0]]; ok && fmt.Sprintf("%v", val) == pieces[1] { - return true - } - return false -} - -// fillInDefaultAnswersStringMap parses through questions and creates an answer map with default answers if missing from map -func fillInDefaultAnswersStringMap(tv *managementClient.TemplateVersion, answers map[string]string) error { - if tv == nil { - return nil - } - for _, question := range tv.Questions { - if _, ok := answers[question.Variable]; !ok && checkShowIfStringMap(question.ShowIf, answers) { - answers[question.Variable] = question.Default - if checkShowSubquestionIfStringMap(question, answers) { - for _, subQuestion := range question.Subquestions { - // set the sub-question if the showIf check passes - if _, ok := answers[subQuestion.Variable]; !ok && checkShowIfStringMap(subQuestion.ShowIf, answers) { - answers[subQuestion.Variable] = subQuestion.Default - } - } - } - } - } - if answers == nil { - return errors.New("could not generate default answers") - } - return nil -} - -// checkShowIfStringMap uses the ShowIf field to determine if a question should be asked -// this field comes in the format = where key is a question id and value is the answer -func checkShowIfStringMap(s string, answers map[string]string) bool { - // No ShowIf so always ask the question - if len(s) == 0 { - return true - } - - pieces := strings.Split(s, "=") - if len(pieces) != 2 { - return false - } - - //if the key exists and the val matches the expression ask the question - if val, ok := answers[pieces[0]]; ok && val == pieces[1] { - return true - } - return false -} - -func checkShowSubquestionIf(q managementClient.Question, answers map[string]interface{}) bool { - if val, ok := answers[q.Variable]; ok { - if fmt.Sprintf("%v", val) == q.ShowSubquestionIf { - return true - } - } - return false -} - -func checkShowSubquestionIfStringMap(q managementClient.Question, answers map[string]string) bool { - if val, ok := answers[q.Variable]; ok { - if val == q.ShowSubquestionIf { - return true - } - } - return false -} diff --git a/cmd/app_test.go b/cmd/app_test.go deleted file mode 100644 index 189003d6..00000000 --- a/cmd/app_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package cmd - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestGetExternalIDInVersion(t *testing.T) { - assert := assert.New(t) - - got, err := updateExternalIDVersion("catalog://?catalog=library&template=cert-manager&version=v0.5.2", "v1.2.3") - assert.Nil(err) - assert.Equal("catalog://?catalog=library&template=cert-manager&version=v1.2.3", got) - - got, err = updateExternalIDVersion("catalog://?catalog=c-29wkq/clusterscope&type=clusterCatalog&template=mysql&version=0.3.8", "0.3.9") - assert.Nil(err) - assert.Equal("catalog://?catalog=c-29wkq/clusterscope&type=clusterCatalog&template=mysql&version=0.3.9", got) - - got, err = updateExternalIDVersion("catalog://?catalog=p-j9gfw/projectscope&type=projectCatalog&template=grafana&version=0.0.31", "0.0.30") - assert.Nil(err) - assert.Equal("catalog://?catalog=p-j9gfw/projectscope&type=projectCatalog&template=grafana&version=0.0.30", got) -} diff --git a/cmd/catalog.go b/cmd/catalog.go deleted file mode 100644 index 1633f152..00000000 --- a/cmd/catalog.go +++ /dev/null @@ -1,311 +0,0 @@ -package cmd - -import ( - "strings" - "time" - - "github.com/pkg/errors" - managementClient "github.com/rancher/rancher/pkg/client/generated/management/v3" - "github.com/sirupsen/logrus" - "github.com/urfave/cli" -) - -const ( - addCatalogDescription = ` -Add a new catalog to the Rancher server - -Example: - # Add a catalog - $ rancher catalog add foo https://my.catalog - - # Add a catalog and specify the branch to use - $ rancher catalog add --branch awesomebranch foo https://my.catalog - - # Add a catalog and specify the helm version to use. Specify 'v2' for helm 2 and 'v3' for helm 3 - $ rancher catalog add --helm-version v3 foo https://my.catalog -` - - refreshCatalogDescription = ` -Refresh a catalog on the Rancher server - -Example: - # Refresh a catalog - $ rancher catalog refresh foo - - # Refresh multiple catalogs - $ rancher catalog refresh foo bar baz - - # Refresh all catalogs - $ rancher catalog refresh --all - - # Refresh is asynchronous unless you specify '--wait' - $ rancher catalog refresh --all --wait --wait-timeout=60 - - # Default wait timeout is 60 seconds, set to 0 to remove the timeout - $ rancher catalog refresh --all --wait --wait-timeout=0 -` -) - -type CatalogData struct { - ID string - Catalog managementClient.Catalog -} - -func CatalogCommand() cli.Command { - catalogLsFlags := []cli.Flag{ - formatFlag, - quietFlag, - cli.BoolFlag{ - Name: "verbose,v", - Usage: "Include the catalog's state", - }, - } - - return cli.Command{ - Name: "catalog", - Usage: "Operations with catalogs", - Action: defaultAction(catalogLs), - Flags: catalogLsFlags, - Subcommands: []cli.Command{ - { - Name: "ls", - Usage: "List catalogs", - Description: "\nList all catalogs in the current Rancher server", - ArgsUsage: "None", - Action: catalogLs, - Flags: catalogLsFlags, - }, - { - Name: "add", - Usage: "Add a catalog", - Description: addCatalogDescription, - ArgsUsage: "[NAME, URL]", - Action: catalogAdd, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "branch", - Usage: "Branch from the url to use", - Value: "master", - }, - cli.StringFlag{ - Name: "helm-version", - Usage: "Version of helm the app(s) in your catalog will use for deployment. Use 'v2' for helm 2 or 'v3' for helm 3", - Value: "v2", - }, - }, - }, - { - Name: "delete", - Usage: "Delete a catalog", - Description: "\nDelete a catalog from the Rancher server", - ArgsUsage: "[CATALOG_NAME/CATALOG_ID]", - Action: catalogDelete, - }, - { - Name: "refresh", - Usage: "Refresh catalog templates", - Description: refreshCatalogDescription, - ArgsUsage: "[CATALOG_NAME/CATALOG_ID]...", - Action: catalogRefresh, - Flags: []cli.Flag{ - cli.BoolFlag{ - Name: "all", - Usage: "Refresh all catalogs", - }, - cli.BoolFlag{ - Name: "wait,w", - Usage: "Wait for catalog(s) to become active", - }, - cli.IntFlag{ - Name: "wait-timeout", - Usage: "Wait timeout duration in seconds", - Value: 60, - }, - }, - }, - }, - } -} - -func catalogLs(ctx *cli.Context) error { - c, err := GetClient(ctx) - if err != nil { - return err - } - - collection, err := c.ManagementClient.Catalog.List(defaultListOpts(ctx)) - if err != nil { - return err - } - - fields := [][]string{ - {"ID", "ID"}, - {"NAME", "Catalog.Name"}, - {"URL", "Catalog.URL"}, - {"BRANCH", "Catalog.Branch"}, - {"KIND", "Catalog.Kind"}, - {"HELMVERSION", "Catalog.HelmVersion"}, - } - - if ctx.Bool("verbose") { - fields = append(fields, []string{"STATE", "Catalog.State"}) - } - - writer := NewTableWriter(fields, ctx) - - defer writer.Close() - - for _, item := range collection.Data { - writer.Write(&CatalogData{ - ID: item.ID, - Catalog: item, - }) - } - - return writer.Err() - -} - -func catalogAdd(ctx *cli.Context) error { - if len(ctx.Args()) < 2 { - return cli.ShowSubcommandHelp(ctx) - } - - c, err := GetClient(ctx) - if err != nil { - return err - } - - catalog := &managementClient.Catalog{ - Branch: ctx.String("branch"), - Name: ctx.Args().First(), - Kind: "helm", - URL: ctx.Args().Get(1), - HelmVersion: strings.ToLower(ctx.String("helm-version")), - } - - _, err = c.ManagementClient.Catalog.Create(catalog) - if err != nil { - return err - } - - return nil -} - -func catalogDelete(ctx *cli.Context) error { - if len(ctx.Args()) < 1 { - return cli.ShowSubcommandHelp(ctx) - } - - c, err := GetClient(ctx) - if err != nil { - return err - } - - for _, arg := range ctx.Args() { - resource, err := Lookup(c, arg, "catalog") - if err != nil { - return err - } - - catalog, err := c.ManagementClient.Catalog.ByID(resource.ID) - if err != nil { - return err - } - - err = c.ManagementClient.Catalog.Delete(catalog) - if err != nil { - return err - } - } - return nil -} - -func catalogRefresh(ctx *cli.Context) error { - if len(ctx.Args()) < 1 && !ctx.Bool("all") { - return cli.ShowSubcommandHelp(ctx) - } - - c, err := GetClient(ctx) - if err != nil { - return err - } - - var catalogs []managementClient.Catalog - - if ctx.Bool("all") { - opts := baseListOpts() - - collection, err := c.ManagementClient.Catalog.List(opts) - if err != nil { - return err - } - - // save the catalogs in case we need to wait for them to become active - catalogs = collection.Data - - _, err = c.ManagementClient.Catalog.CollectionActionRefresh(collection) - if err != nil { - return err - } - - } else { - for _, arg := range ctx.Args() { - resource, err := Lookup(c, arg, "catalog") - if err != nil { - return err - } - - catalog, err := c.ManagementClient.Catalog.ByID(resource.ID) - if err != nil { - return err - } - - // collect the refreshing catalogs in case we need to wait for them later - catalogs = append(catalogs, *catalog) - - _, err = c.ManagementClient.Catalog.ActionRefresh(catalog) - if err != nil { - return err - } - } - } - - if ctx.Bool("wait") { - timeout := time.Duration(ctx.Int("wait-timeout")) * time.Second - start := time.Now() - - logrus.Debugf("catalog: waiting for catalogs to become active (timeout=%v)", timeout) - - for _, catalog := range catalogs { - - logrus.Debugf("catalog: waiting for %s to become active", catalog.Name) - - resource, err := Lookup(c, catalog.Name, "catalog") - if err != nil { - return err - } - - catalog, err := c.ManagementClient.Catalog.ByID(resource.ID) - if err != nil { - return err - } - - for catalog.State != "active" { - time.Sleep(time.Second) - catalog, err = c.ManagementClient.Catalog.ByID(resource.ID) - if err != nil { - return err - } - - if timeout > 0 && time.Since(start) > timeout { - return errors.New("catalog: timed out waiting for refresh") - } - } - - } - logrus.Debugf("catalog: waited for %v", time.Since(start)) - } - - return nil -} diff --git a/cmd/common.go b/cmd/common.go index 18062b07..0dadb8fd 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -248,14 +248,6 @@ func searchForMember(ctx *cli.Context, c *cliclient.MasterClient, name string) ( return &results.Data[selection], nil } -func getRancherServerVersion(c *cliclient.MasterClient) (string, error) { - setting, err := c.ManagementClient.Setting.ByID("server-version") - if err != nil { - return "", err - } - return setting.Value, err -} - func loadAndVerifyCert(path string) (string, error) { caCert, err := os.ReadFile(path) if err != nil { @@ -625,73 +617,6 @@ func createdTimetoHuman(t string) (string, error) { return parsedTime.Format(humanTimeFormat), nil } -func outputMembers(ctx *cli.Context, c *cliclient.MasterClient, members []managementClient.Member) error { - writer := NewTableWriter([][]string{ - {"NAME", "Name"}, - {"MEMBER_TYPE", "MemberType"}, - {"ACCESS_TYPE", "AccessType"}, - }, ctx) - - defer writer.Close() - - for _, m := range members { - principalID := m.UserPrincipalID - if m.UserPrincipalID == "" { - principalID = m.GroupPrincipalID - } - principal, err := c.ManagementClient.Principal.ByID(url.PathEscape(principalID)) - if err != nil { - return err - } - - memberType := fmt.Sprintf("%s %s", principal.Provider, principal.PrincipalType) - writer.Write(&MemberData{ - Name: principal.Name, - MemberType: cases.Title(language.Und).String(memberType), - AccessType: m.AccessType, - }) - } - return writer.Err() -} - -func addMembersByNames(ctx *cli.Context, c *cliclient.MasterClient, members []managementClient.Member, toAddMembers []string, accessType string) ([]managementClient.Member, error) { - for _, name := range toAddMembers { - member, err := searchForMember(ctx, c, name) - if err != nil { - return nil, err - } - - toAddMember := managementClient.Member{ - AccessType: accessType, - } - if member.PrincipalType == "user" { - toAddMember.UserPrincipalID = member.ID - } else { - toAddMember.GroupPrincipalID = member.ID - } - members = append(members, toAddMember) - } - return members, nil -} - -func deleteMembersByNames(ctx *cli.Context, c *cliclient.MasterClient, members []managementClient.Member, todeleteMembers []string) ([]managementClient.Member, error) { - for _, name := range todeleteMembers { - member, err := searchForMember(ctx, c, name) - if err != nil { - return nil, err - } - - var toKeepMembers []managementClient.Member - for _, m := range members { - if m.GroupPrincipalID != member.ID && m.UserPrincipalID != member.ID { - toKeepMembers = append(toKeepMembers, m) - } - } - members = toKeepMembers - } - return members, nil -} - func ConfigDir() (string, error) { homeDir, err := os.UserHomeDir() if err != nil { diff --git a/cmd/multiclusterapp.go b/cmd/multiclusterapp.go deleted file mode 100644 index c8829620..00000000 --- a/cmd/multiclusterapp.go +++ /dev/null @@ -1,1334 +0,0 @@ -package cmd - -import ( - "fmt" - "reflect" - "sort" - "strings" - "time" - - "github.com/rancher/cli/cliclient" - "github.com/rancher/norman/types" - "github.com/rancher/norman/types/slice" - managementClient "github.com/rancher/rancher/pkg/client/generated/management/v3" - "github.com/sirupsen/logrus" - "github.com/urfave/cli" -) - -const ( - installMultiClusterAppDescription = ` -Install a multi-cluster app in the current Rancher server. This defaults to the newest version of the app template. -Specify a version using '--version' if required. - -Example: - # Install the redis template with no other options - $ rancher multiclusterapp install redis appFoo - - # Install the redis template and specify an answers file location - $ rancher multiclusterapp install --answers /example/answers.yaml redis appFoo - - # Install the redis template and set multiple answers and the version to install - $ rancher multiclusterapp install --set foo=bar --set-string baz=bunk --version 1.0.1 redis appFoo - - # Install the redis template and set target projects to install - $ rancher multiclusterapp install --target mycluster:Default --target c-98pjr:p-w6c5f redis appFoo - - # Block cli until installation has finished or encountered an error. Use after multiclusterapp install. - $ rancher wait -` - upgradeStrategySimultaneously = "simultaneously" - upgradeStrategyRollingUpdate = "rolling-update" - argUpgradeStrategy = "upgrade-strategy" - argUpgradeBatchSize = "upgrade-batch-size" - argUpgradeBatchInterval = "upgrade-batch-interval" -) - -var ( - memberAccessTypes = []string{"owner", "member", "read-only"} - upgradeStrategies = []string{upgradeStrategySimultaneously, upgradeStrategyRollingUpdate} -) - -type MultiClusterAppData struct { - ID string - App managementClient.MultiClusterApp - Version string - Targets string -} - -type scopeAnswers struct { - Answers map[string]string - AnswersSetString map[string]string -} - -func MultiClusterAppCommand() cli.Command { - appLsFlags := []cli.Flag{ - formatFlag, - cli.BoolFlag{ - Name: "quiet,q", - Usage: "Only display IDs", - }, - } - - return cli.Command{ - Name: "multiclusterapps", - Aliases: []string{"multiclusterapp", "mcapps", "mcapp"}, - Usage: "Operations with multi-cluster apps", - Action: defaultAction(multiClusterAppLs), - Flags: appLsFlags, - Subcommands: []cli.Command{ - { - Name: "ls", - Usage: "List multi-cluster apps", - Description: "\nList all multi-cluster apps in the current Rancher server", - ArgsUsage: "None", - Action: multiClusterAppLs, - Flags: appLsFlags, - }, - { - Name: "delete", - Usage: "Delete a multi-cluster app", - Action: multiClusterAppDelete, - ArgsUsage: "[APP_NAME]", - }, - { - Name: "install", - Usage: "Install a multi-cluster app", - Description: installMultiClusterAppDescription, - Action: multiClusterAppTemplateInstall, - ArgsUsage: "[TEMPLATE_NAME, APP_NAME]...", - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "answers,a", - Usage: "Path to an answers file, the format of the file is a map with key:value. This supports JSON and YAML.", - }, - cli.StringFlag{ - Name: "values", - Usage: "Path to a helm values file.", - }, - cli.StringSliceFlag{ - Name: "set", - Usage: "Set answers for the template, can be used multiple times. You can set overriding answers for specific clusters or projects " + - "by providing cluster ID or project ID as the prefix. Example: --set foo=bar --set c-rvcrl:foo=bar --set c-rvcrl:p-8w2x8:foo=bar", - }, - cli.StringSliceFlag{ - Name: "set-string", - Usage: "Set string answers for the template (Skips Helm's type conversion), can be used multiple times. You can set overriding answers for specific clusters or projects " + - "by providing cluster ID or project ID as the prefix. Example: --set-string foo=bar --set-string c-rvcrl:foo=bar --set-string c-rvcrl:p-8w2x8:foo=bar", - }, - cli.StringFlag{ - Name: "version", - Usage: "Version of the template to use", - }, - cli.BoolFlag{ - Name: "no-prompt", - Usage: "Suppress asking questions and use the default values when required answers are not provided", - }, - cli.StringSliceFlag{ - Name: "target,t", - Usage: "Target project names/ids to install the app into", - }, - cli.StringSliceFlag{ - Name: "role", - Usage: "Set roles required to launch/manage the apps in target projects. For example, set \"project-member\" role when the app needs to manage resources " + - "in the projects in which it is deployed. Or set \"cluster-owner\" role when the app needs to manage resources in the clusters in which it is deployed. " + - "(default: \"project-member\")", - }, - cli.StringSliceFlag{ - Name: "member", - Usage: "Set members of the app, with the same access type defined by --member-access-type", - }, - cli.StringFlag{ - Name: "member-access-type", - Usage: "Access type of the members. Specify only one value, and it applies to all members defined by --member. Valid options are 'owner', 'member' and 'read-only'", - Value: "owner", - }, - cli.StringFlag{ - Name: argUpgradeStrategy, - Usage: "Strategy for upgrade. Valid options are \"rolling-update\" and \"simultaneously\"", - Value: upgradeStrategySimultaneously, - }, - cli.Int64Flag{ - Name: argUpgradeBatchSize, - Usage: "The number of apps in target projects to be upgraded at a time. Only used if --upgrade-strategy is rolling-update.", - Value: 1, - }, - cli.Int64Flag{ - Name: argUpgradeBatchInterval, - Usage: "The number of seconds between updating the next app during upgrade. Only used if --upgrade-strategy is rolling-update.", - Value: 1, - }, - cli.IntFlag{ - Name: "helm-timeout", - Usage: "Amount of time for helm to wait for k8s commands (default is 300 secs). Example: --helm-timeout 600", - Value: 300, - }, - cli.BoolFlag{ - Name: "helm-wait", - Usage: "Helm will wait for as long as timeout value, for installed resources to be ready (pods, PVCs, deployments, etc.). Example: --helm-wait", - }, - }, - }, - { - Name: "rollback", - Usage: "Rollback a multi-cluster app to a previous version", - Action: multiClusterAppRollback, - ArgsUsage: "[APP_NAME/APP_ID, REVISION_ID/REVISION_NAME]", - Flags: []cli.Flag{ - cli.BoolFlag{ - Name: "show-revisions,r", - Usage: "Show revisions available to rollback to", - }, - }, - }, - { - Name: "upgrade", - Usage: "Upgrade an app to a newer version", - Action: multiClusterAppUpgrade, - ArgsUsage: "[APP_NAME/APP_ID VERSION]", - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "answers,a", - Usage: "Path to an answers file, the format of the file is a map with key:value. Supports JSON and YAML", - }, - cli.StringFlag{ - Name: "values", - Usage: "Path to a helm values file.", - }, - cli.StringSliceFlag{ - Name: "set", - Usage: "Set answers for the template, can be used multiple times. You can set overriding answers for specific clusters or projects " + - "by providing cluster ID or project ID as the prefix. Example: --set foo=bar --set c-rvcrl:foo=bar --set c-rvcrl:p-8w2x8:foo=bar", - }, - cli.StringSliceFlag{ - Name: "set-string", - Usage: "Set string answers for the template (Skips Helm's type conversion), can be used multiple times. You can set overriding answers for specific clusters or projects " + - "by providing cluster ID or project ID as the prefix. Example: --set-string foo=bar --set-string c-rvcrl:foo=bar --set-string c-rvcrl:p-8w2x8:foo=bar", - }, - cli.BoolFlag{ - Name: "reset", - Usage: "Reset all catalog app answers", - }, - cli.StringSliceFlag{ - Name: "role,r", - Usage: "Set roles required to launch/manage the apps in target projects. Specified roles on upgrade will override all the original roles. " + - "For example, provide all existing roles if you want to add additional roles. Leave it empty to keep current roles", - }, - cli.BoolFlag{ - Name: "show-versions,v", - Usage: "Display versions available to upgrade to", - }, - cli.StringFlag{ - Name: argUpgradeStrategy, - Usage: "Strategy for upgrade. Valid options are \"rolling-update\" and \"simultaneously\"", - }, - cli.Int64Flag{ - Name: argUpgradeBatchSize, - Usage: "The number of apps in target projects to be upgraded at a time. Only used if --upgrade-strategy is rolling-update.", - }, - cli.Int64Flag{ - Name: argUpgradeBatchInterval, - Usage: "The number of seconds between updating the next app during upgrade. Only used if --upgrade-strategy is rolling-update.", - }, - }, - }, - { - Name: "add-project", - Usage: "Add target projects to a multi-cluster app", - Action: addMcappTargetProject, - Description: "Examples:\n #Add 'p1' project in cluster 'mycluster' to target projects of a multi-cluster app named 'myapp'\n rancher multiclusterapp add-project myapp mycluster:p1\n", - ArgsUsage: "[APP_NAME/APP_ID, CLUSTER_NAME:PROJECT_NAME/PROJECT_ID...]", - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "answers,a", - Usage: "Path to an answers file that provides overriding answers for the new target projects, the format of the file is a map with key:value. Supports JSON and YAML", - }, - cli.StringFlag{ - Name: "values", - Usage: "Path to a helm values file that provides overriding answers for the new target projects", - }, - cli.StringSliceFlag{ - Name: "set", - Usage: "Set overriding answers for the new target projects", - }, - cli.StringSliceFlag{ - Name: "set-string", - Usage: "Set overriding string answers for the new target projects", - }, - }, - }, - { - Name: "delete-project", - Usage: "Delete target projects from a multi-cluster app", - Action: deleteMcappTargetProject, - Description: "Examples:\n #Delete 'p1' project in cluster 'mycluster' from target projects of a multi-cluster app named 'myapp'\n rancher multiclusterapp delete-project myapp mycluster:p1\n", - ArgsUsage: "[APP_NAME/APP_ID, CLUSTER_NAME:PROJECT_NAME/PROJECT_ID...]", - }, - { - Name: "add-member", - Usage: "Add members to a multi-cluster app", - Action: addMcappMember, - Description: "Examples:\n #Add 'user1' and 'user2' as the owners of a multi-cluster app named 'myapp'\n rancher multiclusterapp add-member myapp owner user1 user2\n", - ArgsUsage: "[APP_NAME/APP_ID, ACCESS_TYPE, USER_NAME/USER_ID...]", - }, - { - Name: "delete-member", - Usage: "Delete members from a multi-cluster app", - Action: deleteMcappMember, - Description: "Examples:\n #Delete the membership of a user named 'user1' from a multi-cluster app named 'myapp'\n rancher multiclusterapp delete-member myapp user1\n", - ArgsUsage: "[APP_NAME/APP_ID, USER_NAME/USER_ID...]", - }, - { - Name: "list-members", - Aliases: []string{"lm"}, - Usage: "List current members of a multi-cluster app", - ArgsUsage: "[APP_NAME/APP_ID]", - Action: listMultiClusterAppMembers, - Flags: []cli.Flag{ - formatFlag, - }, - }, - { - Name: "list-answers", - Aliases: []string{"la"}, - Usage: "List current answers of a multi-cluster app", - ArgsUsage: "[APP_NAME/APP_ID]", - Action: listMultiClusterAppAnswers, - Flags: []cli.Flag{ - formatFlag, - }, - }, - { - Name: "list-templates", - Aliases: []string{"lt"}, - Usage: "List templates available for installation", - Description: "\nList all app templates in the current Rancher server", - ArgsUsage: "None", - Action: globalTemplateLs, - Flags: []cli.Flag{ - formatFlag, - cli.StringFlag{ - Name: "catalog", - Usage: "Specify the catalog to list templates for", - }, - }, - }, - { - Name: "show-template", - Aliases: []string{"st"}, - Usage: "Show versions available to install for an app template", - Description: "\nShow all available versions of an app template", - ArgsUsage: "[TEMPLATE_ID]", - Action: templateShow, - }, - { - Name: "show-app", - Aliases: []string{"sa"}, - Usage: "Show an app's available versions and revisions", - ArgsUsage: "[APP_NAME/APP_ID]", - Action: showMultiClusterApp, - Flags: []cli.Flag{ - formatFlag, - cli.BoolFlag{ - Name: "show-roles", - Usage: "Show roles required to manage the app", - }, - }, - }, - }, - } -} - -func multiClusterAppLs(ctx *cli.Context) error { - c, err := GetClient(ctx) - if err != nil { - return err - } - - collection, err := c.ManagementClient.MultiClusterApp.List(defaultListOpts(ctx)) - if err != nil { - return err - } - - writer := NewTableWriter([][]string{ - {"ID", "ID"}, - {"NAME", "App.Name"}, - {"STATE", "App.State"}, - {"VERSION", "Version"}, - {"TARGET_PROJECTS", "Targets"}, - }, ctx) - - defer writer.Close() - - clusterCache, projectCache, err := getClusterProjectMap(ctx, c.ManagementClient) - if err != nil { - return err - } - - templateVersionCache := make(map[string]string) - for _, item := range collection.Data { - version, err := getTemplateVersion(c.ManagementClient, templateVersionCache, item.TemplateVersionID) - if err != nil { - return err - } - targetNames := getReadableTargetNames(clusterCache, projectCache, item.Targets) - writer.Write(&MultiClusterAppData{ - ID: item.ID, - App: item, - Version: version, - Targets: strings.Join(targetNames, ","), - }) - } - return writer.Err() -} - -func getTemplateVersion(client *managementClient.Client, templateVersionCache map[string]string, ID string) (string, error) { - var version string - if cachedVersion, ok := templateVersionCache[ID]; ok { - version = cachedVersion - } else { - templateVersion, err := client.TemplateVersion.ByID(ID) - if err != nil { - return "", err - } - templateVersionCache[templateVersion.ID] = templateVersion.Version - version = templateVersion.Version - } - return version, nil -} - -func getClusterProjectMap(ctx *cli.Context, client *managementClient.Client) (map[string]managementClient.Cluster, map[string]managementClient.Project, error) { - clusters := make(map[string]managementClient.Cluster) - clusterCollectionData, err := listAllClusters(ctx, client) - if err != nil { - return nil, nil, err - } - for _, c := range clusterCollectionData { - clusters[c.ID] = c - } - projects := make(map[string]managementClient.Project) - projectCollectionData, err := listAllProjects(ctx, client) - if err != nil { - return nil, nil, err - } - for _, p := range projectCollectionData { - projects[p.ID] = p - } - return clusters, projects, nil -} - -func listAllClusters(ctx *cli.Context, client *managementClient.Client) ([]managementClient.Cluster, error) { - clusterCollection, err := client.Cluster.List(defaultListOpts(ctx)) - if err != nil { - return nil, err - } - clusterCollectionData := clusterCollection.Data - for { - clusterCollection, err = clusterCollection.Next() - if err != nil { - return nil, err - } - if clusterCollection == nil { - break - } - clusterCollectionData = append(clusterCollectionData, clusterCollection.Data...) - if !clusterCollection.Pagination.Partial { - break - } - } - return clusterCollectionData, nil -} - -func listAllProjects(ctx *cli.Context, client *managementClient.Client) ([]managementClient.Project, error) { - projectCollection, err := client.Project.List(defaultListOpts(ctx)) - if err != nil { - return nil, err - } - projectCollectionData := projectCollection.Data - for { - projectCollection, err = projectCollection.Next() - if err != nil { - return nil, err - } - if projectCollection == nil { - break - } - projectCollectionData = append(projectCollectionData, projectCollection.Data...) - if !projectCollection.Pagination.Partial { - break - } - } - return projectCollectionData, nil -} - -func getReadableTargetNames(clusterCache map[string]managementClient.Cluster, projectCache map[string]managementClient.Project, targets []managementClient.Target) []string { - var targetNames []string - for _, target := range targets { - projectID := target.ProjectID - clusterID, _ := parseScope(projectID) - cluster, ok := clusterCache[clusterID] - if !ok { - logrus.Debugf("Cannot get readable name for target %q, showing ID", target.ProjectID) - targetNames = append(targetNames, target.ProjectID) - continue - } - project, ok := projectCache[projectID] - if !ok { - logrus.Debugf("Cannot get readable name for target %q, showing ID", target.ProjectID) - targetNames = append(targetNames, target.ProjectID) - continue - } - targetNames = append(targetNames, concatScope(cluster.Name, project.Name)) - } - return targetNames -} - -func multiClusterAppDelete(ctx *cli.Context) error { - if ctx.NArg() == 0 { - return cli.ShowSubcommandHelp(ctx) - } - - c, err := GetClient(ctx) - if err != nil { - return err - } - - for _, name := range ctx.Args() { - _, app, err := searchForMcapp(c, name) - if err != nil { - return err - } - - err = c.ManagementClient.MultiClusterApp.Delete(app) - if err != nil { - return err - } - } - - return nil -} - -func multiClusterAppUpgrade(ctx *cli.Context) error { - c, err := GetClient(ctx) - if err != nil { - return err - } - - if ctx.Bool("show-versions") { - if ctx.NArg() == 0 { - return cli.ShowSubcommandHelp(ctx) - } - - _, app, err := searchForMcapp(c, ctx.Args().First()) - if err != nil { - return err - } - - return outputMultiClusterAppVersions(ctx, c, app) - } - - if ctx.NArg() != 2 { - return cli.ShowSubcommandHelp(ctx) - } - - upgradeStrategy := strings.ToLower(ctx.String(argUpgradeStrategy)) - if ctx.IsSet(argUpgradeStrategy) && !slice.ContainsString(upgradeStrategies, upgradeStrategy) { - return fmt.Errorf("invalid upgrade-strategy %q, supported values are \"rolling-update\" and \"simultaneously\"", upgradeStrategy) - } - - _, app, err := searchForMcapp(c, ctx.Args().First()) - if err != nil { - return err - } - - update := make(map[string]interface{}) - answers, answersSetString := fromMultiClusterAppAnswers(app.Answers) - answers, answersSetString, err = processAnswerUpdates(ctx, answers, answersSetString) - if err != nil { - return err - } - update["answers"], err = toMultiClusterAppAnswers(c, answers, answersSetString) - if err != nil { - return err - } - - version := ctx.Args().Get(1) - templateVersion, err := c.ManagementClient.TemplateVersion.ByID(app.TemplateVersionID) - if err != nil { - return err - } - toUpgradeTemplateversionID := strings.TrimSuffix(templateVersion.ID, templateVersion.Version) + version - // Check if the template version is valid before applying it - _, err = c.ManagementClient.TemplateVersion.ByID(toUpgradeTemplateversionID) - if err != nil { - templateName := strings.TrimSuffix(toUpgradeTemplateversionID, "-"+version) - return fmt.Errorf( - "version %s for template %s is invalid, run 'rancher mcapp show-template %s' for available versions", - version, - templateName, - templateName, - ) - } - update["templateVersionId"] = toUpgradeTemplateversionID - - roles := ctx.StringSlice("role") - if len(roles) > 0 { - update["roles"] = roles - } else { - update["roles"] = app.Roles - } - - if upgradeStrategy == upgradeStrategyRollingUpdate { - update["upgradeStrategy"] = &managementClient.UpgradeStrategy{ - RollingUpdate: &managementClient.RollingUpdate{ - BatchSize: ctx.Int64(argUpgradeBatchSize), - Interval: ctx.Int64(argUpgradeBatchInterval), - }, - } - } else if upgradeStrategy == upgradeStrategySimultaneously { - update["upgradeStrategy"] = nil - } - - if _, err := c.ManagementClient.MultiClusterApp.Update(app, update); err != nil { - return err - } - - return nil -} - -func multiClusterAppRollback(ctx *cli.Context) error { - if ctx.NArg() == 0 { - return cli.ShowSubcommandHelp(ctx) - } - - c, err := GetClient(ctx) - if err != nil { - return err - } - - resource, app, err := searchForMcapp(c, ctx.Args().First()) - if err != nil { - return err - } - - if ctx.Bool("show-revisions") { - return outputMultiClusterAppRevisions(ctx, c, resource, app) - } - - if ctx.NArg() != 2 { - return cli.ShowSubcommandHelp(ctx) - } - - revisionResource, err := Lookup(c, ctx.Args().Get(1), managementClient.MultiClusterAppRevisionType) - if err != nil { - return err - } - - rr := &managementClient.MultiClusterAppRollbackInput{ - RevisionID: revisionResource.ID, - } - - if err := c.ManagementClient.MultiClusterApp.ActionRollback(app, rr); err != nil { - return err - } - - return nil -} - -func multiClusterAppTemplateInstall(ctx *cli.Context) error { - if ctx.NArg() > 2 { - return cli.ShowSubcommandHelp(ctx) - } - - templateName := ctx.Args().First() - appName := ctx.Args().Get(1) - - c, err := GetClient(ctx) - if err != nil { - return err - } - - roles := ctx.StringSlice("role") - if len(roles) == 0 { - // Handle the default here because the cli default value for stringSlice do not get overridden. - roles = []string{"project-member"} - } - - app := &managementClient.MultiClusterApp{ - Name: appName, - Roles: roles, - } - - upgradeStrategy := strings.ToLower(ctx.String(argUpgradeStrategy)) - if !slice.ContainsString(upgradeStrategies, upgradeStrategy) { - return fmt.Errorf("invalid upgrade-strategy %q, supported values are \"rolling-update\" and \"simultaneously\"", upgradeStrategy) - } else if upgradeStrategy == upgradeStrategyRollingUpdate { - app.UpgradeStrategy = &managementClient.UpgradeStrategy{ - RollingUpdate: &managementClient.RollingUpdate{ - BatchSize: ctx.Int64(argUpgradeBatchSize), - Interval: ctx.Int64(argUpgradeBatchInterval), - }, - } - } - - resource, err := Lookup(c, templateName, managementClient.TemplateType) - if err != nil { - return err - } - - template, err := getFilteredTemplate(ctx, c, resource.ID) - if err != nil { - return err - } - - latestVersion, err := getTemplateLatestVersion(template) - if err != nil { - return err - } - - templateVersionID := templateVersionIDFromVersionLink(template.VersionLinks[latestVersion]) - userVersion := ctx.String("version") - if userVersion != "" { - if link, ok := template.VersionLinks[userVersion]; ok { - templateVersionID = templateVersionIDFromVersionLink(link) - } else { - return fmt.Errorf( - "version %s for template %s is invalid, run 'rancher mcapp show-template %s' for a list of versions", - userVersion, - templateName, - templateName, - ) - } - } - - templateVersion, err := c.ManagementClient.TemplateVersion.ByID(templateVersionID) - if err != nil { - return err - } - - interactive := !ctx.Bool("no-prompt") - answers, answersSetString, err := processAnswerInstall(ctx, templateVersion, nil, nil, interactive, true) - if err != nil { - return err - } - - projectIDs, err := lookupProjectIDsFromTargets(c, ctx.StringSlice("target")) - if err != nil { - return err - } - - for _, target := range projectIDs { - app.Targets = append(app.Targets, managementClient.Target{ - ProjectID: target, - }) - } - if len(projectIDs) == 0 { - app.Targets = append(app.Targets, managementClient.Target{ - ProjectID: c.UserConfig.Project, - }) - } - - app.Answers, err = toMultiClusterAppAnswers(c, answers, answersSetString) - if err != nil { - return err - } - app.TemplateVersionID = templateVersionID - - accessType := strings.ToLower(ctx.String("member-access-type")) - if !slice.ContainsString(memberAccessTypes, accessType) { - return fmt.Errorf("invalid access type %q, supported values are \"owner\",\"member\" and \"read-only\"", accessType) - } - - members, err := addMembersByNames(ctx, c, app.Members, ctx.StringSlice("member"), accessType) - if err != nil { - return err - } - app.Members = members - - app.Wait = ctx.Bool("helm-wait") - app.Timeout = ctx.Int64("helm-timeout") - - app, err = c.ManagementClient.MultiClusterApp.Create(app) - if err != nil { - return err - } - - fmt.Printf("Installing multi-cluster app %q...\n", app.Name) - - return nil -} - -func lookupProjectIDsFromTargets(c *cliclient.MasterClient, targets []string) ([]string, error) { - var projectIDs []string - for _, target := range targets { - projectID, err := lookupProjectIDFromProjectScope(c, target) - if err != nil { - return nil, err - } - projectIDs = append(projectIDs, projectID) - } - return projectIDs, nil -} - -func lookupClusterIDFromClusterScope(c *cliclient.MasterClient, clusterNameOrID string) (string, error) { - clusterResource, err := Lookup(c, clusterNameOrID, managementClient.ClusterType) - if err != nil { - return "", err - } - return clusterResource.ID, nil -} - -func lookupProjectIDFromProjectScope(c *cliclient.MasterClient, scope string) (string, error) { - cluster, project := parseScope(scope) - clusterResource, err := Lookup(c, cluster, managementClient.ClusterType) - if err != nil { - return "", err - } - if clusterResource.ID == cluster { - // Lookup by ID - projectResource, err := Lookup(c, scope, managementClient.ProjectType) - if err != nil { - return "", err - } - return projectResource.ID, nil - } - // Lookup by clusterName:projectName - projectResource, err := Lookup(c, project, managementClient.ProjectType) - if err != nil { - return "", err - } - return projectResource.ID, nil - -} - -func toMultiClusterAppAnswers(c *cliclient.MasterClient, answers, answersSetString map[string]string) ([]managementClient.Answer, error) { - answerMap := make(map[string]scopeAnswers) - var answerSlice []managementClient.Answer - if err := setValueInAnswerMapByScope(c, answerMap, answers, "Answers"); err != nil { - return nil, err - } - if err := setValueInAnswerMapByScope(c, answerMap, answersSetString, "AnswersSetString"); err != nil { - return nil, err - } - for k, v := range answerMap { - answer := managementClient.Answer{ - Values: v.Answers, - ValuesSetString: v.AnswersSetString, - } - if strings.Contains(k, ":") { - answer.ProjectID = k - } else if k != "" { - answer.ClusterID = k - } - answerSlice = append(answerSlice, answer) - } - return answerSlice, nil -} - -func setValueInAnswerMapByScope(c *cliclient.MasterClient, answerMap map[string]scopeAnswers, inputAnswers map[string]string, scopeAnswersFieldStr string) error { - for k, v := range inputAnswers { - switch parts := strings.SplitN(k, ":", 3); { - case len(parts) == 1: - // Global scope - setValueInAnswerMap(answerMap, "", "", scopeAnswersFieldStr, k, v) - case len(parts) == 2: - // Cluster scope - clusterNameOrID := parts[0] - clusterID, err := lookupClusterIDFromClusterScope(c, clusterNameOrID) - if err != nil { - return err - } - setValueInAnswerMap(answerMap, clusterNameOrID, clusterID, scopeAnswersFieldStr, parts[1], v) - case len(parts) == 3: - // Project scope - projectScope := concatScope(parts[0], parts[1]) - projectID, err := lookupProjectIDFromProjectScope(c, projectScope) - if err != nil { - return err - } - setValueInAnswerMap(answerMap, projectScope, projectID, scopeAnswersFieldStr, parts[2], v) - } - } - return nil -} - -func setValueInAnswerMap(answerMap map[string]scopeAnswers, scope, scopeID, fieldNameToUpdate, key, value string) { - var exist bool - if answerMap[scopeID].Answers == nil && answerMap[scopeID].AnswersSetString == nil { - answerMap[scopeID] = scopeAnswers{ - Answers: make(map[string]string), - AnswersSetString: make(map[string]string), - } - } - scopeAnswersStruct := answerMap[scopeID] - scopeAnswersMap := reflect.ValueOf(&scopeAnswersStruct).Elem().FieldByName(fieldNameToUpdate) - for _, k := range scopeAnswersMap.MapKeys() { - if reflect.ValueOf(key) == k { - exist = true - break - } - } - if exist { - // It is possible that there are different forms of the same answer key in aggregated answers - // In this case, name format from users overrides id format from existing app answers. - if scope != scopeID { - scopeAnswersMap.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(value)) - } - } else { - scopeAnswersMap.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(value)) - } -} - -func fromMultiClusterAppAnswers(answerSlice []managementClient.Answer) (map[string]string, map[string]string) { - answers := make(map[string]string) - answersSetString := make(map[string]string) - for _, answer := range answerSlice { - for k, v := range answer.Values { - scopedKey := getAnswerScopedKey(answer, k) - answers[scopedKey] = v - } - for k, v := range answer.ValuesSetString { - scopedKey := getAnswerScopedKey(answer, k) - answersSetString[scopedKey] = v - } - } - return answers, answersSetString -} - -func getAnswerScopedKey(answer managementClient.Answer, key string) string { - scope := "" - if answer.ProjectID != "" { - scope = answer.ProjectID - } else if answer.ClusterID != "" { - scope = answer.ClusterID - } - scopedKey := key - if scope != "" { - scopedKey = concatScope(scope, key) - } - return scopedKey -} - -func addMcappTargetProject(ctx *cli.Context) error { - if len(ctx.Args()) < 2 { - return cli.ShowSubcommandHelp(ctx) - } - - c, err := GetClient(ctx) - if err != nil { - return err - } - - _, app, err := searchForMcapp(c, ctx.Args().First()) - if err != nil { - return err - } - - input, err := getTargetInput(ctx, c) - if err != nil { - return err - } - - if err := c.ManagementClient.MultiClusterApp.ActionAddProjects(app, input); err != nil { - return err - } - - return nil -} - -func deleteMcappTargetProject(ctx *cli.Context) error { - if len(ctx.Args()) < 2 { - return cli.ShowSubcommandHelp(ctx) - } - - c, err := GetClient(ctx) - if err != nil { - return err - } - - _, app, err := searchForMcapp(c, ctx.Args().First()) - if err != nil { - return err - } - - input, err := getTargetInput(ctx, c) - if err != nil { - return err - } - return c.ManagementClient.MultiClusterApp.ActionRemoveProjects(app, input) -} - -func getTargetInput(ctx *cli.Context, c *cliclient.MasterClient) (*managementClient.UpdateMultiClusterAppTargetsInput, error) { - targets := ctx.Args()[1:] - projectIDs, err := lookupProjectIDsFromTargets(c, targets) - if err != nil { - return nil, err - } - answers, answersSetString, err := processAnswerUpdates(ctx, nil, nil) - if err != nil { - return nil, err - } - mcaAnswers, err := toMultiClusterAppAnswers(c, answers, answersSetString) - if err != nil { - return nil, err - } - input := &managementClient.UpdateMultiClusterAppTargetsInput{ - Projects: projectIDs, - Answers: mcaAnswers, - } - return input, nil -} - -func addMcappMember(ctx *cli.Context) error { - if len(ctx.Args()) < 3 { - return cli.ShowSubcommandHelp(ctx) - } - - appName := ctx.Args().First() - accessType := strings.ToLower(ctx.Args().Get(1)) - memberNames := ctx.Args()[2:] - - if !slice.ContainsString(memberAccessTypes, accessType) { - return fmt.Errorf("invalid access type %q, supported values are \"owner\",\"member\" and \"read-only\"", accessType) - } - - c, err := GetClient(ctx) - if err != nil { - return err - } - - _, app, err := searchForMcapp(c, appName) - if err != nil { - return err - } - - members, err := addMembersByNames(ctx, c, app.Members, memberNames, accessType) - if err != nil { - return err - } - - update := make(map[string]interface{}) - update["members"] = members - update["roles"] = app.Roles - - _, err = c.ManagementClient.MultiClusterApp.Update(app, update) - return err -} - -func deleteMcappMember(ctx *cli.Context) error { - if len(ctx.Args()) < 2 { - return cli.ShowSubcommandHelp(ctx) - } - - appName := ctx.Args().First() - memberNames := ctx.Args()[1:] - - c, err := GetClient(ctx) - if err != nil { - return err - } - - _, app, err := searchForMcapp(c, appName) - if err != nil { - return err - } - - members, err := deleteMembersByNames(ctx, c, app.Members, memberNames) - if err != nil { - return err - } - - update := make(map[string]interface{}) - update["members"] = members - update["roles"] = app.Roles - - _, err = c.ManagementClient.MultiClusterApp.Update(app, update) - return err -} - -func showMultiClusterApp(ctx *cli.Context) error { - if ctx.NArg() == 0 { - return cli.ShowSubcommandHelp(ctx) - } - - c, err := GetClient(ctx) - if err != nil { - return err - } - - resource, app, err := searchForMcapp(c, ctx.Args().First()) - if err != nil { - return err - } - - err = outputMultiClusterAppRevisions(ctx, c, resource, app) - if err != nil { - return err - } - - fmt.Println() - - err = outputMultiClusterAppVersions(ctx, c, app) - if err != nil { - return err - } - - if ctx.Bool("show-roles") { - fmt.Println() - - err = outputMultiClusterAppRoles(ctx, c, app) - if err != nil { - return err - } - } - - return nil -} - -func listMultiClusterAppMembers(ctx *cli.Context) error { - if ctx.NArg() == 0 { - return cli.ShowSubcommandHelp(ctx) - } - - c, err := GetClient(ctx) - if err != nil { - return err - } - - _, app, err := searchForMcapp(c, ctx.Args().First()) - if err != nil { - return err - } - - return outputMembers(ctx, c, app.Members) -} - -func listMultiClusterAppAnswers(ctx *cli.Context) error { - if ctx.NArg() == 0 { - return cli.ShowSubcommandHelp(ctx) - } - - c, err := GetClient(ctx) - if err != nil { - return err - } - - _, app, err := searchForMcapp(c, ctx.Args().First()) - if err != nil { - return err - } - - return outputMultiClusterAppAnswers(ctx, c, app) -} - -func searchForMcapp(c *cliclient.MasterClient, name string) (*types.Resource, *managementClient.MultiClusterApp, error) { - resource, err := Lookup(c, name, managementClient.MultiClusterAppType) - if err != nil { - return nil, nil, err - } - app, err := c.ManagementClient.MultiClusterApp.ByID(resource.ID) - if err != nil { - return nil, nil, err - } - return resource, app, nil -} - -func outputMultiClusterAppVersions(ctx *cli.Context, c *cliclient.MasterClient, app *managementClient.MultiClusterApp) error { - templateVersion, err := c.ManagementClient.TemplateVersion.ByID(app.TemplateVersionID) - if err != nil { - return err - } - - ver, err := getRancherServerVersion(c) - if err != nil { - return err - } - - filter := defaultListOpts(ctx) - filter.Filters["rancherVersion"] = ver - - template := &managementClient.Template{} - if err := c.ManagementClient.Ops.DoGet(templateVersion.Links["template"], filter, template); err != nil { - return err - } - writer := NewTableWriter([][]string{ - {"CURRENT", "Current"}, - {"VERSION", "Version"}, - }, ctx) - - defer writer.Close() - - sortedVersions, err := sortTemplateVersions(template) - if err != nil { - return err - } - - for _, version := range sortedVersions { - var current string - if version.String() == templateVersion.Version { - current = "*" - } - writer.Write(&VersionData{ - Current: current, - Version: version.String(), - }) - } - return writer.Err() -} - -func outputMultiClusterAppRevisions(ctx *cli.Context, c *cliclient.MasterClient, resource *types.Resource, app *managementClient.MultiClusterApp) error { - revisions := &managementClient.MultiClusterAppRevisionCollection{} - if err := c.ManagementClient.GetLink(*resource, "revisions", revisions); err != nil { - return err - } - - var sorted revSlice - for _, rev := range revisions.Data { - parsedTime, err := time.Parse(time.RFC3339, rev.Created) - if err != nil { - return err - } - sorted = append(sorted, revision{Name: rev.Name, Created: parsedTime}) - } - - sort.Sort(sorted) - - writer := NewTableWriter([][]string{ - {"CURRENT", "Current"}, - {"REVISION", "Name"}, - {"CREATED", "Human"}, - }, ctx) - - defer writer.Close() - - for _, rev := range sorted { - if rev.Name == app.Status.RevisionID { - rev.Current = "*" - } - rev.Human = rev.Created.Format("02 Jan 2006 15:04:05 MST") - writer.Write(rev) - - } - return writer.Err() -} - -func outputMultiClusterAppRoles(ctx *cli.Context, c *cliclient.MasterClient, app *managementClient.MultiClusterApp) error { - writer := NewTableWriter([][]string{ - {"ROLE_NAME", "Name"}, - }, ctx) - - defer writer.Close() - - for _, r := range app.Roles { - writer.Write(map[string]string{"Name": r}) - } - return writer.Err() -} - -func outputMultiClusterAppAnswers(ctx *cli.Context, c *cliclient.MasterClient, app *managementClient.MultiClusterApp) error { - writer := NewTableWriter([][]string{ - {"SCOPE", "Scope"}, - {"QUESTION", "Question"}, - {"ANSWER", "Answer"}, - }, ctx) - - defer writer.Close() - - answers := app.Answers - // Sort answers by scope in the Global-Cluster-Project order - sort.Slice(answers, func(i, j int) bool { - if answers[i].ClusterID == "" && answers[i].ProjectID == "" { - return true - } else if answers[i].ClusterID != "" && answers[j].ProjectID != "" { - return true - } - return false - }) - - var scope string - for _, r := range answers { - scope = "Global" - if r.ClusterID != "" { - cluster, err := getClusterByID(c, r.ClusterID) - if err != nil { - return err - } - scope = fmt.Sprintf("All projects in cluster %s", cluster.Name) - } else if r.ProjectID != "" { - project, err := getProjectByID(c, r.ProjectID) - if err != nil { - return err - } - scope = fmt.Sprintf("Project %s", project.Name) - } - for key, value := range r.Values { - writer.Write(map[string]string{ - "Scope": scope, - "Question": key, - "Answer": value, - }) - } - for key, value := range r.ValuesSetString { - writer.Write(map[string]string{ - "Scope": scope, - "Question": key, - "Answer": fmt.Sprintf("\"%s\"", value), - }) - } - } - return writer.Err() -} - -func globalTemplateLs(ctx *cli.Context) error { - c, err := GetClient(ctx) - if err != nil { - return err - } - - filter := defaultListOpts(ctx) - if ctx.String("catalog") != "" { - resource, err := Lookup(c, ctx.String("catalog"), managementClient.CatalogType) - if err != nil { - return err - } - filter.Filters["catalogId"] = resource.ID - } - - collection, err := c.ManagementClient.Template.List(filter) - if err != nil { - return err - } - - writer := NewTableWriter([][]string{ - {"ID", "ID"}, - {"NAME", "Template.Name"}, - {"CATEGORY", "Category"}, - }, ctx) - - defer writer.Close() - - for _, item := range collection.Data { - // Skip non-global catalogs - if item.CatalogID == "" { - continue - } - writer.Write(&TemplateData{ - ID: item.ID, - Template: item, - Category: strings.Join(item.Categories, ","), - }) - } - - return writer.Err() -} - -func concatScope(scope, key string) string { - return fmt.Sprintf("%s:%s", scope, key) -} - -func parseScope(ref string) (scope string, key string) { - parts := strings.SplitN(ref, ":", 2) - if len(parts) == 1 { - return "", parts[0] - } - return parts[0], parts[1] -} diff --git a/cmd/multiclusterapp_test.go b/cmd/multiclusterapp_test.go deleted file mode 100644 index f8c897da..00000000 --- a/cmd/multiclusterapp_test.go +++ /dev/null @@ -1,122 +0,0 @@ -package cmd - -import ( - "testing" - - client "github.com/rancher/rancher/pkg/client/generated/management/v3" - "github.com/stretchr/testify/assert" -) - -func TestFromMultiClusterAppAnswers(t *testing.T) { - assert := assert.New(t) - answerSlice := []client.Answer{ - { - ProjectID: "c-1:p-1", - Values: map[string]string{ - "var-1": "val1", - "var-2": "val2", - }, - ValuesSetString: map[string]string{ - "str-var-1": "str-val1", - "str-var-2": "str-val2", - }, - }, { - ProjectID: "c-1:p-2", - Values: map[string]string{ - "var-3": "val3", - }, - ValuesSetString: map[string]string{ - "str-var-3": "str-val3", - }, - }, { - ClusterID: "c-1", - Values: map[string]string{ - "var-4": "val4", - }, - ValuesSetString: map[string]string{ - "str-var-4": "str-val4", - }, - }, { - ClusterID: "c-2", - Values: map[string]string{ - "var-5": "val5", - }, - ValuesSetString: map[string]string{ - "str-var-5": "str-val5", - }, - }, { - Values: map[string]string{ - "var-6": "val6", - }, - ValuesSetString: map[string]string{ - "str-var-6": "str-val6", - }, - }, - } - - answers, answersSetString := fromMultiClusterAppAnswers(answerSlice) - assert.Equal(len(answers), 6) - assert.Equal(answers["c-1:p-1:var-1"], "val1") - assert.Equal(answers["c-1:p-1:var-2"], "val2") - assert.Equal(answers["c-1:p-2:var-3"], "val3") - assert.Equal(answers["c-1:var-4"], "val4") - assert.Equal(answers["c-2:var-5"], "val5") - assert.Equal(answers["var-6"], "val6") - - assert.Equal(len(answersSetString), 6) - assert.Equal(answersSetString["c-1:p-1:str-var-1"], "str-val1") - assert.Equal(answersSetString["c-1:p-1:str-var-2"], "str-val2") - assert.Equal(answersSetString["c-1:p-2:str-var-3"], "str-val3") - assert.Equal(answersSetString["c-1:str-var-4"], "str-val4") - assert.Equal(answersSetString["c-2:str-var-5"], "str-val5") - assert.Equal(answersSetString["str-var-6"], "str-val6") -} - -func TestGetReadableTargetNames(t *testing.T) { - assert := assert.New(t) - clusters := map[string]client.Cluster{ - "c-1": { - Name: "cn-1", - }, - "c-2": { - Name: "cn-2", - }, - } - projects := map[string]client.Project{ - "c-1:p-1": { - Name: "pn-1", - }, - "c-1:p-2": { - Name: "pn-2", - }, - "c-2:p-3": { - Name: "pn-3", - }, - "c-2:p-4": { - Name: "pn-4", - }, - } - targets := []client.Target{ - { - ProjectID: "c-1:p-1", - }, - { - ProjectID: "c-1:p-2", - }, - { - ProjectID: "c-2:p-3", - }, - } - result := getReadableTargetNames(clusters, projects, targets) - assert.Contains(result, "cn-1:pn-1") - assert.Contains(result, "cn-1:pn-2") - assert.Contains(result, "cn-2:pn-3") - - targets = []client.Target{ - { - ProjectID: "c-0:p-0", - }, - } - result = getReadableTargetNames(clusters, projects, targets) - assert.Contains(result, "c-0:p-0") -} diff --git a/cmd/wait.go b/cmd/wait.go index abd98b5d..86e80a17 100644 --- a/cmd/wait.go +++ b/cmd/wait.go @@ -11,7 +11,7 @@ import ( ) var ( - waitTypes = []string{"cluster", "app", "project", "multiClusterApp"} + waitTypes = []string{"cluster", "project"} ) func WaitCommand() cli.Command { diff --git a/go.mod b/go.mod index 315887b1..5a30f74b 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,6 @@ replace k8s.io/client-go => k8s.io/client-go v0.31.1 require ( github.com/ghodss/yaml v1.0.0 github.com/grantae/certinfo v0.0.0-20170412194111-59d56a35515b - github.com/hashicorp/go-version v1.2.1 github.com/pkg/errors v0.9.1 github.com/rancher/norman v0.0.0-20241001183610-78a520c160ab github.com/rancher/rancher/pkg/apis v0.0.0-20241119163817-d801b4924311 @@ -23,7 +22,6 @@ require ( golang.org/x/sync v0.8.0 golang.org/x/term v0.25.0 golang.org/x/text v0.19.0 - gopkg.in/yaml.v2 v2.4.0 k8s.io/client-go v12.0.0+incompatible ) @@ -75,6 +73,7 @@ require ( golang.org/x/time v0.7.0 // indirect google.golang.org/protobuf v1.35.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.31.1 // indirect k8s.io/apimachinery v0.31.1 // indirect diff --git a/go.sum b/go.sum index d797996f..12df7125 100644 --- a/go.sum +++ b/go.sum @@ -51,8 +51,6 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grantae/certinfo v0.0.0-20170412194111-59d56a35515b h1:NGgE5ELokSf2tZ/bydyDUKrvd/jP8lrAoPNeBuMOTOk= github.com/grantae/certinfo v0.0.0-20170412194111-59d56a35515b/go.mod h1:zT/uzhdQGTqlwTq7Lpbj3JoJQWfPfIJ1tE0OidAmih8= -github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= -github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= diff --git a/main.go b/main.go index ce53b3f8..aa3d78af 100644 --- a/main.go +++ b/main.go @@ -105,15 +105,12 @@ func mainErr() error { }, } app.Commands = []cli.Command{ - cmd.AppCommand(), - cmd.CatalogCommand(), cmd.ClusterCommand(), cmd.ContextCommand(), cmd.InspectCommand(), cmd.KubectlCommand(), cmd.LoginCommand(), cmd.MachineCommand(), - cmd.MultiClusterAppCommand(), cmd.NamespaceCommand(), cmd.NodeCommand(), cmd.ProjectCommand(),