// CheckLatest checks whether this version of Helm 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 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 is available. You have %s. The latest is %v", version, ver) if dl, err := release.LatestDownloadURL(); err == nil { log.Info("Download version %s here: %s", ver, dl) } } }
// 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") }
// AltInstall allows loading a chart from the current directory. // // It does not directly support chart tables (repos). func AltInstall(chartName, cachedir, home, namespace string, force bool, dryRun bool) { // Make sure there is a chart in the cachedir. if _, err := os.Stat(filepath.Join(cachedir, "Chart.yaml")); err != nil { log.Die("Expected a Chart.yaml in %s: %s", cachedir, err) } // Make sure there is a manifests dir. if fi, err := os.Stat(filepath.Join(cachedir, "manifests")); err != nil { log.Die("Expected 'manifests/' in %s: %s", cachedir, err) } else if !fi.IsDir() { log.Die("Expected 'manifests/' to be a directory in %s: %s", cachedir, err) } dest := filepath.Join(home, WorkspaceChartPath, chartName) if ok, err := isSamePath(dest, cachedir); err != nil || ok { log.Die("Cannot read from and write to the same place: %s. %v", cachedir, err) } // Copy the source chart to the workspace. We ruthlessly overwrite in // this case. if err := copyDir(cachedir, dest); err != nil { log.Die("Failed to copy %s to %s: %s", cachedir, dest, err) } // Load the chart. c, err := chart.Load(dest) 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, filepath.Join(home, WorkspaceChartPath)) 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.") } } msg := "Running `kubectl create -f` ..." if dryRun { msg = "Performing a dry run of `kubectl create -f` ..." } log.Info(msg) if err := uploadManifests(c, namespace, dryRun); err != nil { log.Die("Failed to upload manifests: %s", err) } }
// 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) } 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) }
// LintAll vlaidates all charts are well-formed // // - homedir is the home directory for the user func LintAll(homedir string) { md := util.WorkspaceChartDirectory(homedir, "*") chartPaths, err := filepath.Glob(md) if err != nil { log.Warn("Could not find any charts in %q: %s", md, err) } if len(chartPaths) == 0 { log.Warn("Could not find any charts in %q", md) } else { for _, chartPath := range chartPaths { Lint(chartPath) } } }
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, n %s. Running `helm 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) } }
// openValues opens a values file and tries to parse it with the right parser. // // It returns an interface{} containing data, if found. Any error opening or // parsing the file will be passed back. func openValues(filename string) (interface{}, error) { data, err := ioutil.ReadFile(filename) if err != nil { // We generate a warning here, but do not require that a values // file exists. log.Warn("Skipped file %s: %s", filename, err) return map[string]interface{}{}, nil } ext := filepath.Ext(filename) var um func(p []byte, v interface{}) error switch ext { case ".yaml", ".yml": um = yaml.Unmarshal case ".toml": um = toml.Unmarshal case ".json": um = json.Unmarshal default: return nil, fmt.Errorf("Unsupported file type: %s", ext) } var res interface{} err = um(data, &res) return res, err }
// 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 }
// Valid returns true if every validation passes. func (cv *ChartValidation) Valid() bool { var valid bool = 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 }
// Files gets a list of all manifest files inside of a chart. // // chartDir should contain the path to a chart (the directory which // holds a Chart.yaml file). // // This returns an error if it can't access the directory. func Files(chartDir string) ([]string, error) { dir := filepath.Join(chartDir, "manifests") files := []string{} if _, err := os.Stat(dir); err != nil { return files, err } // add manifest files walker := func(fname string, fi os.FileInfo, e error) error { if e != nil { log.Warn("Encountered error walking %q: %s", fname, e) return nil } if fi.IsDir() { return nil } if filepath.Ext(fname) == ".yaml" { files = append(files, fname) } return nil } filepath.Walk(dir, walker) return files, nil }
// sortManifests sorts manifests into their respective categories, adding to the Chart. func sortManifests(chart *Chart, manifests []*manifest.Manifest) { for _, m := range manifests { vo := m.VersionedObject if m.Version != "v1" { log.Warn("Unsupported version %q", m.Version) continue } switch m.Kind { default: log.Warn("No support for kind %s. Ignoring.", m.Kind) case "Pod": o, err := vo.Pod() if err != nil { log.Warn("Failed conversion: %s", err) } o.Annotations = setOriginFile(o.Annotations, m.Source) chart.Pods = append(chart.Pods, o) case "ReplicationController": o, err := vo.RC() if err != nil { log.Warn("Failed conversion: %s", err) } o.Annotations = setOriginFile(o.Annotations, m.Source) chart.ReplicationControllers = append(chart.ReplicationControllers, o) case "Service": o, err := vo.Service() if err != nil { log.Warn("Failed conversion: %s", err) } o.Annotations = setOriginFile(o.Annotations, m.Source) chart.Services = append(chart.Services, o) case "Secret": o, err := vo.Secret() if err != nil { log.Warn("Failed conversion: %s", err) } o.Annotations = setOriginFile(o.Annotations, m.Source) chart.Secrets = append(chart.Secrets, o) case "PersistentVolume": o, err := vo.PersistentVolume() if err != nil { log.Warn("Failed conversion: %s", err) } o.Annotations = setOriginFile(o.Annotations, m.Source) chart.PersistentVolumes = append(chart.PersistentVolumes, o) case "Namespace": o, err := vo.Namespace() if err != nil { log.Warn("Failed conversion: %s", err) } o.Annotations = setOriginFile(o.Annotations, m.Source) chart.Namespaces = append(chart.Namespaces, o) } } }
// 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 following order: // // - Namespaces // - Secrets // - Volumes // - Services // - Pods // - ReplicationControllers func Install(chartName, home, namespace string, force bool, dryRun bool) { 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 := filepath.Join(home, WorkspaceChartPath, 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, filepath.Join(home, WorkspaceChartPath)) 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.") } } msg := "Running `kubectl create -f` ..." if dryRun { msg = "Performing a dry run of `kubectl create -f` ..." } log.Info(msg) if err := uploadManifests(c, namespace, dryRun); err != nil { log.Die("Failed to upload manifests: %s", err) } log.Info("Done") PrintREADME(chartName, home) }
// Copy a directory and its subdirectories. func copyDir(src, dst string) error { var failure error walker := func(fname string, fi os.FileInfo, e error) error { if e != nil { log.Warn("Encounter error walking %q: %s", fname, e) failure = e return nil } log.Debug("Copying %s", fname) rf, err := filepath.Rel(src, fname) if err != nil { log.Warn("Could not find relative path: %s", err) return nil } df := filepath.Join(dst, rf) // Handle directories by creating mirrors. if fi.IsDir() { if err := os.MkdirAll(df, fi.Mode()); err != nil { log.Warn("Could not create %q: %s", df, err) failure = err } return nil } // Otherwise, copy files. in, err := os.Open(fname) if err != nil { log.Warn("Skipping file %s: %s", fname, err) return nil } out, err := os.Create(df) if err != nil { in.Close() log.Warn("Skipping file copy %s: %s", fname, err) return nil } if _, err = io.Copy(out, in); err != nil { log.Warn("Copy from %s to %s failed: %s", fname, df, err) } if err := out.Close(); err != nil { log.Warn("Failed to close %q: %s", df, err) } if err := in.Close(); err != nil { log.Warn("Failed to close reader %q: %s", fname, err) } return nil } filepath.Walk(src, walker) return failure }
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 { 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)) } } }
// mustConfig parses a config file or dies trying. func mustConfig(homedir string) *config.Configfile { rpath := filepath.Join(homedir, helm.Configfile) cfg, err := config.Load(rpath) if err != nil { log.Warn("Oops! Looks like we had some issues running your command! Running `helm doctor` to ensure we have all the necessary prerequisites in place...") Doctor(homedir) cfg, err = config.Load(rpath) if err != nil { log.Die("Oops! Could not load %s. Error: %s", rpath, err) } log.Info("Continuing onwards and upwards!") } return cfg }
func searchAll(term, homedir string) (map[string]*chart.Chartfile, error) { r := mustConfig(homedir).Repos results := map[string]*chart.Chartfile{} for _, table := range r.Tables { tablename := table.Name if table.Name == r.Default { tablename = "" } base := filepath.Join(homedir, CachePath, table.Name, "*") if err := search(term, base, tablename, results); err != nil { log.Warn("Search error: %s", err) } } return results, nil }
// 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) } }
// PrintREADME prints the README file (if it exists) to the console. func PrintREADME(chart, home string) { p := filepath.Join(home, WorkspaceChartPath, chart, "README.*") files, err := filepath.Glob(p) if err != nil || len(files) == 0 { // No README. Skip. log.Debug("No readme in %s", p) return } f, err := os.Open(files[0]) if err != nil { log.Warn("Could not read README: %s", err) return } log.Msg(strings.Repeat("=", 40)) io.Copy(log.Stdout, f) log.Msg(strings.Repeat("=", 40)) f.Close() }
func deleteChart(c *chart.Chart, ns string) error { // We delete charts in the ALMOST reverse order that we created them. We // start with services to effectively shut down traffic. ktype := "service" for _, o := range c.Services { if err := kubectlDelete(o.Name, ktype, ns); err != nil { log.Warn("Could not delete %s %s (Skipping): %s", ktype, o.Name, err) } } ktype = "pod" for _, o := range c.Pods { if err := kubectlDelete(o.Name, ktype, ns); err != nil { log.Warn("Could not delete %s %s (Skipping): %s", ktype, o.Name, err) } } ktype = "rc" for _, o := range c.ReplicationControllers { if err := kubectlDelete(o.Name, ktype, ns); err != nil { log.Warn("Could not delete %s %s (Skipping): %s", ktype, o.Name, err) } } ktype = "secret" for _, o := range c.Secrets { if err := kubectlDelete(o.Name, ktype, ns); err != nil { log.Warn("Could not delete %s %s (Skipping): %s", ktype, o.Name, err) } } ktype = "persistentvolume" for _, o := range c.PersistentVolumes { if err := kubectlDelete(o.Name, ktype, ns); err != nil { log.Warn("Could not delete %s %s (Skipping): %s", ktype, o.Name, err) } } ktype = "namespace" for _, o := range c.Namespaces { if err := kubectlDelete(o.Name, ktype, ns); err != nil { log.Warn("Could not delete %s %s (Skipping): %s", ktype, o.Name, err) } } return nil }
// 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()) } } }
// Walk walks a chart directory and executes generators as it finds them. // // Returns the number of generators executed. // // Walking will error out whenever a generator cannot be completely executed. // This includes cases such as not finding the generator referenced, and // cases where the generator itself exits with a non-zero exit code. func Walk(dir string, exclude []string) (int, error) { excludes := make(map[string]bool, len(exclude)) for i := 0; i < len(exclude); i++ { excludes[filepath.Join(dir, exclude[i])] = true } count := 0 err := filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error { // dive-bomb if we hit an error. if err != nil { return err } // Exclude anything explicitly excluded. if excludes[path] == true { if fi.IsDir() { return filepath.SkipDir } return nil } // Skip directory entries. If the directory prefix is . or _, skip the // contents of the directory as well. if fi.IsDir() { return skip(path) } f, err := os.Open(path) if err != nil { return err } defer f.Close() line, err := readGenerator(f) if err != nil { return err } if line == "" { return nil } // Run the generator. os.Setenv("HELM_GENERATE_COMMAND", line) os.Setenv("HELM_GENERATE_FILE", path) os.Setenv("HELM_GENERATE_DIR", dir) line = os.ExpandEnv(line) os.Setenv("HELM_GENERATE_COMMAND_EXPANDED", line) log.Debug("File: %s, Command: %s", path, line) count++ // Execute the command in the file's directory to make relative // paths usable. origin, err := os.Getwd() if err != nil { log.Warn("Could not get PWD: %s", err) } else if err := os.Chdir(dir); err != nil { log.Warn("Could not change directory to %s: %s", dir, err) } else { origin = dir defer func() { if e := os.Chdir(origin); e != nil { log.Warn("Could not return to %s: %s", origin, e) } }() } err = execute(line) if err != nil { return fmt.Errorf("failed to execute %s (%s): %s", line, path, err) } return nil }) return count, err }