// 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) } }
// Fetch gets a chart from the source repo and copies to the workdir. // // - chartName is the source // - lname is the local name for that chart (chart-name); if blank, it is set to the chart. // - homedir is the home directory for the user func Fetch(chartName, lname, homedir string) { r := mustConfig(homedir).Repos repository, chartName := r.RepoChart(chartName) if lname == "" { lname = chartName } fetch(chartName, lname, homedir, repository) chartFilePath := helm.WorkspaceChartDirectory(homedir, lname, Chartfile) cfile, err := chart.LoadChartfile(chartFilePath) if err != nil { log.Die("Source is not a valid chart. Missing Chart.yaml: %s", err) } deps, err := dependency.Resolve(cfile, helm.WorkspaceChartDirectory(homedir)) if err != nil { log.Warn("Could not check dependencies: %s", err) return } if len(deps) > 0 { log.Warn("Unsatisfied dependencies:") for _, d := range deps { log.Msg("\t%s %s", d.Name, d.Version) } } log.Info("Fetched chart into workspace %s", helm.WorkspaceChartDirectory(homedir, lname)) log.Info("Done") }
// Uninstall removes a chart from Kubernetes. // // Manifests are removed from Kubernetes in the order specified by // chart.UninstallOrder. Any unknown types are removed before that sequence // is run. func Uninstall(chartName, home, namespace string, force bool, client kubectl.Runner) { // This is a stop-gap until kubectl respects namespaces in manifests. if namespace == "" { log.Die("This command requires a namespace. Did you mean '-n default'?") } if !chartFetched(chartName, home) { log.Info("No chart named %q in your workspace. Nothing to delete.", chartName) return } cd := helm.WorkspaceChartDirectory(home, chartName) c, err := chart.Load(cd) if err != nil { log.Die("Failed to load chart: %s", err) } if err := deleteChart(c, namespace, true, client); err != nil { log.Die("Failed to list charts: %s", err) } if !force && !promptConfirm("Uninstall the listed objects?") { log.Info("Aborted uninstall") return } CheckKubePrereqs() log.Info("Running `kubectl delete` ...") if err := deleteChart(c, namespace, false, client); err != nil { log.Die("Failed to completely delete chart: %s", err) } log.Info("Done") }
// Doctor helps you see what's wrong with your Helm Classic setup func Doctor(home string) { log.Info("Checking things locally...") CheckLocalPrereqs(home) CheckKubePrereqs() log.Info("Everything looks good! Happy helming!") }
// Install loads a chart into Kubernetes. // // If the chart is not found in the workspace, it is fetched and then installed. // // During install, manifests are sent to Kubernetes in the ordered specified by InstallOrder. func Install(chartName, home, namespace string, force bool, generate bool, exclude []string, client kubectl.Runner) { ochart := chartName r := mustConfig(home).Repos table, chartName := r.RepoChart(chartName) if !chartFetched(chartName, home) { log.Info("No chart named %q in your workspace. Fetching now.", ochart) fetch(chartName, chartName, home, table) } cd := helm.WorkspaceChartDirectory(home, chartName) c, err := chart.Load(cd) if err != nil { log.Die("Failed to load chart: %s", err) } // Give user the option to bale if dependencies are not satisfied. nope, err := dependency.Resolve(c.Chartfile, helm.WorkspaceChartDirectory(home)) if err != nil { log.Warn("Failed to check dependencies: %s", err) if !force { log.Die("Re-run with --force to install anyway.") } } else if len(nope) > 0 { log.Warn("Unsatisfied dependencies:") for _, d := range nope { log.Msg("\t%s %s", d.Name, d.Version) } if !force { log.Die("Stopping install. Re-run with --force to install anyway.") } } // Run the generator if -g is set. if generate { Generate(chartName, home, exclude, force) } CheckKubePrereqs() log.Info("Running `kubectl create -f` ...") if err := uploadManifests(c, namespace, client); err != nil { log.Die("Failed to upload manifests: %s", err) } log.Info("Done") PrintREADME(chartName, home) }
// List lists all of the local charts. func List(homedir string) { md := helm.WorkspaceChartDirectory(homedir, "*") charts, err := filepath.Glob(md) if err != nil { log.Warn("Could not find any charts in %q: %s", md, err) } for _, c := range charts { cname := filepath.Base(c) if ch, err := chart.LoadChartfile(filepath.Join(c, Chartfile)); err == nil { log.Info("\t%s (%s %s) - %s", cname, ch.Name, ch.Version, ch.Description) continue } log.Info("\t%s (unknown)", cname) } }
// EnsureHome ensures that a HELMC_HOME exists. func EnsureHome(home string) { must := []string{home, CacheDirectory(home), filepath.Join(home, workspacePath)} for _, p := range must { if fi, err := os.Stat(p); err != nil { log.Debug("Creating %s", p) if err := os.MkdirAll(p, 0755); err != nil { log.Die("Could not create %q: %s", p, err) } } else if !fi.IsDir() { log.Die("%s must be a directory.", home) } } refi := filepath.Join(home, Configfile) if _, err := os.Stat(refi); err != nil { log.Info("Creating %s", refi) // Attempt to create a Repos.yaml if err := ioutil.WriteFile(refi, []byte(DefaultConfigfile), 0755); err != nil { log.Die("Could not create %s: %s", refi, err) } } if err := os.Chdir(home); err != nil { log.Die("Could not change to directory %q: %s", home, err) } }
func createWithChart(chart *chart.Chartfile, chartName, homeDir string) { chartDir := helm.WorkspaceChartDirectory(homeDir, chartName) // create directories if err := os.MkdirAll(filepath.Join(chartDir, "manifests"), 0755); err != nil { log.Die("Could not create %q: %s", chartDir, err) } // create Chartfile.yaml if err := chart.Save(filepath.Join(chartDir, Chartfile)); err != nil { log.Die("Could not create Chart.yaml: err", err) } // create README.md if err := createReadme(chartDir, chart); err != nil { log.Die("Could not create README.md: err", err) } // create example-pod if err := createExampleManifest(chartDir); err != nil { log.Die("Could not create example manifest: err", err) } log.Info("Created chart in %s", chartDir) }
// UpdateAll does a git fast-forward pull from each remote repo. func (r *Repos) UpdateAll() error { for _, table := range r.Tables { log.Info("Checking repository %s", table.Name) rpath := filepath.Join(r.Dir, table.Name) g, err := ensureRepo(table.Repo, rpath) if err != nil { return err } if g.IsDirty() { return fmt.Errorf("Repository '%s' is dirty. Commit changes before updating", table.Name) } initialVersion, err := g.Version() if err != nil { return fmt.Errorf("Could not get current sha of repository '%s'.", table.Name) } if err := g.Update(); err != nil { return err } diff, err := repoChartDiff(rpath, initialVersion) if err != nil { return err } printSummary(diff) } return nil }
func fetch(chartName, lname, homedir, chartpath string) { src := helm.CacheDirectory(homedir, chartpath, chartName) dest := helm.WorkspaceChartDirectory(homedir, lname) fi, err := os.Stat(src) if err != nil { log.Warn("Oops. Looks like there was an issue finding the chart, %s, in %s. Running `helmc update` to ensure you have the latest version of all Charts from Github...", lname, src) Update(homedir) fi, err = os.Stat(src) if err != nil { log.Die("Chart %s not found in %s", lname, src) } log.Info("Good news! Looks like that did the trick. Onwards and upwards!") } if !fi.IsDir() { log.Die("Malformed chart %s: Chart must be in a directory.", chartName) } if err := os.MkdirAll(dest, 0755); err != nil { log.Die("Could not create %q: %s", dest, err) } log.Debug("Fetching %s to %s", src, dest) if err := helm.CopyDir(src, dest); err != nil { log.Die("Failed copying %s to %s", src, dest) } if err := updateChartfile(src, dest, lname); err != nil { log.Die("Failed to update Chart.yaml: %s", err) } }
// 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 (r *Repos) deleteRepo(name string) error { rpath := filepath.Join(r.Dir, name) if fi, err := os.Stat(rpath); err != nil || !fi.IsDir() { log.Info("Deleted nothing. No repo named %s", name) return nil } log.Debug("Deleting %s", rpath) return os.RemoveAll(rpath) }
// AddRepo adds a repo to the list of repositories. func AddRepo(homedir, name, repository string) { cfg := mustConfig(homedir) if err := cfg.Repos.Add(name, repository); err != nil { log.Die(err.Error()) } if err := cfg.Save(""); err != nil { log.Die("Could not save configuration: %s", err) } log.Info("Hooray! Successfully added the repo.") }
// Update fetches the remote repo into the home directory. func Update(home string) { home, err := filepath.Abs(home) if err != nil { log.Die("Could not generate absolute path for %q: %s", home, err) } CheckLocalPrereqs(home) rc := mustConfig(home).Repos if err := rc.UpdateAll(); err != nil { log.Die("Not all repos could be updated: %s", err) } log.Info("Done") }
// Generate runs generators on the entire chart. // // By design, this only operates on workspaces, as it should never be run // on the cache. func Generate(chart, homedir string, exclude []string, force bool) { if abs, err := filepath.Abs(homedir); err == nil { homedir = abs } chartPath := util.WorkspaceChartDirectory(homedir, chart) // Although helmc itself may use the new HELMC_HOME environment variable to optionally define its // home directory, to maintain compatibility with charts created for the ORIGINAL helm, we // continue to support expansion of these "legacy" environment variables, including HELM_HOME. os.Setenv("HELM_HOME", homedir) os.Setenv("HELM_DEFAULT_REPO", mustConfig(homedir).Repos.Default) os.Setenv("HELM_FORCE_FLAG", strconv.FormatBool(force)) count, err := generator.Walk(chartPath, exclude, force) if err != nil { log.Die("Failed to complete generation: %s", err) } log.Info("Ran %d generators.", count) }
// Resolve takes a chart and a location and checks whether the chart's dependencies are satisfied. // // The `installdir` is the location where installed charts are located. Typically // this is in $HELMC_HOME/workspace/charts. // // This returns a list of unsatisfied dependencies (NOT an error condition). // // It returns an error only if it cannot perform the task of resolving dependencies. // Failed dependencies to not constitute an error. func Resolve(cf *chart.Chartfile, installdir string) ([]*chart.Dependency, error) { if len(cf.Dependencies) == 0 { log.Debug("No dependencies to check. :achievement-unlocked:") return []*chart.Dependency{}, nil } cache, err := dependencyCache(installdir) if err != nil { log.Debug("Failed to build dependency cache: %s", err) return []*chart.Dependency{}, err } res := []*chart.Dependency{} // TODO: This could be made more efficient. for _, check := range cf.Dependencies { resolved := false for n, chart := range cache { log.Debug("Checking if %s (%s) %s meets %s %s", chart.Name, n, chart.Version, check.Name, check.Version) if chart.From != nil { log.Debug("✔︎") if satisfies(chart.From, check) { resolved = true break } } else { log.Info("Chart %s is pre-0.2.0. Legacy mode enabled.", chart.Name) if chart.Name == check.Name && check.VersionOK(chart.Version) { log.Debug("✔︎") resolved = true break } } } if !resolved { log.Debug("No matches found for %s %s", check.Name, check.Version) res = append(res, check) } } return res, nil }
func uninstallKind(kind []*manifest.Manifest, ns, ktype string, dry bool, client kubectl.Runner) { for _, o := range kind { if dry { log.Msg("%s/%s", ktype, o.Name) } else { // If it's a keeper manifest, skip uninstall. if data, err := o.VersionedObject.JSON(); err == nil { if manifest.IsKeeper(data) { log.Warn("Not uninstalling %s %s because of \"helm-keep\" annotation.\n"+ "---> Use kubectl to uninstall keeper manifests.\n", ktype, o.Name) continue } } out, err := client.Delete(o.Name, ktype, ns) if err != nil { log.Warn("Could not delete %s %s (Skipping): %s", ktype, o.Name, err) } log.Info(string(out)) } } }
// CheckLatest checks whether this version of Helm Classic is the latest version. // // This does not ensure that this is the latest. If a newer version is found, // this generates a message indicating that. // // The passed-in version is the base version that will be checked against the // remote release list. func CheckLatest(version string) { ver, err := release.LatestVersion() if err != nil { log.Warn("Skipped Helm Classic version check: %s", err) return } current, err := semver.NewVersion(version) if err != nil { log.Warn("Local version %s is not well-formed", version) return } remote, err := semver.NewVersion(ver) if err != nil { log.Warn("Remote version %s is not well-formed", ver) return } if remote.GreaterThan(current) { log.Warn("A new version of Helm Classic is available. You have %s. The latest is %v", version, ver) log.Info("Download version %s by running: %s", ver, "curl -s https://get.helm.sh | bash") } }
// 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) }
func printOrQuiet(c *cli.Context, msg string, v ...interface{}) { if !c.Bool("quiet") { log.Info(msg, v...) } }
// 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()) } } }