cli/cmd/app.go

1465 lines
37 KiB
Go

package cmd
import (
"bufio"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"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 <app-id>
# 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{
cli.Command{
Name: "ls",
Usage: "List apps",
Description: "\nList all apps in the current Rancher server",
ArgsUsage: "None",
Action: appLs,
Flags: appLsFlags,
},
cli.Command{
Name: "delete",
Usage: "Delete an app",
Action: appDelete,
ArgsUsage: "[APP_NAME/APP_ID]",
},
cli.Command{
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",
},
},
},
cli.Command{
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)",
},
},
},
cli.Command{
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)",
},
},
},
cli.Command{
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",
},
},
},
cli.Command{
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,
},
cli.Command{
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,
},
},
cli.Command{
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 := ioutil.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 := ioutil.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 := ioutil.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 <key>=<value> 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 <key>=<value> 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
}