Merge branch 'main' into feat/subcharts-scope

pull/9957/head
Matt Farina 4 years ago committed by GitHub
commit ac80a5eec9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -72,6 +72,16 @@ To load completions for every new session, execute once:
You will need to start a new shell for this setup to take effect.
`
const powershellCompDesc = `
Generate the autocompletion script for powershell.
To load completions in your current shell session:
PS C:\> helm completion powershell | Out-String | Invoke-Expression
To load completions for every new session, add the output of the above command
to your powershell profile.
`
const (
noDescFlagName = "no-descriptions"
noDescFlagText = "disable completion descriptions"
@ -88,16 +98,16 @@ func newCompletionCmd(out io.Writer) *cobra.Command {
}
bash := &cobra.Command{
Use: "bash",
Short: "generate autocompletion script for bash",
Long: bashCompDesc,
Args: require.NoArgs,
DisableFlagsInUseLine: true,
ValidArgsFunction: noCompletions,
Use: "bash",
Short: "generate autocompletion script for bash",
Long: bashCompDesc,
Args: require.NoArgs,
ValidArgsFunction: noCompletions,
RunE: func(cmd *cobra.Command, args []string) error {
return runCompletionBash(out, cmd)
},
}
bash.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText)
zsh := &cobra.Command{
Use: "zsh",
@ -123,13 +133,25 @@ func newCompletionCmd(out io.Writer) *cobra.Command {
}
fish.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText)
cmd.AddCommand(bash, zsh, fish)
powershell := &cobra.Command{
Use: "powershell",
Short: "generate autocompletion script for powershell",
Long: powershellCompDesc,
Args: require.NoArgs,
ValidArgsFunction: noCompletions,
RunE: func(cmd *cobra.Command, args []string) error {
return runCompletionPowershell(out, cmd)
},
}
powershell.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText)
cmd.AddCommand(bash, zsh, fish, powershell)
return cmd
}
func runCompletionBash(out io.Writer, cmd *cobra.Command) error {
err := cmd.Root().GenBashCompletion(out)
err := cmd.Root().GenBashCompletionV2(out, !disableCompDescriptions)
// In case the user renamed the helm binary (e.g., to be able to run
// both helm2 and helm3), we hook the new binary name to the completion function
@ -180,6 +202,13 @@ func runCompletionFish(out io.Writer, cmd *cobra.Command) error {
return cmd.Root().GenFishCompletion(out, !disableCompDescriptions)
}
func runCompletionPowershell(out io.Writer, cmd *cobra.Command) error {
if disableCompDescriptions {
return cmd.Root().GenPowerShellCompletion(out)
}
return cmd.Root().GenPowerShellCompletionWithDesc(out)
}
// Function to disable file completion
func noCompletions(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveNoFileComp

@ -277,11 +277,6 @@ func TestPluginDynamicCompletion(t *testing.T) {
cmd: "__complete echo -n mynamespace ''",
golden: "output/plugin_echo_no_directive.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin bad directive",
cmd: "__complete echo ''",
golden: "output/plugin_echo_bad_directive.txt",
rels: []*release.Release{},
}}
for _, test := range tests {
settings.PluginsDirectory = "testdata/helmhome/helm/plugins"

@ -67,7 +67,7 @@ func newRegistryLoginCmd(cfg *action.Configuration, out io.Writer) *cobra.Comman
return cmd
}
// Adapted from https://github.com/deislabs/oras
// Adapted from https://oras.land/oras-go
func getUsernamePassword(usernameOpt string, passwordOpt string, passwordFromStdinOpt bool) (string, string, error) {
var err error
username := usernameOpt
@ -110,7 +110,7 @@ func getUsernamePassword(usernameOpt string, passwordOpt string, passwordFromStd
return username, password, nil
}
// Copied/adapted from https://github.com/deislabs/oras
// Copied/adapted from https://oras.land/oras-go
func readLine(prompt string, silent bool) (string, error) {
fmt.Print(prompt)
if silent {

@ -48,6 +48,7 @@ type repoAddOptions struct {
url string
username string
password string
passwordFromStdinOpt bool
passCredentialsAll bool
forceUpdate bool
allowDeprecatedRepos bool
@ -85,6 +86,7 @@ func newRepoAddCmd(out io.Writer) *cobra.Command {
f := cmd.Flags()
f.StringVar(&o.username, "username", "", "chart repository username")
f.StringVar(&o.password, "password", "", "chart repository password")
f.BoolVarP(&o.passwordFromStdinOpt, "password-stdin", "", false, "read chart repository password from stdin")
f.BoolVar(&o.forceUpdate, "force-update", false, "replace (overwrite) the repo if it already exists")
f.BoolVar(&o.deprecatedNoUpdate, "no-update", false, "Ignored. Formerly, it would disabled forced updates. It is deprecated by force-update.")
f.StringVar(&o.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file")
@ -143,14 +145,24 @@ func (o *repoAddOptions) run(out io.Writer) error {
}
if o.username != "" && o.password == "" {
fd := int(os.Stdin.Fd())
fmt.Fprint(out, "Password: ")
password, err := term.ReadPassword(fd)
fmt.Fprintln(out)
if err != nil {
return err
if o.passwordFromStdinOpt {
passwordFromStdin, err := io.ReadAll(os.Stdin)
if err != nil {
return err
}
password := strings.TrimSuffix(string(passwordFromStdin), "\n")
password = strings.TrimSuffix(password, "\r")
o.password = password
} else {
fd := int(os.Stdin.Fd())
fmt.Fprint(out, "Password: ")
password, err := term.ReadPassword(fd)
fmt.Fprintln(out)
if err != nil {
return err
}
o.password = string(password)
}
o.password = string(password)
}
c := repo.Entry{

@ -21,6 +21,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"sync"
"testing"
@ -204,3 +205,33 @@ func TestRepoAddFileCompletion(t *testing.T) {
checkFileCompletion(t, "repo add reponame", false)
checkFileCompletion(t, "repo add reponame https://example.com", false)
}
func TestRepoAddWithPasswordFromStdin(t *testing.T) {
srv := repotest.NewTempServerWithCleanupAndBasicAuth(t, "testdata/testserver/*.*")
defer srv.Stop()
defer resetEnv()()
in, err := os.Open("testdata/password")
if err != nil {
t.Errorf("unexpected error, got '%v'", err)
}
tmpdir := ensure.TempDir(t)
repoFile := filepath.Join(tmpdir, "repositories.yaml")
store := storageFixture()
const testName = "test-name"
const username = "username"
cmd := fmt.Sprintf("repo add %s %s --repository-config %s --repository-cache %s --username %s --password-stdin", testName, srv.URL(), repoFile, tmpdir, username)
var result string
_, result, err = executeActionCommandStdinC(store, in, cmd)
if err != nil {
t.Errorf("unexpected error, got '%v'", err)
}
if !strings.Contains(result, fmt.Sprintf("\"%s\" has been added to your repositories", testName)) {
t.Errorf("Repo was not successfully added. Output: %s", result)
}
}

@ -17,6 +17,7 @@ limitations under the License.
package main
import (
"fmt"
"io"
"strings"
@ -131,7 +132,7 @@ func compListRepos(prefix string, ignoredRepoNames []string) []string {
filteredRepos := filterRepos(f.Repositories, ignoredRepoNames)
for _, repo := range filteredRepos {
if strings.HasPrefix(repo.Name, prefix) {
rNames = append(rNames, repo.Name)
rNames = append(rNames, fmt.Sprintf("%s\t%s", repo.Name, repo.URL))
}
}
}

@ -18,6 +18,7 @@ package main
import (
"bytes"
"fmt"
"os"
"path/filepath"
"strings"
@ -161,6 +162,51 @@ func testCacheFiles(t *testing.T, cacheIndexFile string, cacheChartsFile string,
}
}
func TestRepoRemoveCompletion(t *testing.T) {
ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*")
if err != nil {
t.Fatal(err)
}
defer ts.Stop()
rootDir := ensure.TempDir(t)
repoFile := filepath.Join(rootDir, "repositories.yaml")
repoCache := filepath.Join(rootDir, "cache/")
var testRepoNames = []string{"foo", "bar", "baz"}
// Add test repos
for _, repoName := range testRepoNames {
o := &repoAddOptions{
name: repoName,
url: ts.URL(),
repoFile: repoFile,
}
if err := o.run(os.Stderr); err != nil {
t.Error(err)
}
}
repoSetup := fmt.Sprintf("--repository-config %s --repository-cache %s", repoFile, repoCache)
// In the following tests, we turn off descriptions for completions by using __completeNoDesc.
// We have to do this because the description will contain the port used by the webserver,
// and that port changes each time we run the test.
tests := []cmdTestCase{{
name: "completion for repo remove",
cmd: fmt.Sprintf("%s __completeNoDesc repo remove ''", repoSetup),
golden: "output/repo_list_comp.txt",
}, {
name: "completion for repo remove repetition",
cmd: fmt.Sprintf("%s __completeNoDesc repo remove foo ''", repoSetup),
golden: "output/repo_repeat_comp.txt",
}}
for _, test := range tests {
runTestCmd(t, []cmdTestCase{test})
}
}
func TestRepoRemoveFileCompletion(t *testing.T) {
checkFileCompletion(t, "repo remove", false)
checkFileCompletion(t, "repo remove repo1", false)

@ -41,10 +41,11 @@ To update all the repositories, use 'helm repo update'.
var errNoRepositories = errors.New("no repositories found. You must add one before updating")
type repoUpdateOptions struct {
update func([]*repo.ChartRepository, io.Writer)
repoFile string
repoCache string
names []string
update func([]*repo.ChartRepository, io.Writer, bool) error
repoFile string
repoCache string
names []string
failOnRepoUpdateFail bool
}
func newRepoUpdateCmd(out io.Writer) *cobra.Command {
@ -66,6 +67,13 @@ func newRepoUpdateCmd(out io.Writer) *cobra.Command {
return o.run(out)
},
}
f := cmd.Flags()
// Adding this flag for Helm 3 as stop gap functionality for https://github.com/helm/helm/issues/10016.
// This should be deprecated in Helm 4 by update to the behaviour of `helm repo update` command.
f.BoolVar(&o.failOnRepoUpdateFail, "fail-on-repo-update-fail", false, "update fails if any of the repository updates fail")
return cmd
}
@ -103,26 +111,34 @@ func (o *repoUpdateOptions) run(out io.Writer) error {
}
}
o.update(repos, out)
return nil
return o.update(repos, out, o.failOnRepoUpdateFail)
}
func updateCharts(repos []*repo.ChartRepository, out io.Writer) {
func updateCharts(repos []*repo.ChartRepository, out io.Writer, failOnRepoUpdateFail bool) error {
fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...")
var wg sync.WaitGroup
var repoFailList []string
for _, re := range repos {
wg.Add(1)
go func(re *repo.ChartRepository) {
defer wg.Done()
if _, err := re.DownloadIndexFile(); err != nil {
fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", re.Config.Name, re.Config.URL, err)
repoFailList = append(repoFailList, re.Config.URL)
} else {
fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", re.Config.Name)
}
}(re)
}
wg.Wait()
if len(repoFailList) > 0 && failOnRepoUpdateFail {
return errors.New(fmt.Sprintf("Failed to update the following repositories: %s",
repoFailList))
}
fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈")
return nil
}
func checkRequestedRepos(requestedRepos []string, validRepos []*repo.Entry) error {

@ -35,10 +35,11 @@ func TestUpdateCmd(t *testing.T) {
var out bytes.Buffer
// Instead of using the HTTP updater, we provide our own for this test.
// The TestUpdateCharts test verifies the HTTP behavior independently.
updater := func(repos []*repo.ChartRepository, out io.Writer) {
updater := func(repos []*repo.ChartRepository, out io.Writer, failOnRepoUpdateFail bool) error {
for _, re := range repos {
fmt.Fprintln(out, re.Config.Name)
}
return nil
}
o := &repoUpdateOptions{
update: updater,
@ -59,10 +60,11 @@ func TestUpdateCmdMultiple(t *testing.T) {
var out bytes.Buffer
// Instead of using the HTTP updater, we provide our own for this test.
// The TestUpdateCharts test verifies the HTTP behavior independently.
updater := func(repos []*repo.ChartRepository, out io.Writer) {
updater := func(repos []*repo.ChartRepository, out io.Writer, failOnRepoUpdateFail bool) error {
for _, re := range repos {
fmt.Fprintln(out, re.Config.Name)
}
return nil
}
o := &repoUpdateOptions{
update: updater,
@ -84,10 +86,11 @@ func TestUpdateCmdInvalid(t *testing.T) {
var out bytes.Buffer
// Instead of using the HTTP updater, we provide our own for this test.
// The TestUpdateCharts test verifies the HTTP behavior independently.
updater := func(repos []*repo.ChartRepository, out io.Writer) {
updater := func(repos []*repo.ChartRepository, out io.Writer, failOnRepoUpdateFail bool) error {
for _, re := range repos {
fmt.Fprintln(out, re.Config.Name)
}
return nil
}
o := &repoUpdateOptions{
update: updater,
@ -144,7 +147,7 @@ func TestUpdateCharts(t *testing.T) {
}
b := bytes.NewBuffer(nil)
updateCharts([]*repo.ChartRepository{r}, b)
updateCharts([]*repo.ChartRepository{r}, b, false)
got := b.String()
if strings.Contains(got, "Unable to get an update") {
@ -159,3 +162,79 @@ func TestRepoUpdateFileCompletion(t *testing.T) {
checkFileCompletion(t, "repo update", false)
checkFileCompletion(t, "repo update repo1", false)
}
func TestUpdateChartsFail(t *testing.T) {
defer resetEnv()()
defer ensure.HelmHome(t)()
ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*")
if err != nil {
t.Fatal(err)
}
defer ts.Stop()
var invalidURL = ts.URL() + "55"
r, err := repo.NewChartRepository(&repo.Entry{
Name: "charts",
URL: invalidURL,
}, getter.All(settings))
if err != nil {
t.Error(err)
}
b := bytes.NewBuffer(nil)
if err := updateCharts([]*repo.ChartRepository{r}, b, false); err != nil {
t.Error("Repo update should not return error if update of repository fails")
}
got := b.String()
if !strings.Contains(got, "Unable to get an update") {
t.Errorf("Repo should have failed update but instead got: %q", got)
}
if !strings.Contains(got, "Update Complete.") {
t.Error("Update was not successful")
}
}
func TestUpdateChartsFailWithError(t *testing.T) {
defer resetEnv()()
defer ensure.HelmHome(t)()
ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*")
if err != nil {
t.Fatal(err)
}
defer ts.Stop()
var invalidURL = ts.URL() + "55"
r, err := repo.NewChartRepository(&repo.Entry{
Name: "charts",
URL: invalidURL,
}, getter.All(settings))
if err != nil {
t.Error(err)
}
b := bytes.NewBuffer(nil)
err = updateCharts([]*repo.ChartRepository{r}, b, true)
if err == nil {
t.Error("Repo update should return error because update of repository fails and 'fail-on-repo-update-fail' flag set")
return
}
var expectedErr = "Failed to update the following repositories"
var receivedErr = err.Error()
if !strings.Contains(receivedErr, expectedErr) {
t.Errorf("Expected error (%s) but got (%s) instead", expectedErr, receivedErr)
}
if !strings.Contains(receivedErr, invalidURL) {
t.Errorf("Expected invalid URL (%s) in error message but got (%s) instead", invalidURL, receivedErr)
}
got := b.String()
if !strings.Contains(got, "Unable to get an update") {
t.Errorf("Repo should have failed update but instead got: %q", got)
}
if strings.Contains(got, "Update Complete.") {
t.Error("Update was not successful and should return error message because 'fail-on-repo-update-fail' flag set")
}
}

@ -302,7 +302,14 @@ func compListCharts(toComplete string, includeFiles bool) ([]string, cobra.Shell
// First check completions for repos
repos := compListRepos("", nil)
for _, repo := range repos {
for _, repoInfo := range repos {
// Split name from description
repoInfo := strings.Split(repoInfo, "\t")
repo := repoInfo[0]
repoDesc := ""
if len(repoInfo) > 1 {
repoDesc = repoInfo[1]
}
repoWithSlash := fmt.Sprintf("%s/", repo)
if strings.HasPrefix(toComplete, repoWithSlash) {
// Must complete with charts within the specified repo
@ -310,15 +317,15 @@ func compListCharts(toComplete string, includeFiles bool) ([]string, cobra.Shell
noSpace = false
break
} else if strings.HasPrefix(repo, toComplete) {
// Must complete the repo name
completions = append(completions, repoWithSlash)
// Must complete the repo name with the slash, followed by the description
completions = append(completions, fmt.Sprintf("%s\t%s", repoWithSlash, repoDesc))
noSpace = true
}
}
cobra.CompDebugln(fmt.Sprintf("Completions after repos: %v", completions), settings.Debug)
// Now handle completions for url prefixes
for _, url := range []string{"https://", "http://", "file://"} {
for _, url := range []string{"https://\tChart URL prefix", "http://\tChart URL prefix", "file://\tChart local URL prefix"} {
if strings.HasPrefix(toComplete, url) {
// The user already put in the full url prefix; we don't have
// anything to add, but make sure the shell does not default
@ -355,7 +362,7 @@ func compListCharts(toComplete string, includeFiles bool) ([]string, cobra.Shell
// If the user didn't provide any input to completion,
// we provide a hint that a path can also be used
if includeFiles && len(toComplete) == 0 {
completions = append(completions, "./", "/")
completions = append(completions, "./\tRelative path prefix to local chart", "/\tAbsolute path prefix to local chart")
}
cobra.CompDebugln(fmt.Sprintf("Completions after checking empty input: %v", completions), settings.Debug)

@ -51,6 +51,11 @@ This command inspects a chart (directory, file, or URL) and displays the content
of the README file
`
const showCRDsDesc = `
This command inspects a chart (directory, file, or URL) and displays the contents
of the CustomResourceDefintion files
`
func newShowCmd(out io.Writer) *cobra.Command {
client := action.NewShow(action.ShowAll)
@ -139,7 +144,24 @@ func newShowCmd(out io.Writer) *cobra.Command {
},
}
cmds := []*cobra.Command{all, readmeSubCmd, valuesSubCmd, chartSubCmd}
crdsSubCmd := &cobra.Command{
Use: "crds [CHART]",
Short: "show the chart's CRDs",
Long: showCRDsDesc,
Args: require.ExactArgs(1),
ValidArgsFunction: validArgsFunc,
RunE: func(cmd *cobra.Command, args []string) error {
client.OutputFormat = action.ShowCRDs
output, err := runShow(args, client)
if err != nil {
return err
}
fmt.Fprint(out, output)
return nil
},
}
cmds := []*cobra.Command{all, readmeSubCmd, valuesSubCmd, chartSubCmd, crdsSubCmd}
for _, subCmd := range cmds {
addShowFlags(subCmd, client)
showCommand.AddCommand(subCmd)

@ -145,3 +145,7 @@ func TestShowReadmeFileCompletion(t *testing.T) {
func TestShowValuesFileCompletion(t *testing.T) {
checkFileCompletion(t, "show values", true)
}
func TestShowCRDsFileCompletion(t *testing.T) {
checkFileCompletion(t, "show crds", true)
}

@ -7,8 +7,7 @@ echo "Args received: ${@}"
# Final printout is the optional completion directive of the form :<directive>
if [ "$HELM_NAMESPACE" = "default" ]; then
# Output an invalid directive, which should be ignored
echo ":2222"
echo ":0"
# else
# Don't include the directive, to test it is really optional
fi

@ -1,6 +0,0 @@
echo plugin.complete was called
Namespace: default
Num args received: 1
Args received:
:0
Completion ended with directive: ShellCompDirectiveDefault

@ -0,0 +1,5 @@
foo
bar
baz
:4
Completion ended with directive: ShellCompDirectiveNoFileComp

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

@ -0,0 +1 @@
password

@ -10,10 +10,9 @@ require (
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.4
github.com/containerd/containerd v1.5.4
github.com/cyphar/filepath-securejoin v0.2.2
github.com/deislabs/oras v0.11.1
github.com/docker/distribution v2.7.1+incompatible
github.com/distribution/distribution/v3 v3.0.0-20210804104954-38ab4c606ee3
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible
github.com/docker/go-units v0.4.0
github.com/evanphx/json-patch v4.9.0+incompatible
@ -28,22 +27,24 @@ require (
github.com/opencontainers/image-spec v1.0.1
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
github.com/pkg/errors v0.9.1
github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351
github.com/rubenv/sql-migrate v0.0.0-20210614095031-55d5740dbbcc
github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.1.3
github.com/spf13/cobra v1.2.1
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.0
github.com/xeipuuv/gojsonschema v1.2.0
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
github.com/ziutek/mymysql v1.5.4 // indirect
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d
k8s.io/api v0.21.0
k8s.io/apiextensions-apiserver v0.21.0
k8s.io/apimachinery v0.21.0
k8s.io/apiserver v0.21.0
k8s.io/cli-runtime v0.21.0
k8s.io/client-go v0.21.0
k8s.io/api v0.21.3
k8s.io/apiextensions-apiserver v0.21.3
k8s.io/apimachinery v0.21.3
k8s.io/apiserver v0.21.3
k8s.io/cli-runtime v0.21.3
k8s.io/client-go v0.21.3
k8s.io/klog/v2 v2.8.0
k8s.io/kubectl v0.21.0
k8s.io/kubectl v0.21.3
oras.land/oras-go v0.4.0
sigs.k8s.io/yaml v1.2.0
)

770
go.sum

File diff suppressed because it is too large Load Diff

@ -17,7 +17,7 @@ limitations under the License.
package registry // import "helm.sh/helm/v3/internal/experimental/registry"
import (
"github.com/deislabs/oras/pkg/auth"
"oras.land/oras-go/pkg/auth"
)
type (

@ -28,11 +28,11 @@ import (
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/errdefs"
orascontent "github.com/deislabs/oras/pkg/content"
digest "github.com/opencontainers/go-digest"
specs "github.com/opencontainers/image-spec/specs-go"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
orascontent "oras.land/oras-go/pkg/content"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"

@ -25,12 +25,12 @@ import (
"net/http"
"sort"
auth "github.com/deislabs/oras/pkg/auth/docker"
"github.com/deislabs/oras/pkg/content"
"github.com/deislabs/oras/pkg/oras"
"github.com/gosuri/uitable"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
auth "oras.land/oras-go/pkg/auth/docker"
"oras.land/oras-go/pkg/content"
"oras.land/oras-go/pkg/oras"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/helmpath"

@ -32,14 +32,14 @@ import (
"time"
"github.com/containerd/containerd/errdefs"
auth "github.com/deislabs/oras/pkg/auth/docker"
"github.com/docker/distribution/configuration"
"github.com/docker/distribution/registry"
_ "github.com/docker/distribution/registry/auth/htpasswd"
_ "github.com/docker/distribution/registry/storage/driver/inmemory"
"github.com/distribution/distribution/v3/configuration"
"github.com/distribution/distribution/v3/registry"
_ "github.com/distribution/distribution/v3/registry/auth/htpasswd"
_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
"github.com/phayes/freeport"
"github.com/stretchr/testify/suite"
"golang.org/x/crypto/bcrypt"
auth "oras.land/oras-go/pkg/auth/docker"
"helm.sh/helm/v3/pkg/chart"
)

@ -22,9 +22,9 @@ import (
"io"
"time"
orascontext "github.com/deislabs/oras/pkg/context"
units "github.com/docker/go-units"
"github.com/sirupsen/logrus"
orascontext "oras.land/oras-go/pkg/context"
)
// byteCountBinary produces a human-readable file size

@ -16,6 +16,7 @@ limitations under the License.
package resolver
import (
"runtime"
"testing"
"helm.sh/helm/v3/pkg/chart"
@ -246,24 +247,28 @@ func TestGetLocalPath(t *testing.T) {
repo string
chartpath string
expect string
winExpect string
err bool
}{
{
name: "absolute path",
repo: "file:////tmp",
expect: "/tmp",
name: "absolute path",
repo: "file:////",
expect: "/",
winExpect: "\\",
},
{
name: "relative path",
repo: "file://../../testdata/chartpath/base",
chartpath: "foo/bar",
expect: "testdata/chartpath/base",
winExpect: "testdata\\chartpath\\base",
},
{
name: "current directory path",
repo: "../charts/localdependency",
chartpath: "testdata/chartpath/charts",
expect: "testdata/chartpath/charts/localdependency",
winExpect: "testdata\\chartpath\\charts\\localdependency",
},
{
name: "invalid local path",
@ -291,8 +296,12 @@ func TestGetLocalPath(t *testing.T) {
if tt.err {
t.Fatalf("Expected error in test %q", tt.name)
}
if p != tt.expect {
t.Errorf("%q: expected %q, got %q", tt.name, tt.expect, p)
expect := tt.expect
if runtime.GOOS == "windows" {
expect = tt.winExpect
}
if p != expect {
t.Errorf("%q: expected %q, got %q", tt.name, expect, p)
}
})
}

@ -24,8 +24,8 @@ import (
"path/filepath"
"testing"
dockerauth "github.com/deislabs/oras/pkg/auth/docker"
fakeclientset "k8s.io/client-go/kubernetes/fake"
dockerauth "oras.land/oras-go/pkg/auth/docker"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/chart"

@ -17,6 +17,7 @@ limitations under the License.
package action
import (
"bytes"
"fmt"
"strings"
@ -41,6 +42,8 @@ const (
ShowValues ShowOutputFormat = "values"
// ShowReadme is the format which only shows the chart's README
ShowReadme ShowOutputFormat = "readme"
// ShowCRDs is the format which only shows the chart's CRDs
ShowCRDs ShowOutputFormat = "crds"
)
var readmeFileNames = []string{"readme.md", "readme.txt", "readme"}
@ -115,6 +118,18 @@ func (s *Show) Run(chartpath string) (string, error) {
}
fmt.Fprintf(&out, "%s\n", readme.Data)
}
if s.OutputFormat == ShowCRDs || s.OutputFormat == ShowAll {
crds := s.chart.CRDObjects()
if len(crds) > 0 {
if s.OutputFormat == ShowAll && !bytes.HasPrefix(crds[0].File.Data, []byte("---")) {
fmt.Fprintln(&out, "---")
}
for _, crd := range crds {
fmt.Fprintf(&out, "%s\n", string(crd.File.Data))
}
}
}
return out.String(), nil
}

@ -28,6 +28,9 @@ func TestShow(t *testing.T) {
Metadata: &chart.Metadata{Name: "alpine"},
Files: []*chart.File{
{Name: "README.md", Data: []byte("README\n")},
{Name: "crds/ignoreme.txt", Data: []byte("error")},
{Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")},
{Name: "crds/bar.json", Data: []byte("---\nbar\n")},
},
Raw: []*chart.File{
{Name: "values.yaml", Data: []byte("VALUES\n")},
@ -48,6 +51,12 @@ VALUES
---
README
---
foo
---
bar
`
if output != expect {
t.Errorf("Expected\n%q\nGot\n%q\n", expect, output)
@ -83,3 +92,31 @@ func TestShowValuesByJsonPathFormat(t *testing.T) {
t.Errorf("Expected\n%q\nGot\n%q\n", expect, output)
}
}
func TestShowCRDs(t *testing.T) {
client := NewShow(ShowCRDs)
client.chart = &chart.Chart{
Metadata: &chart.Metadata{Name: "alpine"},
Files: []*chart.File{
{Name: "crds/ignoreme.txt", Data: []byte("error")},
{Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")},
{Name: "crds/bar.json", Data: []byte("---\nbar\n")},
},
}
output, err := client.Run("")
if err != nil {
t.Fatal(err)
}
expect := `---
foo
---
bar
`
if output != expect {
t.Errorf("Expected\n%q\nGot\n%q\n", expect, output)
}
}

@ -16,7 +16,6 @@ limitations under the License.
package downloader
import (
"net/http"
"os"
"path/filepath"
"testing"
@ -171,19 +170,7 @@ func TestIsTar(t *testing.T) {
}
func TestDownloadTo(t *testing.T) {
// Set up a fake repo with basic auth enabled
srv, err := repotest.NewTempServerWithCleanup(t, "testdata/*.tgz*")
srv.Stop()
if err != nil {
t.Fatal(err)
}
srv.WithMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
username, password, ok := r.BasicAuth()
if !ok || username != "username" || password != "password" {
t.Errorf("Expected request to use basic auth and for username == 'username' and password == 'password', got '%v', '%s', '%s'", ok, username, password)
}
}))
srv.Start()
srv := repotest.NewTempServerWithCleanupAndBasicAuth(t, "testdata/*.tgz*")
defer srv.Stop()
if err := srv.CreateIndex(); err != nil {
t.Fatal(err)

@ -249,22 +249,24 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
destPath := filepath.Join(m.ChartPath, "charts")
tmpPath := filepath.Join(m.ChartPath, "tmpcharts")
// Create 'charts' directory if it doesn't already exist.
if fi, err := os.Stat(destPath); err != nil {
// Check if 'charts' directory is not actally a directory. If it does not exist, create it.
if fi, err := os.Stat(destPath); err == nil {
if !fi.IsDir() {
return errors.Errorf("%q is not a directory", destPath)
}
} else if os.IsNotExist(err) {
if err := os.MkdirAll(destPath, 0755); err != nil {
return err
}
} else if !fi.IsDir() {
return errors.Errorf("%q is not a directory", destPath)
}
if err := fs.RenameWithFallback(destPath, tmpPath); err != nil {
return errors.Wrap(err, "unable to move current charts to tmp dir")
} else {
return fmt.Errorf("unable to retrieve file info for '%s': %v", destPath, err)
}
if err := os.MkdirAll(destPath, 0755); err != nil {
// Prepare tmpPath
if err := os.MkdirAll(tmpPath, 0755); err != nil {
return err
}
defer os.RemoveAll(tmpPath)
fmt.Fprintf(m.Out, "Saving %d charts\n", len(deps))
var saveError error
@ -273,10 +275,11 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
// No repository means the chart is in charts directory
if dep.Repository == "" {
fmt.Fprintf(m.Out, "Dependency %s did not declare a repository. Assuming it exists in the charts directory\n", dep.Name)
chartPath := filepath.Join(tmpPath, dep.Name)
// NOTE: we are only validating the local dependency conforms to the constraints. No copying to tmpPath is necessary.
chartPath := filepath.Join(destPath, dep.Name)
ch, err := loader.LoadDir(chartPath)
if err != nil {
return fmt.Errorf("unable to load chart: %v", err)
return fmt.Errorf("unable to load chart '%s': %v", chartPath, err)
}
constraint, err := semver.NewConstraint(dep.Version)
@ -354,8 +357,7 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
getter.WithTagName(version))
}
_, _, err = dl.DownloadTo(churl, version, destPath)
if err != nil {
if _, _, err = dl.DownloadTo(churl, version, tmpPath); err != nil {
saveError = errors.Wrapf(err, "could not download %s", churl)
break
}
@ -363,36 +365,14 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
churls[churl] = struct{}{}
}
// TODO: this should probably be refactored to be a []error, so we can capture and provide more information rather than "last error wins".
if saveError == nil {
fmt.Fprintln(m.Out, "Deleting outdated charts")
for _, dep := range deps {
// Chart from local charts directory stays in place
if dep.Repository != "" {
if err := m.safeDeleteDep(dep.Name, tmpPath); err != nil {
return err
}
}
}
if err := move(tmpPath, destPath); err != nil {
// now we can move all downloaded charts to destPath and delete outdated dependencies
if err := m.safeMoveDeps(deps, tmpPath, destPath); err != nil {
return err
}
if err := os.RemoveAll(tmpPath); err != nil {
return errors.Wrapf(err, "failed to remove %v", tmpPath)
}
} else {
fmt.Fprintln(m.Out, "Save error occurred: ", saveError)
fmt.Fprintln(m.Out, "Deleting newly downloaded charts, restoring pre-update state")
for _, dep := range deps {
if err := m.safeDeleteDep(dep.Name, destPath); err != nil {
return err
}
}
if err := os.RemoveAll(destPath); err != nil {
return errors.Wrapf(err, "failed to remove %v", destPath)
}
if err := fs.RenameWithFallback(tmpPath, destPath); err != nil {
return errors.Wrap(err, "unable to move current charts to tmp dir")
}
return saveError
}
return nil
@ -410,36 +390,75 @@ func parseOCIRef(chartRef string) (string, string, error) {
return chartRef, tag, nil
}
// safeDeleteDep deletes any versions of the given dependency in the given directory.
// safeMoveDep moves all dependencies in the source and moves them into dest.
//
// It does this by first matching the file name to an expected pattern, then loading
// the file to verify that it is a chart with the same name as the given name.
// the file to verify that it is a chart.
//
// Because it requires tar file introspection, it is more intensive than a basic delete.
// Any charts in dest that do not exist in source are removed (barring local dependencies)
//
// Because it requires tar file introspection, it is more intensive than a basic move.
//
// This will only return errors that should stop processing entirely. Other errors
// will emit log messages or be ignored.
func (m *Manager) safeDeleteDep(name, dir string) error {
files, err := filepath.Glob(filepath.Join(dir, name+"-*.tgz"))
func (m *Manager) safeMoveDeps(deps []*chart.Dependency, source, dest string) error {
existsInSourceDirectory := map[string]bool{}
isLocalDependency := map[string]bool{}
sourceFiles, err := ioutil.ReadDir(source)
if err != nil {
// Only for ErrBadPattern
return err
}
for _, fname := range files {
ch, err := loader.LoadFile(fname)
if err != nil {
fmt.Fprintf(m.Out, "Could not verify %s for deletion: %s (Skipping)", fname, err)
// attempt to read destFiles; fail fast if we can't
destFiles, err := ioutil.ReadDir(dest)
if err != nil {
return err
}
for _, dep := range deps {
if dep.Repository == "" {
isLocalDependency[dep.Name] = true
}
}
for _, file := range sourceFiles {
if file.IsDir() {
continue
}
if ch.Name() != name {
// This is not the file you are looking for.
filename := file.Name()
sourcefile := filepath.Join(source, filename)
destfile := filepath.Join(dest, filename)
existsInSourceDirectory[filename] = true
if _, err := loader.LoadFile(sourcefile); err != nil {
fmt.Fprintf(m.Out, "Could not verify %s for moving: %s (Skipping)", sourcefile, err)
continue
}
if err := os.Remove(fname); err != nil {
fmt.Fprintf(m.Out, "Could not delete %s: %s (Skipping)", fname, err)
// NOTE: no need to delete the dest; os.Rename replaces it.
if err := fs.RenameWithFallback(sourcefile, destfile); err != nil {
fmt.Fprintf(m.Out, "Unable to move %s to charts dir %s (Skipping)", sourcefile, err)
continue
}
}
fmt.Fprintln(m.Out, "Deleting outdated charts")
// find all files that exist in dest that do not exist in source; delete them (outdated dependendencies)
for _, file := range destFiles {
if !file.IsDir() && !existsInSourceDirectory[file.Name()] {
fname := filepath.Join(dest, file.Name())
ch, err := loader.LoadFile(fname)
if err != nil {
fmt.Fprintf(m.Out, "Could not verify %s for deletion: %s (Skipping)", fname, err)
}
// local dependency - skip
if isLocalDependency[ch.Name()] {
continue
}
if err := os.Remove(fname); err != nil {
fmt.Fprintf(m.Out, "Could not delete %s: %s (Skipping)", fname, err)
continue
}
}
}
return nil
}
@ -868,20 +887,6 @@ func tarFromLocalDir(chartpath, name, repo, version string) (string, error) {
return "", errors.Errorf("can't get a valid version for dependency %s", name)
}
// move files from tmppath to destpath
func move(tmpPath, destPath string) error {
files, _ := os.ReadDir(tmpPath)
for _, file := range files {
filename := file.Name()
tmpfile := filepath.Join(tmpPath, filename)
destfile := filepath.Join(destPath, filename)
if err := fs.RenameWithFallback(tmpfile, destfile); err != nil {
return errors.Wrap(err, "unable to move local charts to charts dir")
}
}
return nil
}
// The prefix to use for cache keys created by the manager for repo names
const managerKeyPrefix = "helm-manager-"

@ -17,11 +17,14 @@ package downloader
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"testing"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/repo/repotest"
@ -213,6 +216,54 @@ func TestGetRepoNames(t *testing.T) {
}
}
func TestDownloadAll(t *testing.T) {
chartPath, err := ioutil.TempDir("", "test-downloadall")
if err != nil {
t.Fatalf("could not create tempdir: %v", err)
}
defer os.RemoveAll(chartPath)
m := &Manager{
Out: new(bytes.Buffer),
RepositoryConfig: repoConfig,
RepositoryCache: repoCache,
ChartPath: chartPath,
}
signtest, err := loader.LoadDir(filepath.Join("testdata", "signtest"))
if err != nil {
t.Fatal(err)
}
if err := chartutil.SaveDir(signtest, filepath.Join(chartPath, "testdata")); err != nil {
t.Fatal(err)
}
local, err := loader.LoadDir(filepath.Join("testdata", "local-subchart"))
if err != nil {
t.Fatal(err)
}
if err := chartutil.SaveDir(local, filepath.Join(chartPath, "charts")); err != nil {
t.Fatal(err)
}
signDep := &chart.Dependency{
Name: signtest.Name(),
Repository: "file://./testdata/signtest",
Version: signtest.Metadata.Version,
}
localDep := &chart.Dependency{
Name: local.Name(),
Repository: "",
Version: local.Metadata.Version,
}
// create a 'tmpcharts' directory to test #5567
if err := os.MkdirAll(filepath.Join(chartPath, "tmpcharts"), 0755); err != nil {
t.Fatal(err)
}
if err := m.downloadAll([]*chart.Dependency{signDep, localDep}); err != nil {
t.Error(err)
}
}
func TestUpdateBeforeBuild(t *testing.T) {
// Set up a fake repo
srv, err := repotest.NewTempServerWithCleanup(t, "testdata/*.tgz*")

@ -1,7 +1,7 @@
apiVersion: v1
entries:
tlsfoo:
- name: tlsfoo
- name: tlsfoo
description: TLS FOO Chart
home: https://helm.sh/helm
keywords: []

@ -97,7 +97,7 @@ const warnStartDelim = "HELM_ERR_START"
const warnEndDelim = "HELM_ERR_END"
const recursionMaxNums = 1000
var warnRegex = regexp.MustCompile(warnStartDelim + `(.*)` + warnEndDelim)
var warnRegex = regexp.MustCompile(warnStartDelim + `((?s).*)` + warnEndDelim)
func warnWrap(warn string) string {
return warnStartDelim + warn + warnEndDelim
@ -347,8 +347,13 @@ func allTemplates(c *chart.Chart, vals chartutil.Values) map[string]renderable {
// scope.
func recAllTpls(c *chart.Chart, templates map[string]renderable, vals chartutil.Values) map[string]interface{} {
subCharts := make(map[string]interface{})
chartMetaData := struct {
chart.Metadata
IsRoot bool
}{*c.Metadata, c.IsRoot()}
next := map[string]interface{}{
"Chart": c.Metadata,
"Chart": chartMetaData,
"Files": newFiles(c.Files),
"Release": vals["Release"],
"Capabilities": vals["Capabilities"],

@ -245,44 +245,65 @@ func TestParseErrors(t *testing.T) {
func TestExecErrors(t *testing.T) {
vals := chartutil.Values{"Values": map[string]interface{}{}}
tplsMissingRequired := map[string]renderable{
"missing_required": {tpl: `{{required "foo is required" .Values.foo}}`, vals: vals},
}
_, err := new(Engine).render(tplsMissingRequired)
if err == nil {
t.Fatalf("Expected failures while rendering: %s", err)
}
expected := `execution error at (missing_required:1:2): foo is required`
if err.Error() != expected {
t.Errorf("Expected '%s', got %q", expected, err.Error())
}
tplsMissingRequired = map[string]renderable{
"missing_required_with_colons": {tpl: `{{required ":this: message: has many: colons:" .Values.foo}}`, vals: vals},
}
_, err = new(Engine).render(tplsMissingRequired)
if err == nil {
t.Fatalf("Expected failures while rendering: %s", err)
}
expected = `execution error at (missing_required_with_colons:1:2): :this: message: has many: colons:`
if err.Error() != expected {
t.Errorf("Expected '%s', got %q", expected, err.Error())
}
issue6044tpl := `{{ $someEmptyValue := "" }}
cases := []struct {
name string
tpls map[string]renderable
expected string
}{
{
name: "MissingRequired",
tpls: map[string]renderable{
"missing_required": {tpl: `{{required "foo is required" .Values.foo}}`, vals: vals},
},
expected: `execution error at (missing_required:1:2): foo is required`,
},
{
name: "MissingRequiredWithColons",
tpls: map[string]renderable{
"missing_required_with_colons": {tpl: `{{required ":this: message: has many: colons:" .Values.foo}}`, vals: vals},
},
expected: `execution error at (missing_required_with_colons:1:2): :this: message: has many: colons:`,
},
{
name: "Issue6044",
tpls: map[string]renderable{
"issue6044": {
vals: vals,
tpl: `{{ $someEmptyValue := "" }}
{{ $myvar := "abc" }}
{{- required (printf "%s: something is missing" $myvar) $someEmptyValue | repeat 0 }}`
tplsMissingRequired = map[string]renderable{
"issue6044": {tpl: issue6044tpl, vals: vals},
}
_, err = new(Engine).render(tplsMissingRequired)
if err == nil {
t.Fatalf("Expected failures while rendering: %s", err)
{{- required (printf "%s: something is missing" $myvar) $someEmptyValue | repeat 0 }}`,
},
},
expected: `execution error at (issue6044:3:4): abc: something is missing`,
},
{
name: "MissingRequiredWithNewlines",
tpls: map[string]renderable{
"issue9981": {tpl: `{{required "foo is required\nmore info after the break" .Values.foo}}`, vals: vals},
},
expected: `execution error at (issue9981:1:2): foo is required
more info after the break`,
},
{
name: "FailWithNewlines",
tpls: map[string]renderable{
"issue9981": {tpl: `{{fail "something is wrong\nlinebreak"}}`, vals: vals},
},
expected: `execution error at (issue9981:1:2): something is wrong
linebreak`,
},
}
expected = `execution error at (issue6044:3:4): abc: something is missing`
if err.Error() != expected {
t.Errorf("Expected '%s', got %q", expected, err.Error())
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
_, err := new(Engine).render(tt.tpls)
if err == nil {
t.Fatalf("Expected failures while rendering: %s", err)
}
if err.Error() != tt.expected {
t.Errorf("Expected %q, got %q", tt.expected, err.Error())
}
})
}
}
@ -346,6 +367,36 @@ func TestAllTemplates(t *testing.T) {
}
}
func TestChartValuesContainsIsRoot(t *testing.T) {
ch1 := &chart.Chart{
Metadata: &chart.Metadata{Name: "parent"},
Templates: []*chart.File{
{Name: "templates/isroot", Data: []byte("{{.Chart.IsRoot}}")},
},
}
dep1 := &chart.Chart{
Metadata: &chart.Metadata{Name: "child"},
Templates: []*chart.File{
{Name: "templates/isroot", Data: []byte("{{.Chart.IsRoot}}")},
},
}
ch1.AddDependency(dep1)
out, err := Render(ch1, chartutil.Values{})
if err != nil {
t.Fatalf("failed to render templates: %s", err)
}
expects := map[string]string{
"parent/charts/child/templates/isroot": "false",
"parent/templates/isroot": "true",
}
for file, expect := range expects {
if out[file] != expect {
t.Errorf("Expected %q, got %q", expect, out[file])
}
}
}
func TestRenderDependency(t *testing.T) {
deptpl := `{{define "myblock"}}World{{end}}`
toptpl := `Hello {{template "myblock"}}`

@ -227,7 +227,7 @@ func (c *ReadyChecker) jobReady(job *batchv1.Job) bool {
c.log("Job is failed: %s/%s", job.GetNamespace(), job.GetName())
return false
}
if job.Status.Succeeded < *job.Spec.Completions {
if job.Spec.Completions != nil && job.Status.Succeeded < *job.Spec.Completions {
c.log("Job is not completed: %s/%s", job.GetNamespace(), job.GetName())
return false
}

@ -241,39 +241,44 @@ func Test_ReadyChecker_jobReady(t *testing.T) {
}{
{
name: "job is completed",
args: args{job: newJob("foo", 1, 1, 1, 0)},
args: args{job: newJob("foo", 1, intToInt32(1), 1, 0)},
want: true,
},
{
name: "job is incomplete",
args: args{job: newJob("foo", 1, 1, 0, 0)},
args: args{job: newJob("foo", 1, intToInt32(1), 0, 0)},
want: false,
},
{
name: "job is failed",
args: args{job: newJob("foo", 1, 1, 0, 1)},
args: args{job: newJob("foo", 1, intToInt32(1), 0, 1)},
want: false,
},
{
name: "job is completed with retry",
args: args{job: newJob("foo", 1, 1, 1, 1)},
args: args{job: newJob("foo", 1, intToInt32(1), 1, 1)},
want: true,
},
{
name: "job is failed with retry",
args: args{job: newJob("foo", 1, 1, 0, 2)},
args: args{job: newJob("foo", 1, intToInt32(1), 0, 2)},
want: false,
},
{
name: "job is completed single run",
args: args{job: newJob("foo", 0, 1, 1, 0)},
args: args{job: newJob("foo", 0, intToInt32(1), 1, 0)},
want: true,
},
{
name: "job is failed single run",
args: args{job: newJob("foo", 0, 1, 0, 1)},
args: args{job: newJob("foo", 0, intToInt32(1), 0, 1)},
want: false,
},
{
name: "job with null completions",
args: args{job: newJob("foo", 0, nil, 1, 0)},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -481,7 +486,7 @@ func newPersistentVolumeClaim(name string, phase corev1.PersistentVolumeClaimPha
}
}
func newJob(name string, backoffLimit, completions, succeeded, failed int) *batchv1.Job {
func newJob(name string, backoffLimit int, completions *int32, succeeded int, failed int) *batchv1.Job {
return &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: name,
@ -489,7 +494,7 @@ func newJob(name string, backoffLimit, completions, succeeded, failed int) *batc
},
Spec: batchv1.JobSpec{
BackoffLimit: intToInt32(backoffLimit),
Completions: intToInt32(completions),
Completions: completions,
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Name: name,

@ -146,6 +146,7 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace
linter.RunLinterRule(support.WarningSev, fpath, validateNoDeprecations(yamlStruct))
linter.RunLinterRule(support.ErrorSev, fpath, validateMatchSelector(yamlStruct, renderedContent))
linter.RunLinterRule(support.ErrorSev, fpath, validateListAnnotations(yamlStruct, renderedContent))
}
}
}
@ -293,6 +294,28 @@ func validateMatchSelector(yamlStruct *K8sYamlStruct, manifest string) error {
}
return nil
}
func validateListAnnotations(yamlStruct *K8sYamlStruct, manifest string) error {
if yamlStruct.Kind == "List" {
m := struct {
Items []struct {
Metadata struct {
Annotations map[string]string
}
}
}{}
if err := yaml.Unmarshal([]byte(manifest), &m); err != nil {
return validateYamlContent(err)
}
for _, i := range m.Items {
if _, ok := i.Metadata.Annotations["helm.sh/resource-policy"]; ok {
return errors.New("Annotation 'helm.sh/resource-policy' within List objects are ignored")
}
}
}
return nil
}
// K8sYamlStruct stubs a Kubernetes YAML file.
//

@ -424,3 +424,41 @@ func TestEmptyWithCommentsManifests(t *testing.T) {
t.Fatalf("Expected 0 lint errors, got %d", l)
}
}
func TestValidateListAnnotations(t *testing.T) {
md := &K8sYamlStruct{
APIVersion: "v1",
Kind: "List",
Metadata: k8sYamlMetadata{
Name: "list",
},
}
manifest := `
apiVersion: v1
kind: List
items:
- apiVersion: v1
kind: ConfigMap
metadata:
annotations:
helm.sh/resource-policy: keep
`
if err := validateListAnnotations(md, manifest); err == nil {
t.Fatal("expected list with nested keep annotations to fail")
}
manifest = `
apiVersion: v1
kind: List
metadata:
annotations:
helm.sh/resource-policy: keep
items:
- apiVersion: v1
kind: ConfigMap
`
if err := validateListAnnotations(md, manifest); err != nil {
t.Fatalf("List objects keep annotations should pass. got: %s", err)
}
}

@ -286,7 +286,8 @@ func FindChartInAuthAndTLSAndPassRepoURL(repoURL, username, password, chartName,
// ResolveReferenceURL resolves refURL relative to baseURL.
// If refURL is absolute, it simply returns refURL.
func ResolveReferenceURL(baseURL, refURL string) (string, error) {
parsedBaseURL, err := url.Parse(baseURL)
// We need a trailing slash for ResolveReference to work, but make sure there isn't already one
parsedBaseURL, err := url.Parse(strings.TrimSuffix(baseURL, "/") + "/")
if err != nil {
return "", errors.Wrapf(err, "failed to parse %s as URL", baseURL)
}
@ -296,8 +297,6 @@ func ResolveReferenceURL(baseURL, refURL string) (string, error) {
return "", errors.Wrapf(err, "failed to parse %s as URL", refURL)
}
// We need a trailing slash for ResolveReference to work, but make sure there isn't already one
parsedBaseURL.Path = strings.TrimSuffix(parsedBaseURL.Path, "/") + "/"
return parsedBaseURL.ResolveReference(parsedRefURL).String(), nil
}

@ -400,4 +400,12 @@ func TestResolveReferenceURL(t *testing.T) {
if chartURL != "https://charts.helm.sh/stable/nginx-0.2.0.tgz" {
t.Errorf("%s", chartURL)
}
chartURL, err = ResolveReferenceURL("http://localhost:8123/charts%2fwith%2fescaped%2fslash", "nginx-0.2.0.tgz")
if err != nil {
t.Errorf("%s", err)
}
if chartURL != "http://localhost:8123/charts%2fwith%2fescaped%2fslash/nginx-0.2.0.tgz" {
t.Errorf("%s", chartURL)
}
}

@ -50,6 +50,8 @@ var (
ErrNoChartVersion = errors.New("no chart version found")
// ErrNoChartName indicates that a chart with the given name is not found.
ErrNoChartName = errors.New("no chart name found")
// ErrEmptyIndexYaml indicates that the content of index.yaml is empty.
ErrEmptyIndexYaml = errors.New("empty index.yaml file")
)
// ChartVersions is a list of versioned chart references.
@ -326,6 +328,11 @@ func IndexDirectory(dir, baseURL string) (*IndexFile, error) {
// This will fail if API Version is not set (ErrNoAPIVersion) or if the unmarshal fails.
func loadIndex(data []byte, source string) (*IndexFile, error) {
i := &IndexFile{}
if len(data) == 0 {
return i, ErrEmptyIndexYaml
}
if err := yaml.UnmarshalStrict(data, i); err != nil {
return i, err
}

@ -152,6 +152,12 @@ func TestLoadIndex_Duplicates(t *testing.T) {
}
}
func TestLoadIndex_Empty(t *testing.T) {
if _, err := loadIndex([]byte(""), "indexWithEmpty"); err == nil {
t.Errorf("Expected an error when index.yaml is empty.")
}
}
func TestLoadIndexFileAnnotations(t *testing.T) {
i, err := LoadIndexFile(annotationstestfile)
if err != nil {

@ -26,13 +26,13 @@ import (
"testing"
"time"
auth "github.com/deislabs/oras/pkg/auth/docker"
"github.com/docker/distribution/configuration"
"github.com/docker/distribution/registry"
_ "github.com/docker/distribution/registry/auth/htpasswd" // used for docker test registry
_ "github.com/docker/distribution/registry/storage/driver/inmemory" // used for docker test registry
"github.com/distribution/distribution/v3/configuration"
"github.com/distribution/distribution/v3/registry"
_ "github.com/distribution/distribution/v3/registry/auth/htpasswd" // used for docker test registry
_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory" // used for docker test registry
"github.com/phayes/freeport"
"golang.org/x/crypto/bcrypt"
auth "oras.land/oras-go/pkg/auth/docker"
"sigs.k8s.io/yaml"
ociRegistry "helm.sh/helm/v3/internal/experimental/registry"
@ -56,6 +56,23 @@ func NewTempServerWithCleanup(t *testing.T, glob string) (*Server, error) {
return srv, err
}
// Set up a fake repo with basic auth enabled
func NewTempServerWithCleanupAndBasicAuth(t *testing.T, glob string) *Server {
srv, err := NewTempServerWithCleanup(t, glob)
srv.Stop()
if err != nil {
t.Fatal(err)
}
srv.WithMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
username, password, ok := r.BasicAuth()
if !ok || username != "username" || password != "password" {
t.Errorf("Expected request to use basic auth and for username == 'username' and password == 'password', got '%v', '%s', '%s'", ok, username, password)
}
}))
srv.Start()
return srv
}
type OCIServer struct {
*registry.Registry
RegistryURL string

@ -55,8 +55,8 @@ func (s *Storage) Get(name string, version int) (*rspb.Release, error) {
}
// Create creates a new storage entry holding the release. An
// error is returned if the storage driver failed to store the
// release, or a release with identical an key already exists.
// error is returned if the storage driver fails to store the
// release, or a release with an identical key already exists.
func (s *Storage) Create(rls *rspb.Release) error {
s.Log("creating release %q", makeKey(rls.Name, rls.Version))
if s.MaxHistory > 0 {

Loading…
Cancel
Save