pull/9417/head
Shoubhik Bose 5 years ago
commit 14853f396a

@ -13,7 +13,7 @@ jobs:
environment:
GOCACHE: "/tmp/go/cache"
GOLANGCI_LINT_VERSION: "1.27.0"
GOLANGCI_LINT_VERSION: "1.36.0"
steps:
- checkout

@ -191,7 +191,9 @@ below.
issue to a milestone until the questions are answered.
- We attempt to do this process at least once per work day.
3. Discussion
- issues that are labeled as `feature` or `bug` should be connected to the PR that resolves it.
- Issues that are labeled `feature` or `proposal` must write a Helm Improvement Proposal (HIP).
See [Proposing an Idea](#proposing-an-idea). Smaller quality-of-life enhancements are exempt.
- Issues that are labeled as `feature` or `bug` should be connected to the PR that resolves it.
- Whoever is working on a `feature` or `bug` issue (whether a maintainer or someone from the
community), should either assign the issue to themself or make a comment in the issue saying
that they are taking it.
@ -200,9 +202,30 @@ below.
and reduce noise. Should the issue need to stay open, the `keep open` label can be added.
4. Issue closure
## Proposing an Idea
Before proposing a new idea to the Helm project, please make sure to write up a [Helm Improvement
Proposal](https://github.com/helm/community/tree/master/hips). A Helm Improvement Proposal is a
design document that describes a new feature for the Helm project. The proposal should provide a
concise technical specification and rationale for the feature.
It is also worth considering vetting your idea with the community via the
[cncf-helm](mailto:cncf-helm@lists.cncf.io) mailing list. Vetting an idea publicly before going as
far as writing a proposal is meant to save the potential author time. Many ideas have been proposed;
it's quite likely there are others in the community who may be working on a similar proposal, or a
similar proposal may have already been written.
HIPs are submitted to the [helm/community repository](https://github.com/helm/community). [HIP
1](https://github.com/helm/community/blob/master/hips/hip-0001.md) describes the process to write a
HIP as well as the review process.
After your proposal has been approved, follow the [developer's
guide](https://helm.sh/docs/community/developers/) to get started.
## How to Contribute a Patch
1. Identify or create the related issue.
1. Identify or create the related issue. If you're proposing a larger change to
Helm, see [Proposing an Idea](#proposing-an-idea).
2. Fork the desired repo; develop and test your code changes.
3. Submit a pull request, making sure to sign your work and link the related issue.

@ -113,7 +113,7 @@ test-coverage:
.PHONY: test-style
test-style:
GO111MODULE=on golangci-lint run
GO111MODULE=on golangci-lint run --timeout 5m0s
@scripts/validate-license.sh
.PHONY: test-acceptance

@ -96,7 +96,6 @@ func newCompletionCmd(out io.Writer) *cobra.Command {
Short: "generate autocompletion script for zsh",
Long: zshCompDesc,
Args: require.NoArgs,
DisableFlagsInUseLine: true,
ValidArgsFunction: noCompletions,
RunE: func(cmd *cobra.Command, args []string) error {
return runCompletionZsh(out, cmd)
@ -109,7 +108,6 @@ func newCompletionCmd(out io.Writer) *cobra.Command {
Short: "generate autocompletion script for fish",
Long: fishCompDesc,
Args: require.NoArgs,
DisableFlagsInUseLine: true,
ValidArgsFunction: noCompletions,
RunE: func(cmd *cobra.Command, args []string) error {
return runCompletionFish(out, cmd)

@ -31,7 +31,12 @@ func checkFileCompletion(t *testing.T, cmdName string, shouldBePerformed bool) {
storage.Create(&release.Release{
Name: "myrelease",
Info: &release.Info{Status: release.StatusDeployed},
Chart: &chart.Chart{},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
Name: "Myrelease-Chart",
Version: "1.2.3",
},
},
Version: 1,
})

@ -68,6 +68,17 @@ func newDocsCmd(out io.Writer) *cobra.Command {
f.StringVar(&o.docTypeString, "type", "markdown", "the type of documentation to generate (markdown, man, bash)")
f.BoolVar(&o.generateHeaders, "generate-headers", false, "generate standard headers for markdown files")
cmd.RegisterFlagCompletionFunc("type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
types := []string{"bash", "man", "markdown"}
var comps []string
for _, t := range types {
if strings.HasPrefix(t, toComplete) {
comps = append(comps, t)
}
}
return comps, cobra.ShellCompDirectiveNoFileComp
})
return cmd
}

@ -20,6 +20,19 @@ import (
"testing"
)
func TestDocsTypeFlagCompletion(t *testing.T) {
tests := []cmdTestCase{{
name: "completion for docs --type",
cmd: "__complete docs --type ''",
golden: "output/docs-type-comp.txt",
}, {
name: "completion for docs --type",
cmd: "__complete docs --type mar",
golden: "output/docs-type-filtered-comp.txt",
}}
runTestCmd(t, tests)
}
func TestDocsFileCompletion(t *testing.T) {
checkFileCompletion(t, "docs", false)
}

@ -21,6 +21,7 @@ import (
"fmt"
"log"
"path/filepath"
"sort"
"strings"
"github.com/spf13/cobra"
@ -66,11 +67,14 @@ func bindOutputFlag(cmd *cobra.Command, varRef *output.Format) {
err := cmd.RegisterFlagCompletionFunc(outputFlag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
var formatNames []string
for _, format := range output.Formats() {
for format, desc := range output.FormatsWithDesc() {
if strings.HasPrefix(format, toComplete) {
formatNames = append(formatNames, format)
formatNames = append(formatNames, fmt.Sprintf("%s\t%s", format, desc))
}
}
// Sort the results to get a deterministic order for the tests
sort.Strings(formatNames)
return formatNames, cobra.ShellCompDirectiveNoFileComp
})
@ -150,7 +154,21 @@ func compVersionFlag(chartRef string, toComplete string) ([]string, cobra.ShellC
for _, details := range indexFile.Entries[chartName] {
version := details.Metadata.Version
if strings.HasPrefix(version, toComplete) {
versions = append(versions, version)
appVersion := details.Metadata.AppVersion
appVersionDesc := ""
if appVersion != "" {
appVersionDesc = fmt.Sprintf("App: %s, ", appVersion)
}
created := details.Created.Format("January 2, 2006")
createdDesc := ""
if created != "" {
createdDesc = fmt.Sprintf("Created: %s ", created)
}
deprecated := ""
if details.Metadata.Deprecated {
deprecated = "(deprecated)"
}
versions = append(versions, fmt.Sprintf("%s\t%s%s%s", version, appVersionDesc, createdDesc, deprecated))
}
}
}

@ -193,7 +193,9 @@ func compListRevisions(toComplete string, cfg *action.Configuration, releaseName
for _, release := range hist {
version := strconv.Itoa(release.Version)
if strings.HasPrefix(version, toComplete) {
revisions = append(revisions, version)
appVersion := fmt.Sprintf("App: %s", release.Chart.Metadata.AppVersion)
chartDesc := fmt.Sprintf("Chart: %s-%s", release.Chart.Metadata.Name, release.Chart.Metadata.Version)
revisions = append(revisions, fmt.Sprintf("%s\t%s, %s", version, appVersion, chartDesc))
}
}
return revisions, cobra.ShellCompDirectiveNoFileComp

@ -126,7 +126,7 @@ func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f.BoolVar(&client.Pending, "pending", false, "show pending releases")
f.BoolVarP(&client.AllNamespaces, "all-namespaces", "A", false, "list releases across all namespaces")
f.IntVarP(&client.Limit, "max", "m", 256, "maximum number of releases to fetch")
f.IntVar(&client.Offset, "offset", 0, "next release name in the list, used to offset from start value")
f.IntVar(&client.Offset, "offset", 0, "next release index in the list, used to offset from start value")
f.StringVarP(&client.Filter, "filter", "f", "", "a regular expression (Perl compatible). Any releases that match the expression will be included in the results")
f.StringVarP(&client.Selector, "selector", "l", "", "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Works only for secret(default) and configmap storage backends.")
bindOutputFlag(cmd, &outfmt)
@ -203,14 +203,15 @@ func compListReleases(toComplete string, cfg *action.Configuration) ([]string, c
client.Filter = fmt.Sprintf("^%s", toComplete)
client.SetStateMask()
results, err := client.Run()
releases, err := client.Run()
if err != nil {
return nil, cobra.ShellCompDirectiveDefault
}
var choices []string
for _, res := range results {
choices = append(choices, res.Name)
for _, rel := range releases {
choices = append(choices,
fmt.Sprintf("%s\t%s-%s -> %s", rel.Name, rel.Chart.Metadata.Name, rel.Chart.Metadata.Version, rel.Info.Status.String()))
}
return choices, cobra.ShellCompDirectiveNoFileComp

@ -51,14 +51,39 @@ func newPluginListCmd(out io.Writer) *cobra.Command {
return cmd
}
// Returns all plugins from plugins, except those with names matching ignoredPluginNames
func filterPlugins(plugins []*plugin.Plugin, ignoredPluginNames []string) []*plugin.Plugin {
// if ignoredPluginNames is nil, just return plugins
if ignoredPluginNames == nil {
return plugins
}
var filteredPlugins []*plugin.Plugin
for _, plugin := range plugins {
found := false
for _, ignoredName := range ignoredPluginNames {
if plugin.Metadata.Name == ignoredName {
found = true
break
}
}
if !found {
filteredPlugins = append(filteredPlugins, plugin)
}
}
return filteredPlugins
}
// Provide dynamic auto-completion for plugin names
func compListPlugins(toComplete string) []string {
func compListPlugins(toComplete string, ignoredPluginNames []string) []string {
var pNames []string
plugins, err := plugin.FindPlugins(settings.PluginsDirectory)
if err == nil {
for _, p := range plugins {
if err == nil && len(plugins) > 0 {
filteredPlugins := filterPlugins(plugins, ignoredPluginNames)
for _, p := range filteredPlugins {
if strings.HasPrefix(p.Metadata.Name, toComplete) {
pNames = append(pNames, p.Metadata.Name)
pNames = append(pNames, fmt.Sprintf("%s\t%s", p.Metadata.Name, p.Metadata.Usage))
}
}
}

@ -305,6 +305,50 @@ func TestLoadPlugins_HelmNoPlugins(t *testing.T) {
}
}
func TestPluginCmdsCompletion(t *testing.T) {
tests := []cmdTestCase{{
name: "completion for plugin update",
cmd: "__complete plugin update ''",
golden: "output/plugin_list_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin update repetition",
cmd: "__complete plugin update args ''",
golden: "output/plugin_repeat_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin uninstall",
cmd: "__complete plugin uninstall ''",
golden: "output/plugin_list_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin uninstall repetition",
cmd: "__complete plugin uninstall args ''",
golden: "output/plugin_repeat_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin list",
cmd: "__complete plugin list ''",
golden: "output/empty_nofile_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin install no args",
cmd: "__complete plugin install ''",
golden: "output/empty_default_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin install one arg",
cmd: "__complete plugin list /tmp ''",
golden: "output/empty_nofile_comp.txt",
rels: []*release.Release{},
}, {}}
for _, test := range tests {
settings.PluginsDirectory = "testdata/helmhome/helm/plugins"
runTestCmd(t, []cmdTestCase{test})
}
}
func TestPluginFileCompletion(t *testing.T) {
checkFileCompletion(t, "plugin", false)
}

@ -39,10 +39,7 @@ func newPluginUninstallCmd(out io.Writer) *cobra.Command {
Aliases: []string{"rm", "remove"},
Short: "uninstall one or more Helm plugins",
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return compListPlugins(toComplete), cobra.ShellCompDirectiveNoFileComp
return compListPlugins(toComplete, args), cobra.ShellCompDirectiveNoFileComp
},
PreRunE: func(cmd *cobra.Command, args []string) error {
return o.complete(args)

@ -40,10 +40,7 @@ func newPluginUpdateCmd(out io.Writer) *cobra.Command {
Aliases: []string{"up"},
Short: "update one or more Helm plugins",
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return compListPlugins(toComplete), cobra.ShellCompDirectiveNoFileComp
return compListPlugins(toComplete, args), cobra.ShellCompDirectiveNoFileComp
},
PreRunE: func(cmd *cobra.Command, args []string) error {
return o.complete(args)

@ -63,9 +63,15 @@ func newRepoUpdateCmd(out io.Writer) *cobra.Command {
func (o *repoUpdateOptions) run(out io.Writer) error {
f, err := repo.LoadFile(o.repoFile)
if isNotExist(err) || len(f.Repositories) == 0 {
switch {
case isNotExist(err):
return errNoRepositories
case err != nil:
return errors.Wrapf(err, "failed loading file: %s", o.repoFile)
case len(f.Repositories) == 0:
return errNoRepositories
}
var repos []*repo.ChartRepository
for _, cfg := range f.Repositories {
r, err := repo.NewChartRepository(cfg, getter.All(settings))

@ -19,6 +19,7 @@ import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
@ -53,20 +54,27 @@ func TestUpdateCmd(t *testing.T) {
}
func TestUpdateCustomCacheCmd(t *testing.T) {
var out bytes.Buffer
rootDir := ensure.TempDir(t)
cachePath := filepath.Join(rootDir, "updcustomcache")
_ = os.Mkdir(cachePath, os.ModePerm)
os.Mkdir(cachePath, os.ModePerm)
defer os.RemoveAll(cachePath)
ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*")
if err != nil {
t.Fatal(err)
}
defer ts.Stop()
o := &repoUpdateOptions{
update: updateCharts,
repoFile: "testdata/repositories.yaml",
repoFile: filepath.Join(ts.Root(), "repositories.yaml"),
repoCache: cachePath,
}
if err := o.run(&out); err != nil {
b := ioutil.Discard
if err := o.run(b); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(filepath.Join(cachePath, "charts-index.yaml")); err != nil {
if _, err := os.Stat(filepath.Join(cachePath, "test-index.yaml")); err != nil {
t.Fatalf("error finding created index file in custom cache: %v", err)
}
}

@ -131,13 +131,13 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
if config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
loadingRules,
&clientcmd.ConfigOverrides{}).RawConfig(); err == nil {
ctxs := []string{}
for name := range config.Contexts {
comps := []string{}
for name, context := range config.Contexts {
if strings.HasPrefix(name, toComplete) {
ctxs = append(ctxs, name)
comps = append(comps, fmt.Sprintf("%s\t%s", name, context.Cluster))
}
}
return ctxs, cobra.ShellCompDirectiveNoFileComp
return comps, cobra.ShellCompDirectiveNoFileComp
}
return nil, cobra.ShellCompDirectiveNoFileComp
})

@ -143,7 +143,7 @@ func (o *searchRepoOptions) setupSearchedVersion() {
}
func (o *searchRepoOptions) applyConstraint(res []*search.Result) ([]*search.Result, error) {
if len(o.version) == 0 {
if o.version == "" {
return res, nil
}
@ -154,26 +154,19 @@ func (o *searchRepoOptions) applyConstraint(res []*search.Result) ([]*search.Res
data := res[:0]
foundNames := map[string]bool{}
appendSearchResults := func(res *search.Result) {
data = append(data, res)
if !o.versions {
foundNames[res.Name] = true // If user hasn't requested all versions, only show the latest that matches
}
}
for _, r := range res {
if _, found := foundNames[r.Name]; found {
// if not returning all versions and already have found a result,
// you're done!
if !o.versions && foundNames[r.Name] {
continue
}
v, err := semver.NewVersion(r.Chart.Version)
if err != nil {
// If the current version number check appears ErrSegmentStartsZero or ErrInvalidPrerelease error and not devel mode, ignore
if (err == semver.ErrSegmentStartsZero || err == semver.ErrInvalidPrerelease) && !o.devel {
continue
}
appendSearchResults(r)
} else if constraint.Check(v) {
appendSearchResults(r)
if constraint.Check(v) {
data = append(data, r)
foundNames[r.Name] = true
}
}
@ -194,6 +187,7 @@ func (o *searchRepoOptions) buildIndex() (*search.Index, error) {
ind, err := repo.LoadIndexFile(f)
if err != nil {
warning("Repo %q is corrupt or missing. Try 'helm repo update'.", n)
warning("%s", err)
continue
}

@ -68,14 +68,6 @@ func TestSearchRepositoriesCmd(t *testing.T) {
name: "search for 'maria', expect valid json output",
cmd: "search repo maria --output json",
golden: "output/search-output-json.txt",
}, {
name: "search for 'maria', expect one match with semver begin with zero development version",
cmd: "search repo maria --devel",
golden: "output/search-semver-pre-zero-devel-release.txt",
}, {
name: "search for 'nginx-ingress', expect one match with invalid development pre version",
cmd: "search repo nginx-ingress --devel",
golden: "output/search-semver-pre-invalid-release.txt",
}, {
name: "search for 'alpine', expect valid yaml output",
cmd: "search repo alpine --output yaml",

@ -118,56 +118,72 @@ func mustParseTime(t string) helmtime.Time {
}
func TestStatusCompletion(t *testing.T) {
releasesMockWithStatus := func(info *release.Info, hooks ...*release.Hook) []*release.Release {
info.LastDeployed = helmtime.Unix(1452902400, 0).UTC()
return []*release.Release{{
rels := []*release.Release{
{
Name: "athos",
Namespace: "default",
Info: info,
Chart: &chart.Chart{},
Hooks: hooks,
Info: &release.Info{
Status: release.StatusDeployed,
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
Name: "Athos-chart",
Version: "1.2.3",
},
},
}, {
Name: "porthos",
Namespace: "default",
Info: info,
Chart: &chart.Chart{},
Hooks: hooks,
Info: &release.Info{
Status: release.StatusFailed,
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
Name: "Porthos-chart",
Version: "111.222.333",
},
},
}, {
Name: "aramis",
Namespace: "default",
Info: info,
Chart: &chart.Chart{},
Hooks: hooks,
Info: &release.Info{
Status: release.StatusUninstalled,
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
Name: "Aramis-chart",
Version: "0.0.0",
},
},
}, {
Name: "dartagnan",
Namespace: "gascony",
Info: info,
Chart: &chart.Chart{},
Hooks: hooks,
Info: &release.Info{
Status: release.StatusUnknown,
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
Name: "Dartagnan-chart",
Version: "1.2.3-prerelease",
},
},
}}
}
tests := []cmdTestCase{{
name: "completion for status",
cmd: "__complete status a",
golden: "output/status-comp.txt",
rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed,
}),
rels: rels,
}, {
name: "completion for status with too many arguments",
cmd: "__complete status dartagnan ''",
golden: "output/status-wrong-args-comp.txt",
rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed,
}),
rels: rels,
}, {
name: "completion for status with too many arguments",
name: "completion for status with global flag",
cmd: "__complete status --debug a",
golden: "output/status-comp.txt",
rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed,
}),
rels: rels,
}}
runTestCmd(t, tests)
}

@ -4,6 +4,8 @@ entries:
- name: alpine
url: https://charts.helm.sh/stable/alpine-0.1.0.tgz
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
created: "2018-06-27T10:00:18.230700509Z"
deprecated: true
home: https://helm.sh/helm
sources:
- https://github.com/helm/helm
@ -13,9 +15,11 @@ entries:
keywords: []
maintainers: []
icon: ""
apiVersion: v2
- name: alpine
url: https://charts.helm.sh/stable/alpine-0.2.0.tgz
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
created: "2018-07-09T11:34:37.797864902Z"
home: https://helm.sh/helm
sources:
- https://github.com/helm/helm
@ -25,9 +29,11 @@ entries:
keywords: []
maintainers: []
icon: ""
apiVersion: v2
- name: alpine
url: https://charts.helm.sh/stable/alpine-0.3.0-rc.1.tgz
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
created: "2020-11-12T08:44:58.872726222Z"
home: https://helm.sh/helm
sources:
- https://github.com/helm/helm
@ -37,10 +43,12 @@ entries:
keywords: []
maintainers: []
icon: ""
apiVersion: v2
mariadb:
- name: mariadb
url: https://charts.helm.sh/stable/mariadb-0.3.0.tgz
checksum: 65229f6de44a2be9f215d11dbff311673fc8ba56
created: "2018-04-23T08:20:27.160959131Z"
home: https://mariadb.org
sources:
- https://github.com/bitnami/bitnami-docker-mariadb
@ -55,33 +63,4 @@ entries:
- name: Bitnami
email: containers@bitnami.com
icon: ""
- name: mariadb
url: https://charts.helm.sh/stable/mariadb-0.3.0-0565674.tgz
checksum: 65229f6de44a2be9f215d11dbff311673fc8ba56
home: https://mariadb.org
sources:
- https://github.com/bitnami/bitnami-docker-mariadb
version: 0.3.0-0565674
description: Chart for MariaDB
keywords:
- mariadb
- mysql
- database
- sql
maintainers:
- name: Bitnami
email: containers@bitnami.com
icon: ""
nginx-ingress:
- name: nginx-ingress
url: https://github.com/kubernetes/ingress-nginx/ingress-a.b.c.sdfsdf.tgz
checksum: 25229f6de44a2be9f215d11dbff31167ddc8ba56
home: https://github.com/kubernetes/ingress-nginx
sources:
- https://github.com/kubernetes/ingress-nginx
version: a.b.c.sdfsdf
description: Chart for nginx-ingress
keywords:
- ingress
- nginx
icon: ""
apiVersion: v2

@ -0,0 +1,5 @@
bash
man
markdown
:4
Completion ended with directive: ShellCompDirectiveNoFileComp

@ -0,0 +1,3 @@
markdown
:4
Completion ended with directive: ShellCompDirectiveNoFileComp

@ -0,0 +1,2 @@
:0
Completion ended with directive: ShellCompDirectiveDefault

@ -0,0 +1,2 @@
:4
Completion ended with directive: ShellCompDirectiveNoFileComp

@ -1,5 +1,5 @@
table
json
yaml
json Output result in JSON format
table Output result in human-readable format
yaml Output result in YAML format
:4
Completion ended with directive: ShellCompDirectiveNoFileComp

@ -0,0 +1,7 @@
args echo args
echo echo stuff
env env stuff
exitwith exitwith code
fullenv show env vars
:4
Completion ended with directive: ShellCompDirectiveNoFileComp

@ -0,0 +1,6 @@
echo echo stuff
env env stuff
exitwith exitwith code
fullenv show env vars
:4
Completion ended with directive: ShellCompDirectiveNoFileComp

@ -1,6 +1,6 @@
8
9
10
11
8 App: 1.0, Chart: foo-0.1.0-beta.1
9 App: 1.0, Chart: foo-0.1.0-beta.1
10 App: 1.0, Chart: foo-0.1.0-beta.1
11 App: 1.0, Chart: foo-0.1.0-beta.1
:4
Completion ended with directive: ShellCompDirectiveNoFileComp

@ -1,4 +1,4 @@
carabins
musketeers
carabins foo-0.1.0-beta.1 -> superseded
musketeers foo-0.1.0-beta.1 -> deployed
:4
Completion ended with directive: ShellCompDirectiveNoFileComp

@ -1,2 +0,0 @@
NAME CHART VERSION APP VERSION DESCRIPTION
testing/nginx-ingress a.b.c.sdfsdf Chart for nginx-ingress

@ -1,2 +0,0 @@
NAME CHART VERSION APP VERSION DESCRIPTION
testing/mariadb 0.3.0-0565674 Chart for MariaDB

@ -1,4 +1,4 @@
aramis
athos
aramis Aramis-chart-0.0.0 -> uninstalled
athos Athos-chart-1.2.3 -> deployed
:4
Completion ended with directive: ShellCompDirectiveNoFileComp

@ -1,5 +1,5 @@
0.3.0-rc.1
0.2.0
0.1.0
0.3.0-rc.1 App: 3.0.0, Created: November 12, 2020
0.2.0 App: 2.3.4, Created: July 9, 2018
0.1.0 App: 1.2.3, Created: June 27, 2018 (deprecated)
:4
Completion ended with directive: ShellCompDirectiveNoFileComp

@ -5,15 +5,15 @@ go 1.15
require (
github.com/BurntSushi/toml v0.3.1
github.com/DATA-DOG/go-sqlmock v1.5.0
github.com/Masterminds/goutils v1.1.0
github.com/Masterminds/goutils v1.1.1
github.com/Masterminds/semver/v3 v3.1.1
github.com/Masterminds/sprig/v3 v3.2.0
github.com/Masterminds/sprig/v3 v3.2.2
github.com/Masterminds/squirrel v1.5.0
github.com/Masterminds/vcs v1.13.1
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535
github.com/containerd/containerd v1.4.3
github.com/cyphar/filepath-securejoin v0.2.2
github.com/deislabs/oras v0.9.0
github.com/deislabs/oras v0.10.0
github.com/docker/distribution v2.7.1+incompatible
github.com/docker/docker v1.4.2-0.20200203170920-46ec8731fbce
github.com/docker/go-units v0.4.0
@ -24,7 +24,7 @@ require (
github.com/jmoiron/sqlx v1.2.0
github.com/lib/pq v1.9.0
github.com/mattn/go-shellwords v1.0.11
github.com/mitchellh/copystructure v1.0.0
github.com/mitchellh/copystructure v1.1.1
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.0.1
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
@ -44,7 +44,7 @@ require (
k8s.io/apiserver v0.20.2
k8s.io/cli-runtime v0.20.2
k8s.io/client-go v0.20.2
k8s.io/klog/v2 v2.4.0
k8s.io/klog/v2 v2.5.0
k8s.io/kubectl v0.20.2
sigs.k8s.io/yaml v1.2.0
)

@ -52,12 +52,12 @@ github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd h1:sjQovDkwrZp8u+gxLtPgKGjk5hCxuy2hrRejBTA9xFU=
github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E=
github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg=
github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Masterminds/sprig/v3 v3.2.0 h1:P1ekkbuU73Ui/wS0nK1HOM37hh4xdfZo485UPf8rc+Y=
github.com/Masterminds/sprig/v3 v3.2.0/go.mod h1:tWhwTbUTndesPNeF0C900vKoq283u6zp4APT9vaF3SI=
github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8=
github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk=
github.com/Masterminds/squirrel v1.5.0 h1:JukIZisrUXadA9pl3rMkjhiamxiB0cXiu+HGp/Y8cY8=
github.com/Masterminds/squirrel v1.5.0/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
github.com/Masterminds/vcs v1.13.1 h1:NL3G1X7/7xduQtA2sJLpVpfHTNBALVNSjob6KEjPXNQ=
@ -170,16 +170,16 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE=
github.com/deislabs/oras v0.9.0 h1:R6PRN3bTruUjHcGKgdteurzbpsCxwf3XJCLsxLFyBuU=
github.com/deislabs/oras v0.9.0/go.mod h1:QXnMi3+eEm/rkgGT6L+Lt0TT2WLA7pOzuk7tZIsUhFM=
github.com/deislabs/oras v0.10.0 h1:Eufbi8zVaULb7vYj5HKM9qv9qw6fJ7P75JSjn//gR0E=
github.com/deislabs/oras v0.10.0/go.mod h1:N1UzE7rBa9qLyN4l8IlBTxc2PkrRcKgWQ3HTJvRnJRE=
github.com/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=
github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/docker/cli v20.10.2+incompatible h1:CR/6BZX5w3TLgAHZTyRpVh3yi+Q8Sj5j1fCsb0J2rCk=
github.com/docker/cli v20.10.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v20.10.3+incompatible h1:WVEgoV/GpsTK5hruhHdYi79blQ+nmcm+7Ru/ZuiF+7E=
github.com/docker/cli v20.10.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v0.0.0-20191216044856-a8371794149d h1:jC8tT/S0OGx2cswpeUTn4gOIea8P08lD3VFQT0cOZ50=
github.com/docker/distribution v0.0.0-20191216044856-a8371794149d/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
github.com/docker/docker-credential-helpers v0.6.3 h1:zI2p9+1NQYdnG6sMU26EX4aVGlqbInSQxQXLvzJ4RPQ=
@ -242,6 +242,8 @@ github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-logr/logr v0.2.0 h1:QvGt2nLcHH0WK9orKa+ppBPAxREcH364nPUedEpK0TY=
github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc=
github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
@ -466,6 +468,8 @@ github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3N
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/copystructure v1.1.1 h1:Bp6x9R1Wn16SIz3OfeDr0b7RnCG2OB66Y7PQyC/cvq4=
github.com/mitchellh/copystructure v1.1.1/go.mod h1:EBArHfARyrSWO/+Wyr9zwEkc6XMFB9XyNgFNmRkZZU4=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
@ -479,6 +483,8 @@ github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f h1:2+myh5ml7lgEU/5
github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE=
github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/moby v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible h1:NT0cwArZg/wGdvY8pzej4tPr+9WGmDdkF8Suj+mkz2g=
github.com/moby/moby v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc=
github.com/moby/term v0.0.0-20200312100748-672ec06f55cd h1:aY7OQNf2XqY/JQ6qREWamhI/81os/agb2BAGpcx5yWI=
@ -836,6 +842,8 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3 h1:kzM6+9dur93BcC2kVlYl34cHU+TYZLanmpSJHVMmL64=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -1032,6 +1040,8 @@ k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
k8s.io/klog/v2 v2.4.0 h1:7+X0fUguPyrKEC4WjH8iGDg3laWgMo5tMnRTIGTTxGQ=
k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
k8s.io/klog/v2 v2.5.0 h1:8mOnjf1RmUPW6KRqQCfYSZq/K20Unmp3IhuZUhxl8KI=
k8s.io/klog/v2 v2.5.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec=
k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd h1:sOHNzJIkytDF6qadMNKhhDRpc6ODik8lVC6nOur7B2c=
k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM=
k8s.io/kubectl v0.20.2 h1:mXExF6N4eQUYmlfXJmfWIheCBLF6/n4VnwQKbQki5iE=

@ -13,6 +13,7 @@ entries:
keywords: []
maintainers: []
icon: ""
apiVersion: v2
- name: alpine
urls:
- https://charts.helm.sh/stable/alpine-0.2.0.tgz
@ -25,6 +26,7 @@ entries:
keywords: []
maintainers: []
icon: ""
apiVersion: v2
mariadb:
- name: mariadb
urls:
@ -44,3 +46,4 @@ entries:
- name: Bitnami
email: containers@bitnami.com
icon: ""
apiVersion: v2

@ -77,6 +77,7 @@ func path(filename string) string {
}
func compare(actual []byte, filename string) error {
actual = normalize(actual)
if err := update(filename, actual); err != nil {
return err
}
@ -85,6 +86,7 @@ func compare(actual []byte, filename string) error {
if err != nil {
return errors.Wrapf(err, "unable to read testdata %s", filename)
}
expected = normalize(expected)
if !bytes.Equal(expected, actual) {
return errors.Errorf("does not match golden file %s\n\nWANT:\n'%s'\n\nGOT:\n'%s'\n", filename, expected, actual)
}

@ -111,6 +111,10 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error)
}
kept, errs := u.deleteRelease(rel)
if kept != "" {
kept = "These resources were kept due to the resource policy:\n" + kept
}
res.Info = kept
if !u.DisableHooks {
@ -189,7 +193,7 @@ func (u *Uninstall) deleteRelease(rel *release.Release) (string, []error) {
filesToKeep, filesToDelete := filterManifestsToKeep(files)
var kept string
for _, f := range filesToKeep {
kept += f.Name + "\n"
kept += "[" + f.Head.Kind + "] " + f.Head.Metadata.Name + "\n"
}
var builder strings.Builder

@ -0,0 +1,62 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"testing"
"github.com/stretchr/testify/assert"
)
func uninstallAction(t *testing.T) *Uninstall {
config := actionConfigFixture(t)
unAction := NewUninstall(config)
return unAction
}
func TestUninstallRelease_deleteRelease(t *testing.T) {
is := assert.New(t)
unAction := uninstallAction(t)
unAction.DisableHooks = true
unAction.DryRun = false
unAction.KeepHistory = true
rel := releaseStub()
rel.Name = "keep-secret"
rel.Manifest = `{
"apiVersion": "v1",
"kind": "Secret",
"metadata": {
"name": "secret",
"annotations": {
"helm.sh/resource-policy": "keep"
}
},
"type": "Opaque",
"data": {
"password": "password"
}
}`
unAction.cfg.Releases.Create(rel)
res, err := unAction.Run(rel.Name)
is.NoError(err)
expected := `These resources were kept due to the resource policy:
[Secret] secret
`
is.Contains(res.Info, expected)
}

@ -49,6 +49,23 @@ type Dependency struct {
Alias string `json:"alias,omitempty"`
}
// Validate checks for common problems with the dependency datastructure in
// the chart. This check must be done at load time before the dependency's charts are
// loaded.
func (d *Dependency) Validate() error {
d.Name = sanitizeString(d.Name)
d.Version = sanitizeString(d.Version)
d.Repository = sanitizeString(d.Repository)
d.Condition = sanitizeString(d.Condition)
for i := range d.Tags {
d.Tags[i] = sanitizeString(d.Tags[i])
}
if d.Alias != "" && !aliasNameFormat.MatchString(d.Alias) {
return ValidationErrorf("dependency %q has disallowed characters in the alias", d.Name)
}
return nil
}
// Lock is a lock file for dependencies.
//
// It represents the state that the dependencies should be in.

@ -0,0 +1,44 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package chart
import (
"testing"
)
func TestValidateDependency(t *testing.T) {
dep := &Dependency{
Name: "example",
}
for value, shouldFail := range map[string]bool{
"abcdefghijklmenopQRSTUVWXYZ-0123456780_": false,
"-okay": false,
"_okay": false,
"- bad": true,
" bad": true,
"bad\nvalue": true,
"bad ": true,
"bad$": true,
} {
dep.Alias = value
res := dep.Validate()
if res != nil && !shouldFail {
t.Errorf("Failed on case %q", dep.Alias)
} else if res == nil && shouldFail {
t.Errorf("Expected failure for %q", dep.Alias)
}
}
}

@ -15,6 +15,13 @@ limitations under the License.
package chart
import (
"strings"
"unicode"
"github.com/Masterminds/semver/v3"
)
// Maintainer describes a Chart maintainer.
type Maintainer struct {
// Name is a user name or organization name
@ -25,15 +32,23 @@ type Maintainer struct {
URL string `json:"url,omitempty"`
}
// Validate checks valid data and sanitizes string characters.
func (m *Maintainer) Validate() error {
m.Name = sanitizeString(m.Name)
m.Email = sanitizeString(m.Email)
m.URL = sanitizeString(m.URL)
return nil
}
// Metadata for a Chart file. This models the structure of a Chart.yaml file.
type Metadata struct {
// The name of the chart
// The name of the chart. Required.
Name string `json:"name,omitempty"`
// The URL to a relevant project page, git repo, or contact person
Home string `json:"home,omitempty"`
// Source is the URL to the source code of this chart
Sources []string `json:"sources,omitempty"`
// A SemVer 2 conformant version string of the chart
// A SemVer 2 conformant version string of the chart. Required.
Version string `json:"version,omitempty"`
// A one-sentence description of the chart
Description string `json:"description,omitempty"`
@ -43,7 +58,7 @@ type Metadata struct {
Maintainers []*Maintainer `json:"maintainers,omitempty"`
// The URL to an icon file.
Icon string `json:"icon,omitempty"`
// The API Version of this chart.
// The API Version of this chart. Required.
APIVersion string `json:"apiVersion,omitempty"`
// The condition to check to enable chart
Condition string `json:"condition,omitempty"`
@ -64,11 +79,28 @@ type Metadata struct {
Type string `json:"type,omitempty"`
}
// Validate checks the metadata for known issues, returning an error if metadata is not correct
// Validate checks the metadata for known issues and sanitizes string
// characters.
func (md *Metadata) Validate() error {
if md == nil {
return ValidationError("chart.metadata is required")
}
md.Name = sanitizeString(md.Name)
md.Description = sanitizeString(md.Description)
md.Home = sanitizeString(md.Home)
md.Icon = sanitizeString(md.Icon)
md.Condition = sanitizeString(md.Condition)
md.Tags = sanitizeString(md.Tags)
md.AppVersion = sanitizeString(md.AppVersion)
md.KubeVersion = sanitizeString(md.KubeVersion)
for i := range md.Sources {
md.Sources[i] = sanitizeString(md.Sources[i])
}
for i := range md.Keywords {
md.Keywords[i] = sanitizeString(md.Keywords[i])
}
if md.APIVersion == "" {
return ValidationError("chart.metadata.apiVersion is required")
}
@ -78,19 +110,26 @@ func (md *Metadata) Validate() error {
if md.Version == "" {
return ValidationError("chart.metadata.version is required")
}
if !isValidSemver(md.Version) {
return ValidationErrorf("chart.metadata.version %q is invalid", md.Version)
}
if !isValidChartType(md.Type) {
return ValidationError("chart.metadata.type must be application or library")
}
for _, m := range md.Maintainers {
if err := m.Validate(); err != nil {
return err
}
}
// Aliases need to be validated here to make sure that the alias name does
// not contain any illegal characters.
for _, dependency := range md.Dependencies {
if err := validateDependency(dependency); err != nil {
if err := dependency.Validate(); err != nil {
return err
}
}
// TODO validate valid semver here?
return nil
}
@ -102,12 +141,20 @@ func isValidChartType(in string) bool {
return false
}
// validateDependency checks for common problems with the dependency datastructure in
// the chart. This check must be done at load time before the dependency's charts are
// loaded.
func validateDependency(dep *Dependency) error {
if len(dep.Alias) > 0 && !aliasNameFormat.MatchString(dep.Alias) {
return ValidationErrorf("dependency %q has disallowed characters in the alias", dep.Name)
func isValidSemver(v string) bool {
_, err := semver.NewVersion(v)
return err == nil
}
return nil
// sanitizeString normalize spaces and removes non-printable characters.
func sanitizeString(str string) string {
return strings.Map(func(r rune) rune {
if unicode.IsSpace(r) {
return ' '
}
if unicode.IsPrint(r) {
return r
}
return -1
}, str)
}

@ -72,6 +72,10 @@ func TestValidate(t *testing.T) {
},
ValidationError("dependency \"bad\" has disallowed characters in the alias"),
},
{
&Metadata{APIVersion: "v2", Name: "test", Version: "1.2.3.4"},
ValidationError("chart.metadata.version \"1.2.3.4\" is invalid"),
},
}
for _, tt := range tests {
@ -82,26 +86,15 @@ func TestValidate(t *testing.T) {
}
}
func TestValidateDependency(t *testing.T) {
dep := &Dependency{
Name: "example",
func TestValidate_sanitize(t *testing.T) {
md := &Metadata{APIVersion: "v2", Name: "test", Version: "1.0", Description: "\adescr\u0081iption\rtest", Maintainers: []*Maintainer{{Name: "\r"}}}
if err := md.Validate(); err != nil {
t.Fatalf("unexpected error: %s", err)
}
for value, shouldFail := range map[string]bool{
"abcdefghijklmenopQRSTUVWXYZ-0123456780_": false,
"-okay": false,
"_okay": false,
"- bad": true,
" bad": true,
"bad\nvalue": true,
"bad ": true,
"bad$": true,
} {
dep.Alias = value
res := validateDependency(dep)
if res != nil && !shouldFail {
t.Errorf("Failed on case %q", dep.Alias)
} else if res == nil && shouldFail {
t.Errorf("Expected failure for %q", dep.Alias)
if md.Description != "description test" {
t.Fatalf("description was not sanitized: %q", md.Description)
}
if md.Maintainers[0].Name != " " {
t.Fatal("maintainer name was not sanitized")
}
}

@ -139,7 +139,7 @@ func TestSavePreservesTimestamps(t *testing.T) {
Metadata: &chart.Metadata{
APIVersion: chart.APIVersionV1,
Name: "ahab",
Version: "1.2.3.4",
Version: "1.2.3",
},
Values: map[string]interface{}{
"imageName": "testimage",

@ -40,6 +40,16 @@ func Formats() []string {
return []string{Table.String(), JSON.String(), YAML.String()}
}
// FormatsWithDesc returns a list of the string representation of the supported formats
// including a description
func FormatsWithDesc() map[string]string {
return map[string]string{
Table.String(): "Output result in human-readable format",
JSON.String(): "Output result in JSON format",
YAML.String(): "Output result in YAML format",
}
}
// ErrInvalidFormatType is returned when an unsupported format type is used
var ErrInvalidFormatType = fmt.Errorf("invalid format type")

@ -13,6 +13,7 @@ entries:
keywords: []
maintainers: []
icon: ""
apiVersion: v2
- name: alpine
urls:
- https://charts.helm.sh/stable/alpine-0.2.0.tgz
@ -25,6 +26,7 @@ entries:
keywords: []
maintainers: []
icon: ""
apiVersion: v2
mariadb:
- name: mariadb
urls:
@ -44,3 +46,4 @@ entries:
- name: Bitnami
email: containers@bitnami.com
icon: ""
apiVersion: v2

@ -13,3 +13,4 @@ entries:
keywords: []
maintainers: []
icon: ""
apiVersion: v2

@ -12,3 +12,4 @@ entries:
- http://username:password@example.com/foo-1.2.3.tgz
version: 1.2.3
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
apiVersion: v2

@ -12,3 +12,4 @@ entries:
- https://example.com/foo-1.2.3.tgz
version: 1.2.3
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
apiVersion: v2

@ -12,3 +12,4 @@ entries:
- https://example.com/foo-1.2.3.tgz
version: 1.2.3
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
apiVersion: v2

@ -13,6 +13,7 @@ entries:
keywords: []
maintainers: []
icon: ""
apiVersion: v2
- name: alpine
urls:
- http://example.com/alpine-0.2.0.tgz
@ -26,6 +27,7 @@ entries:
keywords: []
maintainers: []
icon: ""
apiVersion: v2
foo:
- name: foo
description: Foo Chart
@ -38,3 +40,4 @@ entries:
- http://example.com/foo-1.2.3.tgz
version: 1.2.3
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
apiVersion: v2

@ -13,3 +13,4 @@ entries:
keywords: []
maintainers: []
icon: ""
apiVersion: v2

@ -12,6 +12,7 @@ entries:
- charts/foo-1.2.3.tgz
version: 1.2.3
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
apiVersion: v2
bar:
- name: bar
description: Bar Chart With Relative Path
@ -24,3 +25,4 @@ entries:
- bar-1.2.3.tgz
version: 1.2.3
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
apiVersion: v2

@ -12,6 +12,7 @@ entries:
- charts/foo-1.2.3.tgz
version: 1.2.3
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
apiVersion: v2
bar:
- name: bar
description: Bar Chart With Relative Path
@ -24,3 +25,4 @@ entries:
- bar-1.2.3.tgz
version: 1.2.3
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
apiVersion: v2

@ -23,6 +23,7 @@ import (
"regexp"
"runtime"
"strings"
"unicode"
"github.com/pkg/errors"
"sigs.k8s.io/yaml"
@ -175,10 +176,25 @@ func validatePluginData(plug *Plugin, filepath string) error {
if !validPluginName.MatchString(plug.Metadata.Name) {
return fmt.Errorf("invalid plugin name at %q", filepath)
}
plug.Metadata.Usage = sanitizeString(plug.Metadata.Usage)
// We could also validate SemVer, executable, and other fields should we so choose.
return nil
}
// sanitizeString normalize spaces and removes non-printable characters.
func sanitizeString(str string) string {
return strings.Map(func(r rune) rune {
if unicode.IsSpace(r) {
return ' '
}
if unicode.IsPrint(r) {
return r
}
return -1
}, str)
}
func detectDuplicates(plugs []*Plugin) error {
names := map[string]string{}

@ -82,6 +82,8 @@ func NewChartRepository(cfg *Entry, getters getter.Providers) (*ChartRepository,
// Load loads a directory of charts as if it were a repository.
//
// It requires the presence of an index.yaml file in the directory.
//
// Deprecated: remove in Helm 4.
func (r *ChartRepository) Load() error {
dirInfo, err := os.Stat(r.Config.Name)
if err != nil {
@ -99,7 +101,7 @@ func (r *ChartRepository) Load() error {
if strings.Contains(f.Name(), "-index.yaml") {
i, err := LoadIndexFile(path)
if err != nil {
return nil
return err
}
r.IndexFile = i
} else if strings.HasSuffix(f.Name(), ".tgz") {
@ -137,7 +139,7 @@ func (r *ChartRepository) DownloadIndexFile() (string, error) {
return "", err
}
indexFile, err := loadIndex(index)
indexFile, err := loadIndex(index, r.Config.URL)
if err != nil {
return "", err
}
@ -187,7 +189,9 @@ func (r *ChartRepository) generateIndex() error {
}
if !r.IndexFile.Has(ch.Name(), ch.Metadata.Version) {
r.IndexFile.Add(ch.Metadata, path, r.Config.URL, digest)
if err := r.IndexFile.MustAdd(ch.Metadata, path, r.Config.URL, digest); err != nil {
return errors.Wrapf(err, "failed adding to %s to index", path)
}
}
// TODO: If a chart exists, but has a different Digest, should we error?
}

@ -19,6 +19,7 @@ package repo
import (
"bytes"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
@ -105,16 +106,27 @@ func LoadIndexFile(path string) (*IndexFile, error) {
if err != nil {
return nil, err
}
return loadIndex(b)
i, err := loadIndex(b, path)
if err != nil {
return nil, errors.Wrapf(err, "error loading %s", path)
}
return i, nil
}
// Add adds a file to the index
// MustAdd adds a file to the index
// This can leave the index in an unsorted state
func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) {
func (i IndexFile) MustAdd(md *chart.Metadata, filename, baseURL, digest string) error {
if md.APIVersion == "" {
md.APIVersion = chart.APIVersionV1
}
if err := md.Validate(); err != nil {
return errors.Wrapf(err, "validate failed for %s", filename)
}
u := filename
if baseURL != "" {
var err error
_, file := filepath.Split(filename)
var err error
u, err = urlutil.URLJoin(baseURL, file)
if err != nil {
u = path.Join(baseURL, file)
@ -126,10 +138,17 @@ func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) {
Digest: digest,
Created: time.Now(),
}
if ee, ok := i.Entries[md.Name]; !ok {
i.Entries[md.Name] = ChartVersions{cr}
} else {
ee := i.Entries[md.Name]
i.Entries[md.Name] = append(ee, cr)
return nil
}
// Add adds a file to the index and logs an error.
//
// Deprecated: Use index.MustAdd instead.
func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) {
if err := i.MustAdd(md, filename, baseURL, digest); err != nil {
log.Printf("skipping loading invalid entry for chart %q %q from %s: %s", md.Name, md.Version, filename, err)
}
}
@ -294,19 +313,34 @@ func IndexDirectory(dir, baseURL string) (*IndexFile, error) {
if err != nil {
return index, err
}
index.Add(c.Metadata, fname, parentURL, hash)
if err := index.MustAdd(c.Metadata, fname, parentURL, hash); err != nil {
return index, errors.Wrapf(err, "failed adding to %s to index", fname)
}
}
return index, nil
}
// loadIndex loads an index file and does minimal validity checking.
//
// The source parameter is only used for logging.
// This will fail if API Version is not set (ErrNoAPIVersion) or if the unmarshal fails.
func loadIndex(data []byte) (*IndexFile, error) {
func loadIndex(data []byte, source string) (*IndexFile, error) {
i := &IndexFile{}
if err := yaml.UnmarshalStrict(data, i); err != nil {
return i, err
}
for name, cvs := range i.Entries {
for idx := len(cvs) - 1; idx >= 0; idx-- {
if cvs[idx].APIVersion == "" {
cvs[idx].APIVersion = chart.APIVersionV1
}
if err := cvs[idx].Validate(); err != nil {
log.Printf("skipping loading invalid entry for chart %q %q from %s: %s", name, cvs[idx].Version, source, err)
cvs = append(cvs[:idx], cvs[idx+1:]...)
}
}
}
i.SortEntries()
if i.APIVersion == "" {
return i, ErrNoAPIVersion

@ -27,11 +27,10 @@ import (
"strings"
"testing"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/helmpath"
"helm.sh/helm/v3/pkg/chart"
)
const (
@ -65,12 +64,23 @@ entries:
func TestIndexFile(t *testing.T) {
i := NewIndexFile()
i.Add(&chart.Metadata{Name: "clipper", Version: "0.1.0"}, "clipper-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890")
i.Add(&chart.Metadata{Name: "cutter", Version: "0.1.1"}, "cutter-0.1.1.tgz", "http://example.com/charts", "sha256:1234567890abc")
i.Add(&chart.Metadata{Name: "cutter", Version: "0.1.0"}, "cutter-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890abc")
i.Add(&chart.Metadata{Name: "cutter", Version: "0.2.0"}, "cutter-0.2.0.tgz", "http://example.com/charts", "sha256:1234567890abc")
i.Add(&chart.Metadata{Name: "setter", Version: "0.1.9+alpha"}, "setter-0.1.9+alpha.tgz", "http://example.com/charts", "sha256:1234567890abc")
i.Add(&chart.Metadata{Name: "setter", Version: "0.1.9+beta"}, "setter-0.1.9+beta.tgz", "http://example.com/charts", "sha256:1234567890abc")
for _, x := range []struct {
md *chart.Metadata
filename string
baseURL string
digest string
}{
{&chart.Metadata{APIVersion: "v2", Name: "clipper", Version: "0.1.0"}, "clipper-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890"},
{&chart.Metadata{APIVersion: "v2", Name: "cutter", Version: "0.1.1"}, "cutter-0.1.1.tgz", "http://example.com/charts", "sha256:1234567890abc"},
{&chart.Metadata{APIVersion: "v2", Name: "cutter", Version: "0.1.0"}, "cutter-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890abc"},
{&chart.Metadata{APIVersion: "v2", Name: "cutter", Version: "0.2.0"}, "cutter-0.2.0.tgz", "http://example.com/charts", "sha256:1234567890abc"},
{&chart.Metadata{APIVersion: "v2", Name: "setter", Version: "0.1.9+alpha"}, "setter-0.1.9+alpha.tgz", "http://example.com/charts", "sha256:1234567890abc"},
{&chart.Metadata{APIVersion: "v2", Name: "setter", Version: "0.1.9+beta"}, "setter-0.1.9+beta.tgz", "http://example.com/charts", "sha256:1234567890abc"},
} {
if err := i.MustAdd(x.md, x.filename, x.baseURL, x.digest); err != nil {
t.Errorf("unexpected error adding to index: %s", err)
}
}
i.SortEntries()
@ -126,11 +136,7 @@ func TestLoadIndex(t *testing.T) {
tc := tc
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
b, err := ioutil.ReadFile(tc.Filename)
if err != nil {
t.Fatal(err)
}
i, err := loadIndex(b)
i, err := LoadIndexFile(tc.Filename)
if err != nil {
t.Fatal(err)
}
@ -141,19 +147,11 @@ func TestLoadIndex(t *testing.T) {
// TestLoadIndex_Duplicates is a regression to make sure that we don't non-deterministically allow duplicate packages.
func TestLoadIndex_Duplicates(t *testing.T) {
if _, err := loadIndex([]byte(indexWithDuplicates)); err == nil {
if _, err := loadIndex([]byte(indexWithDuplicates), "indexWithDuplicates"); err == nil {
t.Errorf("Expected an error when duplicate entries are present")
}
}
func TestLoadIndexFile(t *testing.T) {
i, err := LoadIndexFile(testfile)
if err != nil {
t.Fatal(err)
}
verifyLocalIndex(t, i)
}
func TestLoadIndexFileAnnotations(t *testing.T) {
i, err := LoadIndexFile(annotationstestfile)
if err != nil {
@ -170,11 +168,7 @@ func TestLoadIndexFileAnnotations(t *testing.T) {
}
func TestLoadUnorderedIndex(t *testing.T) {
b, err := ioutil.ReadFile(unorderedTestfile)
if err != nil {
t.Fatal(err)
}
i, err := loadIndex(b)
i, err := LoadIndexFile(unorderedTestfile)
if err != nil {
t.Fatal(err)
}
@ -183,20 +177,26 @@ func TestLoadUnorderedIndex(t *testing.T) {
func TestMerge(t *testing.T) {
ind1 := NewIndexFile()
ind1.Add(&chart.Metadata{
Name: "dreadnought",
Version: "0.1.0",
}, "dreadnought-0.1.0.tgz", "http://example.com", "aaaa")
if err := ind1.MustAdd(&chart.Metadata{APIVersion: "v2", Name: "dreadnought", Version: "0.1.0"}, "dreadnought-0.1.0.tgz", "http://example.com", "aaaa"); err != nil {
t.Fatalf("unexpected error: %s", err)
}
ind2 := NewIndexFile()
ind2.Add(&chart.Metadata{
Name: "dreadnought",
Version: "0.2.0",
}, "dreadnought-0.2.0.tgz", "http://example.com", "aaaabbbb")
ind2.Add(&chart.Metadata{
Name: "doughnut",
Version: "0.2.0",
}, "doughnut-0.2.0.tgz", "http://example.com", "ccccbbbb")
for _, x := range []struct {
md *chart.Metadata
filename string
baseURL string
digest string
}{
{&chart.Metadata{APIVersion: "v2", Name: "dreadnought", Version: "0.2.0"}, "dreadnought-0.2.0.tgz", "http://example.com", "aaaabbbb"},
{&chart.Metadata{APIVersion: "v2", Name: "doughnut", Version: "0.2.0"}, "doughnut-0.2.0.tgz", "http://example.com", "ccccbbbb"},
} {
if err := ind2.MustAdd(x.md, x.filename, x.baseURL, x.digest); err != nil {
t.Errorf("unexpected error: %s", err)
}
}
ind1.Merge(ind2)
@ -239,12 +239,7 @@ func TestDownloadIndexFile(t *testing.T) {
t.Fatalf("error finding created index file: %#v", err)
}
b, err := ioutil.ReadFile(idx)
if err != nil {
t.Fatalf("error reading index file: %#v", err)
}
i, err := loadIndex(b)
i, err := LoadIndexFile(idx)
if err != nil {
t.Fatalf("Index %q failed to parse: %s", testfile, err)
}
@ -256,7 +251,7 @@ func TestDownloadIndexFile(t *testing.T) {
t.Fatalf("error finding created charts file: %#v", err)
}
b, err = ioutil.ReadFile(idx)
b, err := ioutil.ReadFile(idx)
if err != nil {
t.Fatalf("error reading charts file: %#v", err)
}
@ -297,12 +292,7 @@ func TestDownloadIndexFile(t *testing.T) {
t.Fatalf("error finding created index file: %#v", err)
}
b, err := ioutil.ReadFile(idx)
if err != nil {
t.Fatalf("error reading index file: %#v", err)
}
i, err := loadIndex(b)
i, err := LoadIndexFile(idx)
if err != nil {
t.Fatalf("Index %q failed to parse: %s", testfile, err)
}
@ -314,7 +304,7 @@ func TestDownloadIndexFile(t *testing.T) {
t.Fatalf("error finding created charts file: %#v", err)
}
b, err = ioutil.ReadFile(idx)
b, err := ioutil.ReadFile(idx)
if err != nil {
t.Fatalf("error reading charts file: %#v", err)
}
@ -345,6 +335,7 @@ func verifyLocalIndex(t *testing.T, i *IndexFile) {
expects := []*ChartVersion{
{
Metadata: &chart.Metadata{
APIVersion: "v2",
Name: "alpine",
Description: "string",
Version: "1.0.0",
@ -359,6 +350,7 @@ func verifyLocalIndex(t *testing.T, i *IndexFile) {
},
{
Metadata: &chart.Metadata{
APIVersion: "v2",
Name: "nginx",
Description: "string",
Version: "0.2.0",
@ -372,6 +364,7 @@ func verifyLocalIndex(t *testing.T, i *IndexFile) {
},
{
Metadata: &chart.Metadata{
APIVersion: "v2",
Name: "nginx",
Description: "string",
Version: "0.1.0",
@ -476,28 +469,44 @@ func TestIndexDirectory(t *testing.T) {
func TestIndexAdd(t *testing.T) {
i := NewIndexFile()
i.Add(&chart.Metadata{Name: "clipper", Version: "0.1.0"}, "clipper-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890")
for _, x := range []struct {
md *chart.Metadata
filename string
baseURL string
digest string
}{
{&chart.Metadata{APIVersion: "v2", Name: "clipper", Version: "0.1.0"}, "clipper-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890"},
{&chart.Metadata{APIVersion: "v2", Name: "alpine", Version: "0.1.0"}, "/home/charts/alpine-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890"},
{&chart.Metadata{APIVersion: "v2", Name: "deis", Version: "0.1.0"}, "/home/charts/deis-0.1.0.tgz", "http://example.com/charts/", "sha256:1234567890"},
} {
if err := i.MustAdd(x.md, x.filename, x.baseURL, x.digest); err != nil {
t.Errorf("unexpected error adding to index: %s", err)
}
}
if i.Entries["clipper"][0].URLs[0] != "http://example.com/charts/clipper-0.1.0.tgz" {
t.Errorf("Expected http://example.com/charts/clipper-0.1.0.tgz, got %s", i.Entries["clipper"][0].URLs[0])
}
i.Add(&chart.Metadata{Name: "alpine", Version: "0.1.0"}, "/home/charts/alpine-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890")
if i.Entries["alpine"][0].URLs[0] != "http://example.com/charts/alpine-0.1.0.tgz" {
t.Errorf("Expected http://example.com/charts/alpine-0.1.0.tgz, got %s", i.Entries["alpine"][0].URLs[0])
}
i.Add(&chart.Metadata{Name: "deis", Version: "0.1.0"}, "/home/charts/deis-0.1.0.tgz", "http://example.com/charts/", "sha256:1234567890")
if i.Entries["deis"][0].URLs[0] != "http://example.com/charts/deis-0.1.0.tgz" {
t.Errorf("Expected http://example.com/charts/deis-0.1.0.tgz, got %s", i.Entries["deis"][0].URLs[0])
}
// test error condition
if err := i.MustAdd(&chart.Metadata{}, "error-0.1.0.tgz", "", ""); err == nil {
t.Fatal("expected error adding to index")
}
}
func TestIndexWrite(t *testing.T) {
i := NewIndexFile()
i.Add(&chart.Metadata{Name: "clipper", Version: "0.1.0"}, "clipper-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890")
if err := i.MustAdd(&chart.Metadata{APIVersion: "v2", Name: "clipper", Version: "0.1.0"}, "clipper-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890"); err != nil {
t.Fatalf("unexpected error: %s", err)
}
dir, err := ioutil.TempDir("", "helm-tmp")
if err != nil {
t.Fatal(err)

@ -14,6 +14,7 @@ entries:
- popular
- web server
- proxy
apiVersion: v2
- urls:
- https://charts.helm.sh/stable/nginx-0.1.0.tgz
name: nginx
@ -25,6 +26,7 @@ entries:
- popular
- web server
- proxy
apiVersion: v2
alpine:
- urls:
- https://charts.helm.sh/stable/alpine-1.0.0.tgz
@ -39,6 +41,7 @@ entries:
- small
- sumtin
digest: "sha256:1234567890abcdef"
apiVersion: v2
chartWithNoURL:
- name: chartWithNoURL
description: string
@ -48,3 +51,4 @@ entries:
- small
- sumtin
digest: "sha256:1234567890abcdef"
apiVersion: v2

@ -12,6 +12,7 @@ entries:
- popular
- web server
- proxy
apiVersion: v2
- urls:
- https://charts.helm.sh/stable/nginx-0.1.0.tgz
name: nginx
@ -23,6 +24,7 @@ entries:
- popular
- web server
- proxy
apiVersion: v2
alpine:
- urls:
- https://charts.helm.sh/stable/alpine-1.0.0.tgz
@ -37,6 +39,7 @@ entries:
- small
- sumtin
digest: "sha256:1234567890abcdef"
apiVersion: v2
chartWithNoURL:
- name: chartWithNoURL
description: string
@ -46,5 +49,6 @@ entries:
- small
- sumtin
digest: "sha256:1234567890abcdef"
apiVersion: v2
annotations:
helm.sh/test: foo bar

@ -12,6 +12,7 @@ entries:
- popular
- web server
- proxy
apiVersion: v2
- urls:
- https://charts.helm.sh/stable/nginx-0.2.0.tgz
name: nginx
@ -23,6 +24,7 @@ entries:
- popular
- web server
- proxy
apiVersion: v2
alpine:
- urls:
- https://charts.helm.sh/stable/alpine-1.0.0.tgz
@ -37,6 +39,7 @@ entries:
- small
- sumtin
digest: "sha256:1234567890abcdef"
apiVersion: v2
chartWithNoURL:
- name: chartWithNoURL
description: string
@ -46,3 +49,4 @@ entries:
- small
- sumtin
digest: "sha256:1234567890abcdef"
apiVersion: v2

@ -12,6 +12,7 @@ entries:
- popular
- web server
- proxy
apiVersion: v2
- urls:
- https://charts.helm.sh/stable/nginx-0.1.0.tgz
name: nginx
@ -23,6 +24,7 @@ entries:
- popular
- web server
- proxy
apiVersion: v2
alpine:
- urls:
- https://charts.helm.sh/stable/alpine-1.0.0.tgz
@ -37,6 +39,7 @@ entries:
- small
- sumtin
digest: "sha256:1234567890abcdef"
apiVersion: v2
chartWithNoURL:
- name: chartWithNoURL
description: string
@ -46,3 +49,4 @@ entries:
- small
- sumtin
digest: "sha256:1234567890abcdef"
apiVersion: v2

@ -61,7 +61,10 @@ func (s *Storage) Create(rls *rspb.Release) error {
s.Log("creating release %q", makeKey(rls.Name, rls.Version))
if s.MaxHistory > 0 {
// Want to make space for one more release.
s.removeLeastRecent(rls.Name, s.MaxHistory-1)
if err := s.removeLeastRecent(rls.Name, s.MaxHistory-1); err != nil &&
!errors.Is(err, driver.ErrReleaseNotFound) {
return err
}
}
return s.Driver.Create(makeKey(rls.Name, rls.Version), rls)
}

@ -21,6 +21,8 @@ import (
"reflect"
"testing"
"github.com/pkg/errors"
rspb "helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/storage/driver"
)
@ -276,6 +278,32 @@ func TestStorageHistory(t *testing.T) {
}
}
func TestStorageRemoveLeastRecentWithError(t *testing.T) {
storage := Init(driver.NewMemory())
storage.Log = t.Logf
storage.MaxHistory = 1
const name = "angry-bird"
// setup storage with test releases
setup := func() {
// release records
rls1 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusSuperseded}.ToRelease()
// create the release records in the storage
assertErrNil(t.Fatal, storage.Driver.Create(makeKey(rls1.Name, rls1.Version), rls1), "Storing release 'angry-bird' (v1)")
}
setup()
rls2 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusSuperseded}.ToRelease()
wantErr := driver.ErrNoDeployedReleases
gotErr := storage.Create(rls2)
if !errors.Is(gotErr, wantErr) {
t.Fatalf("Storing release 'angry-bird' (v2) should return the error %#v, but returned %#v", wantErr, gotErr)
}
}
func TestStorageRemoveLeastRecent(t *testing.T) {
storage := Init(driver.NewMemory())
storage.Log = t.Logf

@ -50,13 +50,11 @@ initOS() {
# runs the given command as root (detects if we are root already)
runAsRoot() {
local CMD="$*"
if [ $EUID -ne 0 -a $USE_SUDO = "true" ]; then
CMD="sudo $CMD"
if [ $EUID -ne 0 -a "$USE_SUDO" = "true" ]; then
sudo "${@}"
else
"${@}"
fi
$CMD
}
# verifySupported checks that the os/arch combination is supported for

@ -23,6 +23,7 @@
: ${VERIFY_CHECKSUM:="true"}
: ${VERIFY_SIGNATURES:="false"}
: ${HELM_INSTALL_DIR:="/usr/local/bin"}
: ${GPG_PUBRING:="pubring.kbx"}
HAS_CURL="$(type "curl" &> /dev/null && echo true || echo false)"
HAS_WGET="$(type "wget" &> /dev/null && echo true || echo false)"
@ -56,13 +57,11 @@ initOS() {
# runs the given command as root (detects if we are root already)
runAsRoot() {
local CMD="$*"
if [ $EUID -ne 0 -a $USE_SUDO = "true" ]; then
CMD="sudo $CMD"
if [ $EUID -ne 0 -a "$USE_SUDO" = "true" ]; then
sudo "${@}"
else
"${@}"
fi
$CMD
}
# verifySupported checks that the os/arch combination is supported for
@ -206,7 +205,7 @@ verifySignatures() {
gpg_stderr_device="/dev/stderr"
fi
gpg --batch --quiet --homedir="${gpg_homedir}" --import "${HELM_TMP_ROOT}/${keys_filename}" 2> "${gpg_stderr_device}"
gpg --batch --no-default-keyring --keyring "${gpg_homedir}/pubring.kbx" --export > "${gpg_keyring}"
gpg --batch --no-default-keyring --keyring "${gpg_homedir}/${GPG_PUBRING}" --export > "${gpg_keyring}"
local github_release_url="https://github.com/helm/helm/releases/download/${TAG}"
if [ "${HAS_CURL}" == "true" ]; then
curl -SsL "${github_release_url}/helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256.asc" -o "${HELM_TMP_ROOT}/helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256.asc"

@ -70,7 +70,7 @@ The community keeps growing, and we'd love to see you there!
- `#helm-users` for questions and just to hang out
- `#helm-dev` for discussing PRs, code, and bugs
- Hang out at the Public Developer Call: Thursday, 9:30 Pacific via [Zoom](https://zoom.us/j/696660622)
- Test, debug, and contribute charts: [GitHub/helm/charts](https://github.com/helm/charts)
- Test, debug, and contribute charts: [ArtifactHub/packages](https://artifacthub.io/packages/search?kind=0)
## Notable Changes
@ -87,7 +87,7 @@ Download Helm ${RELEASE}. The common platform binaries are here:
- [Linux arm64](https://get.helm.sh/helm-${RELEASE}-linux-arm64.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-arm64.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-arm64.tar.gz.sha256))
- [Linux i386](https://get.helm.sh/helm-${RELEASE}-linux-386.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-386.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-386.tar.gz.sha256))
- [Linux ppc64le](https://get.helm.sh/helm-${RELEASE}-linux-ppc64le.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-ppc64le.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-ppc64le.tar.gz.sha256))
- [Linux s390x](https://get.helm.sh/helm-${RELEASE}-linux-s390x.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-s390x.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-darwin-amd64.tar.gz.sha256))
- [Linux s390x](https://get.helm.sh/helm-${RELEASE}-linux-s390x.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-s390x.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-s390x.tar.gz.sha256))
- [Windows amd64](https://get.helm.sh/helm-${RELEASE}-windows-amd64.zip) ([checksum](https://get.helm.sh/helm-${RELEASE}-windows-amd64.zip.sha256sum) / $(cat _dist/helm-${RELEASE}-windows-amd64.zip.sha256))
The [Quickstart Guide](https://helm.sh/docs/intro/quickstart/) will get you going from there. For **upgrade instructions** or detailed installation notes, check the [install guide](https://helm.sh/docs/intro/install/). You can also use a [script to install](https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3) on any system with \`bash\`.

Loading…
Cancel
Save