// promptConfirm prompts a user to confirm (or deny) something. // // True is returned iff the prompt is confirmed. // Errors are reported to the log, and return false. // // Valid confirmations: // y, yes, true, t, aye-aye // // Valid denials: // n, no, f, false // // Any other prompt response will return false, and issue a warning to the // user. func promptConfirm(msg string) bool { oldState, err := terminal.MakeRaw(0) if err != nil { log.Err("Could not get terminal: %s", err) return false } defer terminal.Restore(0, oldState) f := readerWriter(log.Stdin, log.Stdout) t := terminal.NewTerminal(f, msg+" (y/N) ") res, err := t.ReadLine() if err != nil { log.Err("Could not read line: %s", err) return false } res = strings.ToLower(res) switch res { case "yes", "y", "true", "t", "aye-aye": return true case "no", "n", "false", "f": return false } log.Warn("Did not understand answer %q, assuming No", res) return false }
// RepoName gets the name of the Git repo, or an empty string if none is found. func RepoName(chartpath string) string { wd, err := os.Getwd() if err != nil { log.Err("Could not get working directory: %s", err) return "" } defer func() { if err := os.Chdir(wd); err != nil { log.Die("Unrecoverable error: %s", err) } }() if err := os.Chdir(chartpath); err != nil { log.Err("Could not find chartpath %s: %s", chartpath, err) return "" } out, err := exec.Command("git", "config", "--get", "remote.origin.url").CombinedOutput() if err != nil { log.Err("Git failed to get the origin name: %s %s", err, string(out)) return "" } return strings.TrimSpace(string(out)) }
// Target displays information about the cluster func Target(client kubectl.Runner) { out, err := client.ClusterInfo() if err != nil { log.Err(err.Error()) } log.Msg(string(out)) }
// Publish a chart from the workspace to the cache directory // // - chartName being published // - homeDir is the helm home directory for the user // - force publishing even if the chart directory already exists func Publish(chartName, homeDir, repo string, force bool) { if repo == "" { repo = "charts" } if !mustConfig(homeDir).Repos.Exists(repo) { log.Err("Repo %s does not exist", repo) log.Info("Available repositories") ListRepos(homeDir) return } src := helm.WorkspaceChartDirectory(homeDir, chartName) dst := helm.CacheDirectory(homeDir, repo, chartName) if _, err := os.Stat(dst); err == nil { if force != true { log.Info("chart already exists, use -f to force") return } } if err := helm.CopyDir(src, dst); err != nil { log.Die("failed to publish directory: %v", err) } }
// NewIndex creats a new Index. // // NewIndex indexes all of the chart tables configured in the config.yaml file. // For that reason, it may cause substantial overhead on a large set of repos. func NewIndex(cfg *config.Configfile, cachedir string) *Index { lines := map[string]string{} charts := map[string]*chart.Chartfile{} for _, table := range cfg.Repos.Tables { def := cfg.Repos.Default == table.Name base := filepath.Join(cachedir, table.Name, "*/") dirs, err := filepath.Glob(base) if err != nil { log.Err("Failed to read table %s: %s", table.Name, err) } for _, dir := range dirs { bname := filepath.Base(dir) c, err := chart.LoadChartfile(filepath.Join(dir, "Chart.yaml")) if err != nil { // This is not a chart. Skip it. continue } name := table.Name + "/" + c.Name if def { name = c.Name } line := c.Name + sep + table.Name + "/" + bname + sep + c.Description + sep + c.Details lines[name] = strings.ToLower(line) charts[name] = c } } return &Index{lines: lines, charts: charts} }
// Valid returns true if every validation passes. func (cv *ChartValidation) Valid() bool { var valid = true fmt.Printf("\nVerifying %s chart is a valid chart...\n", cv.ChartName()) cv.walk(func(v *Validation) bool { v.path = cv.Path vv := v.valid() if !vv { switch v.level { case 2: cv.ErrorCount = cv.ErrorCount + 1 msg := v.Message + " : " + strconv.FormatBool(vv) log.Err(msg) case 1: cv.WarningCount = cv.WarningCount + 1 msg := v.Message + " : " + strconv.FormatBool(vv) log.Warn(msg) } } else { msg := v.Message + " : " + strconv.FormatBool(vv) log.Info(msg) } valid = valid && vv return valid }) return valid }
func optRepoMatch(from, req *chart.Dependency) bool { // If no repo is set, this is treated as a match. if req.Repo == "" { return true } // Some day we might want to do some git-fu to match different forms of the // same Git repo. a, err := canonicalRepo(req.Repo) if err != nil { log.Err("Could not parse %s: %s", req.Repo, err) return false } b, err := canonicalRepo(from.Repo) if err != nil { log.Err("Could not parse %s: %s", from.Repo, err) return false } return a == b }
// minArgs checks to see if the right number of args are passed. // // If not, it prints an error and quits. func minArgs(c *cli.Context, i int, name string) { if len(c.Args()) < i { m := "arguments" if i == 1 { m = "argument" } log.Err("Expected %d %s", i, m) cli.ShowCommandHelp(c, name) log.Die("") } }
// Remove removes a chart from the workdir. // // - chart is the source // - homedir is the home directory for the user // - force will remove installed charts from workspace func Remove(chart, homedir string, force bool) { chartPath := helm.WorkspaceChartDirectory(homedir, chart) if _, err := os.Stat(chartPath); err != nil { log.Err("Chart not found. %s", err) return } if !force { var connectionFailure bool // check if any chart manifests are installed installed, err := checkManifests(chartPath) if err != nil { if strings.Contains(err.Error(), "unable to connect") { connectionFailure = true } else { log.Die(err.Error()) } } if connectionFailure { log.Err("Could not determine if %s is installed. To remove the chart --force flag must be set.", chart) return } else if len(installed) > 0 { log.Err("Found %d installed manifests for %s. To remove a chart that has been installed the --force flag must be set.", len(installed), chart) return } } // remove local chart files if err := os.RemoveAll(chartPath); err != nil { log.Die("Could not remove chart. %s", err) } log.Info("All clear! You have successfully removed %s from your workspace.", chart) }
// Search looks for packages with 'term' in their name. func Search(term, homedir string, regexp bool) { cfg := mustConfig(homedir) cdir := helm.CacheDirectory(homedir) i := search.NewIndex(cfg, cdir) res, err := i.Search(term, 5, regexp) if err != nil { log.Die("Failed to search: %s", err) } if len(res) == 0 { log.Err("No results found. Try using '--regexp'.") return } search.SortScore(res) for _, r := range res { c, _ := i.Chart(r.Name) log.Msg("%s - %s", r.Name, c.Description) } }
// Template renders a template to an output file. func Template(out, in, data string, force bool) error { var dest io.Writer _, err = os.Stat(out) if !(force || os.Getenv("HELM_FORCE_FLAG") == "true") && err == nil { return fmt.Errorf("File %s already exists. To overwrite it, please re-run this command with the --force/-f flag.", out) } if out != "" { f, err := os.Create(out) if err != nil { log.Die("Failed to open %s: %s", out, err) } defer func() { if err := f.Close(); err != nil { log.Err("Error closing file: %s", err) } }() dest = f } else { dest = log.Stdout } inReader, err := os.Open(in) if err != nil { log.Die("Failed to open template file: %s", err) } var vals interface{} if data != "" { var err error vals, err = openValues(data) if err != nil { log.Die("Error opening value file: %s", err) } } GenerateTemplate(dest, inReader, vals) return nil }
// Cli is the main entrypoint for the Helm Classic CLI. func Cli() *cli.App { app := cli.NewApp() app.Name = "helmc" app.Usage = globalUsage app.Version = version app.EnableBashCompletion = true app.After = func(c *cli.Context) error { if log.ErrorState { return errors.New("Exiting with errors") } return nil } app.Flags = []cli.Flag{ cli.StringFlag{ Name: "home", Value: "$HOME/.helmc", Usage: "The location of your Helm Classic files", EnvVar: "HELMC_HOME", }, cli.BoolFlag{ Name: "debug", Usage: "Enable verbose debugging output", }, } app.Commands = []cli.Command{ createCmd, doctorCmd, editCmd, fetchCmd, homeCmd, infoCmd, installCmd, lintCmd, listCmd, publishCmd, removeCmd, repositoryCmd, searchCmd, targetCmd, uninstallCmd, updateCmd, generateCmd, tplCmd, } app.CommandNotFound = func(c *cli.Context, command string) { if action.HasPlugin(command) { action.Plugin(home(c), command, c.Args()) return } log.Err("No matching command '%s'", command) cli.ShowAppHelp(c) log.Die("") } app.Before = func(c *cli.Context) error { log.IsDebugging = c.Bool("debug") return nil } return app }
// Lint validates that a chart is well-formed // // - chartPath path to chart directory func Lint(chartPath string) { cv := new(validation.ChartValidation) chartPresenceValidation := cv.AddError("Chart found at "+chartPath, func(path string, v *validation.Validation) bool { stat, err := os.Stat(chartPath) cv.Path = chartPath return err == nil && stat.Mode().IsDir() }) chartYamlPresenceValidation := chartPresenceValidation.AddError("Chart.yaml is present", func(path string, v *validation.Validation) bool { stat, err := os.Stat(v.ChartYamlPath()) return err == nil && stat.Mode().IsRegular() }) chartYamlValidation := chartYamlPresenceValidation.AddError("Chart.yaml is valid yaml", func(path string, v *validation.Validation) bool { chartfile, err := v.Chartfile() if err == nil { cv.Chartfile = chartfile } return err == nil }) chartYamlNameValidation := chartYamlValidation.AddError("Chart.yaml has a name field", func(path string, v *validation.Validation) bool { return cv.Chartfile.Name != "" }) chartYamlNameValidation.AddError("Name declared in Chart.yaml is the same as directory name.", func(path string, v *validation.Validation) bool { return cv.Chartfile.Name == cv.ChartName() }) chartYamlValidation.AddError("Chart.yaml has a version field", func(path string, v *validation.Validation) bool { return cv.Chartfile.Version != "" }) chartYamlValidation.AddWarning("Chart.yaml has a description field", func(path string, v *validation.Validation) bool { return cv.Chartfile.Description != "" }) chartYamlValidation.AddWarning("Chart.yaml has a maintainers field", func(path string, v *validation.Validation) bool { return cv.Chartfile.Maintainers != nil }) chartPresenceValidation.AddWarning("README.md is present and not empty", func(path string, v *validation.Validation) bool { readmePath := filepath.Join(path, "README.md") stat, err := os.Stat(readmePath) return err == nil && stat.Mode().IsRegular() && stat.Size() > 0 }) manifestsValidation := chartPresenceValidation.AddError("Manifests directory is present", func(path string, v *validation.Validation) bool { stat, err := os.Stat(v.ChartManifestsPath()) return err == nil && stat.Mode().IsDir() }) manifestsParsingValidation := manifestsValidation.AddError("Manifests are valid yaml", func(path string, v *validation.Validation) bool { manifests, err := manifest.ParseDir(cv.Path) if err == nil { cv.Manifests = manifests } return err == nil && cv.Manifests != nil }) manifestsParsingValidation.AddWarning("Manifests have correct and valid metadata", func(path string, v *validation.Validation) bool { success := true validKinds := InstallOrder for _, m := range cv.Manifests { meta, _ := m.VersionedObject.Meta() if meta.Name == "" || len(meta.Name) > MaxMetadataNameLength { success = false } if match, _ := regexp.MatchString(`[a-z]([-a-z0-9]*[a-z0-9])?`, meta.Name); !match { success = false } val, ok := meta.Labels["heritage"] if !ok || (val != "helm") { success = false } kind := meta.Kind validManifestKind := false for _, validKind := range validKinds { if kind == validKind { validManifestKind = true } } if validManifestKind == false { success = false } } return success }) if cv.Valid() { log.Info("Chart [%s] has passed all necessary checks", cv.ChartName()) } else { if cv.ErrorCount > 0 { log.Err("Chart [%s] has failed some necessary checks. Check out the error and warning messages listed.", cv.ChartName()) } else { log.Warn("Chart [%s] has passed all necessary checks but failed some checks as well. Proceed with caution. Check out the warnings listed.", cv.ChartName()) } } }