Merge branch 'main' into 4030-provide-literal-alternative-for-set-flag

Signed-off-by: Matt Farina <matt@mattfarina.com>
pull/9182/head
Matt Farina 3 years ago committed by GitHub
commit 6611cdcd01
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -34,7 +34,7 @@ else
fi fi
echo "Installing Azure CLI" echo "Installing Azure CLI"
echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ stretch main" | sudo tee /etc/apt/sources.list.d/azure-cli.list echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ jammy main" | sudo tee /etc/apt/sources.list.d/azure-cli.list
curl -L https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add curl -L https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add
sudo apt install apt-transport-https sudo apt install apt-transport-https
sudo apt update sudo apt update

@ -18,12 +18,13 @@ ACCEPTANCE_DIR:=../acceptance-testing
ACCEPTANCE_RUN_TESTS=. ACCEPTANCE_RUN_TESTS=.
# go option # go option
PKG := ./... PKG := ./...
TAGS := TAGS :=
TESTS := . TESTS := .
TESTFLAGS := TESTFLAGS :=
LDFLAGS := -w -s LDFLAGS := -w -s
GOFLAGS := GOFLAGS :=
CGO_ENABLED ?= 0
# Rebuild the binary if any of these files change # Rebuild the binary if any of these files change
SRC := $(shell find . -type f -name '*.go' -print) go.mod go.sum SRC := $(shell find . -type f -name '*.go' -print) go.mod go.sum
@ -77,7 +78,7 @@ all: build
build: $(BINDIR)/$(BINNAME) build: $(BINDIR)/$(BINNAME)
$(BINDIR)/$(BINNAME): $(SRC) $(BINDIR)/$(BINNAME): $(SRC)
GO111MODULE=on CGO_ENABLED=0 go build $(GOFLAGS) -trimpath -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o '$(BINDIR)'/$(BINNAME) ./cmd/helm GO111MODULE=on CGO_ENABLED=$(CGO_ENABLED) go build $(GOFLAGS) -trimpath -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o '$(BINDIR)'/$(BINNAME) ./cmd/helm
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# install # install

@ -59,7 +59,7 @@ func newGetAllCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return tpl(template, data, out) return tpl(template, data, out)
} }
return output.Table.Write(out, &statusPrinter{res, true, false}) return output.Table.Write(out, &statusPrinter{res, true, false, false})
}, },
} }

@ -141,7 +141,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return errors.Wrap(err, "INSTALLATION FAILED") return errors.Wrap(err, "INSTALLATION FAILED")
} }
return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false}) return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false, false})
}, },
} }

@ -72,7 +72,7 @@ func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command
return runErr return runErr
} }
if err := outfmt.Write(out, &statusPrinter{rel, settings.Debug, false}); err != nil { if err := outfmt.Write(out, &statusPrinter{rel, settings.Debug, false, false}); err != nil {
return err return err
} }

@ -155,7 +155,7 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
registryClient, err := registry.NewClient( registryClient, err := registry.NewClient(
registry.ClientOptDebug(settings.Debug), registry.ClientOptDebug(settings.Debug),
registry.ClientOptEnableCache(true), registry.ClientOptEnableCache(true),
registry.ClientOptWriter(out), registry.ClientOptWriter(os.Stderr),
registry.ClientOptCredentialsFile(settings.RegistryConfig), registry.ClientOptCredentialsFile(settings.RegistryConfig),
) )
if err != nil { if err != nil {

@ -17,6 +17,7 @@ limitations under the License.
package main package main
import ( import (
"bytes"
"fmt" "fmt"
"io" "io"
"log" "log"
@ -25,6 +26,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/kubectl/pkg/cmd/get"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/chartutil"
@ -41,7 +44,7 @@ The status consists of:
- state of the release (can be: unknown, deployed, uninstalled, superseded, failed, uninstalling, pending-install, pending-upgrade or pending-rollback) - state of the release (can be: unknown, deployed, uninstalled, superseded, failed, uninstalling, pending-install, pending-upgrade or pending-rollback)
- revision of the release - revision of the release
- description of the release (can be completion message or error message, need to enable --show-desc) - description of the release (can be completion message or error message, need to enable --show-desc)
- list of resources that this release consists of, sorted by kind - list of resources that this release consists of (need to enable --show-resources)
- details on last test suite run, if applicable - details on last test suite run, if applicable
- additional notes provided by the chart - additional notes provided by the chart
` `
@ -70,7 +73,7 @@ func newStatusCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
// strip chart metadata from the output // strip chart metadata from the output
rel.Chart = nil rel.Chart = nil
return outfmt.Write(out, &statusPrinter{rel, false, client.ShowDescription}) return outfmt.Write(out, &statusPrinter{rel, false, client.ShowDescription, client.ShowResources})
}, },
} }
@ -92,6 +95,8 @@ func newStatusCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
bindOutputFlag(cmd, &outfmt) bindOutputFlag(cmd, &outfmt)
f.BoolVar(&client.ShowDescription, "show-desc", false, "if set, display the description message of the named release") f.BoolVar(&client.ShowDescription, "show-desc", false, "if set, display the description message of the named release")
f.BoolVar(&client.ShowResources, "show-resources", false, "if set, display the resources of the named release")
return cmd return cmd
} }
@ -99,6 +104,7 @@ type statusPrinter struct {
release *release.Release release *release.Release
debug bool debug bool
showDescription bool showDescription bool
showResources bool
} }
func (s statusPrinter) WriteJSON(out io.Writer) error { func (s statusPrinter) WriteJSON(out io.Writer) error {
@ -124,6 +130,33 @@ func (s statusPrinter) WriteTable(out io.Writer) error {
fmt.Fprintf(out, "DESCRIPTION: %s\n", s.release.Info.Description) fmt.Fprintf(out, "DESCRIPTION: %s\n", s.release.Info.Description)
} }
if s.showResources && s.release.Info.Resources != nil && len(s.release.Info.Resources) > 0 {
buf := new(bytes.Buffer)
printFlags := get.NewHumanPrintFlags()
typePrinter, _ := printFlags.ToPrinter("")
printer := &get.TablePrinter{Delegate: typePrinter}
var keys []string
for key := range s.release.Info.Resources {
keys = append(keys, key)
}
for _, t := range keys {
fmt.Fprintf(buf, "==> %s\n", t)
vk := s.release.Info.Resources[t]
for _, resource := range vk {
if err := printer.PrintObj(resource, buf); err != nil {
fmt.Fprintf(buf, "failed to print object type %s: %v\n", t, err)
}
}
buf.WriteString("\n")
}
fmt.Fprintf(out, "RESOURCES:\n%s\n", buf.String())
}
executions := executionsByHookEvent(s.release) executions := executionsByHookEvent(s.release)
if tests, ok := executions[release.HookTest]; !ok || len(tests) == 0 { if tests, ok := executions[release.HookTest]; !ok || len(tests) == 0 {
fmt.Fprintln(out, "TEST SUITE: None") fmt.Fprintln(out, "TEST SUITE: None")

@ -68,6 +68,24 @@ func TestStatusCmd(t *testing.T) {
Status: release.StatusDeployed, Status: release.StatusDeployed,
Notes: "release notes", Notes: "release notes",
}), }),
}, {
name: "get status of a deployed release with resources",
cmd: "status --show-resources flummoxed-chickadee",
golden: "output/status-with-resources.txt",
rels: releasesMockWithStatus(
&release.Info{
Status: release.StatusDeployed,
},
),
}, {
name: "get status of a deployed release with resources in json",
cmd: "status --show-resources flummoxed-chickadee -o json",
golden: "output/status-with-resources.json",
rels: releasesMockWithStatus(
&release.Info{
Status: release.StatusDeployed,
},
),
}, { }, {
name: "get status of a deployed release with test suite", name: "get status of a deployed release with test suite",
cmd: "status flummoxed-chickadee", cmd: "status flummoxed-chickadee",

@ -0,0 +1 @@
{"name":"flummoxed-chickadee","info":{"first_deployed":"","last_deployed":"2016-01-16T00:00:00Z","deleted":"","status":"deployed"},"namespace":"default"}

@ -0,0 +1,6 @@
NAME: flummoxed-chickadee
LAST DEPLOYED: Sat Jan 16 00:00:00 2016
NAMESPACE: default
STATUS: deployed
REVISION: 0
TEST SUITE: None

@ -1 +1 @@
version.BuildInfo{Version:"v3.9", GitCommit:"", GitTreeState:"", GoVersion:""} version.BuildInfo{Version:"v3.10", GitCommit:"", GitTreeState:"", GoVersion:""}

@ -1 +1 @@
version.BuildInfo{Version:"v3.9", GitCommit:"", GitTreeState:"", GoVersion:""} version.BuildInfo{Version:"v3.10", GitCommit:"", GitTreeState:"", GoVersion:""}

@ -1 +1 @@
Version: v3.9 Version: v3.10

@ -1 +1 @@
version.BuildInfo{Version:"v3.9", GitCommit:"", GitTreeState:"", GoVersion:""} version.BuildInfo{Version:"v3.10", GitCommit:"", GitTreeState:"", GoVersion:""}

@ -123,7 +123,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
if err != nil { if err != nil {
return err return err
} }
return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false}) return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false, false})
} else if err != nil { } else if err != nil {
return err return err
} }
@ -205,7 +205,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
fmt.Fprintf(out, "Release %q has been upgraded. Happy Helming!\n", args[0]) fmt.Fprintf(out, "Release %q has been upgraded. Happy Helming!\n", args[0])
} }
return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false}) return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false, false})
}, },
} }

@ -1,12 +1,12 @@
module helm.sh/helm/v3 module helm.sh/helm/v3
go 1.18 go 1.17
require ( require (
github.com/BurntSushi/toml v1.1.0 github.com/BurntSushi/toml v1.2.0
github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/DATA-DOG/go-sqlmock v1.5.0
github.com/Masterminds/semver/v3 v3.1.1 github.com/Masterminds/semver/v3 v3.2.0
github.com/Masterminds/sprig/v3 v3.2.2 github.com/Masterminds/sprig/v3 v3.2.3
github.com/Masterminds/squirrel v1.5.3 github.com/Masterminds/squirrel v1.5.3
github.com/Masterminds/vcs v1.13.3 github.com/Masterminds/vcs v1.13.3
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535
@ -18,30 +18,30 @@ require (
github.com/gofrs/flock v0.8.1 github.com/gofrs/flock v0.8.1
github.com/gosuri/uitable v0.0.4 github.com/gosuri/uitable v0.0.4
github.com/jmoiron/sqlx v1.3.5 github.com/jmoiron/sqlx v1.3.5
github.com/lib/pq v1.10.6 github.com/lib/pq v1.10.7
github.com/mattn/go-shellwords v1.0.12 github.com/mattn/go-shellwords v1.0.12
github.com/mitchellh/copystructure v1.2.0 github.com/mitchellh/copystructure v1.2.0
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/rubenv/sql-migrate v1.1.2 github.com/rubenv/sql-migrate v1.2.0
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.5.0 github.com/spf13/cobra v1.6.1
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.0 github.com/stretchr/testify v1.7.4
github.com/xeipuuv/gojsonschema v1.2.0 github.com/xeipuuv/gojsonschema v1.2.0
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e golang.org/x/crypto v0.3.0
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 golang.org/x/term v0.2.0
golang.org/x/text v0.3.7 golang.org/x/text v0.5.0
k8s.io/api v0.25.0 k8s.io/api v0.25.2
k8s.io/apiextensions-apiserver v0.25.0 k8s.io/apiextensions-apiserver v0.25.2
k8s.io/apimachinery v0.25.0 k8s.io/apimachinery v0.25.2
k8s.io/apiserver v0.25.0 k8s.io/apiserver v0.25.2
k8s.io/cli-runtime v0.25.0 k8s.io/cli-runtime v0.25.2
k8s.io/client-go v0.25.0 k8s.io/client-go v0.25.2
k8s.io/klog/v2 v2.70.1 k8s.io/klog/v2 v2.70.1
k8s.io/kubectl v0.25.0 k8s.io/kubectl v0.25.2
oras.land/oras-go v1.2.0 oras.land/oras-go v1.2.0
sigs.k8s.io/yaml v1.3.0 sigs.k8s.io/yaml v1.3.0
) )
@ -82,6 +82,7 @@ require (
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
github.com/fatih/color v1.13.0 // indirect github.com/fatih/color v1.13.0 // indirect
github.com/felixge/httpsnoop v1.0.1 // indirect github.com/felixge/httpsnoop v1.0.1 // indirect
github.com/fvbommel/sortorder v1.0.1 // indirect
github.com/go-errors/errors v1.0.1 // indirect github.com/go-errors/errors v1.0.1 // indirect
github.com/go-gorp/gorp/v3 v3.0.2 // indirect github.com/go-gorp/gorp/v3 v3.0.2 // indirect
github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/logr v1.2.3 // indirect
@ -94,16 +95,16 @@ require (
github.com/gomodule/redigo v1.8.2 // indirect github.com/gomodule/redigo v1.8.2 // indirect
github.com/google/btree v1.0.1 // indirect github.com/google/btree v1.0.1 // indirect
github.com/google/gnostic v0.5.7-v3refs // indirect github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/go-cmp v0.5.6 // indirect github.com/google/go-cmp v0.5.8 // indirect
github.com/google/gofuzz v1.2.0 // indirect github.com/google/gofuzz v1.2.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.2.0 // indirect github.com/google/uuid v1.2.0 // indirect
github.com/gorilla/handlers v1.5.1 // indirect github.com/gorilla/handlers v1.5.1 // indirect
github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/mux v1.8.0 // indirect
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
github.com/huandu/xstrings v1.3.2 // indirect github.com/huandu/xstrings v1.3.3 // indirect
github.com/imdario/mergo v0.3.12 // indirect github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.13.6 // indirect github.com/klauspost/compress v1.13.6 // indirect
@ -142,10 +143,10 @@ require (
github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 // indirect github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 // indirect
github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f // indirect github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f // indirect
go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/net v0.2.0 // indirect
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect golang.org/x/sys v0.2.0 // indirect
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 // indirect google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 // indirect
@ -154,7 +155,7 @@ require (
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/component-base v0.25.0 // indirect k8s.io/component-base v0.25.2 // indirect
k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect
k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect

839
go.sum

File diff suppressed because it is too large Load Diff

@ -29,7 +29,7 @@ var (
// //
// Increment major number for new feature additions and behavioral changes. // Increment major number for new feature additions and behavioral changes.
// Increment minor number for bug fixes and performance enhancements. // Increment minor number for bug fixes and performance enhancements.
version = "v3.9" version = "v3.10"
// metadata is extra build time data // metadata is extra build time data
metadata = "" metadata = ""

@ -76,7 +76,7 @@ func (l *Lint) Run(paths []string, vals map[string]interface{}) *LintResult {
return result return result
} }
// HasWaringsOrErrors checks is LintResult has any warnings or errors // HasWarningsOrErrors checks is LintResult has any warnings or errors
func HasWarningsOrErrors(result *LintResult) bool { func HasWarningsOrErrors(result *LintResult) bool {
for _, msg := range result.Messages { for _, msg := range result.Messages {
if msg.Severity > support.InfoSev { if msg.Severity > support.InfoSev {

@ -17,6 +17,9 @@ limitations under the License.
package action package action
import ( import (
"bytes"
"helm.sh/helm/v3/pkg/kube"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/release"
) )
@ -32,6 +35,10 @@ type Status struct {
// only affect print type table. // only affect print type table.
// TODO Helm 4: Remove this flag and output the description by default. // TODO Helm 4: Remove this flag and output the description by default.
ShowDescription bool ShowDescription bool
// If true, display resources of release to output format
// TODO Helm 4: Remove this flag and output the resources by default.
ShowResources bool
} }
// NewStatus creates a new Status object with the given configuration. // NewStatus creates a new Status object with the given configuration.
@ -47,5 +54,26 @@ func (s *Status) Run(name string) (*release.Release, error) {
return nil, err return nil, err
} }
return s.cfg.releaseContent(name, s.Version) if !s.ShowResources {
return s.cfg.releaseContent(name, s.Version)
}
rel, err := s.cfg.releaseContent(name, s.Version)
if err != nil {
return nil, err
}
resources, _ := s.cfg.KubeClient.Build(bytes.NewBufferString(rel.Manifest), false)
if kubeClient, ok := s.cfg.KubeClient.(kube.InterfaceResources); ok {
resp, err := kubeClient.Get(resources, bytes.NewBufferString(rel.Manifest))
if err != nil {
return nil, err
}
rel.Info.Resources = resp
return rel, nil
}
return nil, err
} }

@ -53,6 +53,9 @@ type Dependency struct {
// the chart. This check must be done at load time before the dependency's charts are // the chart. This check must be done at load time before the dependency's charts are
// loaded. // loaded.
func (d *Dependency) Validate() error { func (d *Dependency) Validate() error {
if d == nil {
return ValidationError("dependency cannot be an empty list")
}
d.Name = sanitizeString(d.Name) d.Name = sanitizeString(d.Name)
d.Version = sanitizeString(d.Version) d.Version = sanitizeString(d.Version)
d.Repository = sanitizeString(d.Repository) d.Repository = sanitizeString(d.Repository)

@ -34,6 +34,9 @@ type Maintainer struct {
// Validate checks valid data and sanitizes string characters. // Validate checks valid data and sanitizes string characters.
func (m *Maintainer) Validate() error { func (m *Maintainer) Validate() error {
if m == nil {
return ValidationError("maintainer cannot be an empty list")
}
m.Name = sanitizeString(m.Name) m.Name = sanitizeString(m.Name)
m.Email = sanitizeString(m.Email) m.Email = sanitizeString(m.Email)
m.URL = sanitizeString(m.URL) m.URL = sanitizeString(m.URL)

@ -72,6 +72,30 @@ func TestValidate(t *testing.T) {
}, },
ValidationError("dependency \"bad\" has disallowed characters in the alias"), ValidationError("dependency \"bad\" has disallowed characters in the alias"),
}, },
{
&Metadata{
Name: "test",
APIVersion: "v2",
Version: "1.0",
Type: "application",
Dependencies: []*Dependency{
nil,
},
},
ValidationError("dependency cannot be an empty list"),
},
{
&Metadata{
Name: "test",
APIVersion: "v2",
Version: "1.0",
Type: "application",
Maintainers: []*Maintainer{
nil,
},
},
ValidationError("maintainer cannot be an empty list"),
},
{ {
&Metadata{APIVersion: "v2", Name: "test", Version: "1.2.3.4"}, &Metadata{APIVersion: "v2", Name: "test", Version: "1.2.3.4"},
ValidationError("chart.metadata.version \"1.2.3.4\" is invalid"), ValidationError("chart.metadata.version \"1.2.3.4\" is invalid"),

@ -62,8 +62,8 @@ func TestDefaultCapabilities(t *testing.T) {
func TestDefaultCapabilitiesHelmVersion(t *testing.T) { func TestDefaultCapabilitiesHelmVersion(t *testing.T) {
hv := DefaultCapabilities.HelmVersion hv := DefaultCapabilities.HelmVersion
if hv.Version != "v3.9" { if hv.Version != "v3.10" {
t.Errorf("Expected default HelmVersion to be v3.9, got %q", hv.Version) t.Errorf("Expected default HelmVersion to be v3.10, got %q", hv.Version)
} }
} }

@ -312,7 +312,7 @@ spec:
imagePullPolicy: {{ .Values.image.pullPolicy }} imagePullPolicy: {{ .Values.image.pullPolicy }}
ports: ports:
- name: http - name: http
containerPort: 80 containerPort: {{ .Values.service.port }}
protocol: TCP protocol: TCP
livenessProbe: livenessProbe:
httpGet: httpGet:

@ -55,7 +55,13 @@ func ValidateAgainstSchema(chrt *chart.Chart, values map[string]interface{}) err
} }
// ValidateAgainstSingleSchema checks that values does not violate the structure laid out in this schema // ValidateAgainstSingleSchema checks that values does not violate the structure laid out in this schema
func ValidateAgainstSingleSchema(values Values, schemaJSON []byte) error { func ValidateAgainstSingleSchema(values Values, schemaJSON []byte) (reterr error) {
defer func() {
if r := recover(); r != nil {
reterr = fmt.Errorf("unable to validate schema: %s", r)
}
}()
valuesData, err := yaml.Marshal(values) valuesData, err := yaml.Marshal(values)
if err != nil { if err != nil {
return err return err

@ -38,6 +38,30 @@ func TestValidateAgainstSingleSchema(t *testing.T) {
} }
} }
func TestValidateAgainstInvalidSingleSchema(t *testing.T) {
values, err := ReadValuesFile("./testdata/test-values.yaml")
if err != nil {
t.Fatalf("Error reading YAML file: %s", err)
}
schema, err := ioutil.ReadFile("./testdata/test-values-invalid.schema.json")
if err != nil {
t.Fatalf("Error reading YAML file: %s", err)
}
var errString string
if err := ValidateAgainstSingleSchema(values, schema); err == nil {
t.Fatalf("Expected an error, but got nil")
} else {
errString = err.Error()
}
expectedErrString := "unable to validate schema: runtime error: invalid " +
"memory address or nil pointer dereference"
if errString != expectedErrString {
t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString)
}
}
func TestValidateAgainstSingleSchemaNegative(t *testing.T) { func TestValidateAgainstSingleSchemaNegative(t *testing.T) {
values, err := ReadValuesFile("./testdata/test-values-negative.yaml") values, err := ReadValuesFile("./testdata/test-values-negative.yaml")
if err != nil { if err != nil {

@ -24,6 +24,7 @@ package cli
import ( import (
"fmt" "fmt"
"net/http"
"os" "os"
"strconv" "strconv"
"strings" "strings"
@ -116,6 +117,9 @@ func New() *EnvSettings {
ImpersonateGroup: &env.KubeAsGroups, ImpersonateGroup: &env.KubeAsGroups,
WrapConfigFn: func(config *rest.Config) *rest.Config { WrapConfigFn: func(config *rest.Config) *rest.Config {
config.Burst = env.BurstLimit config.Burst = env.BurstLimit
config.Wrap(func(rt http.RoundTripper) http.RoundTripper {
return &retryingRoundTripper{wrapped: rt}
})
return config return config
}, },
} }

@ -0,0 +1,77 @@
/*
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 cli
import (
"bytes"
"encoding/json"
"io"
"net/http"
"strings"
)
type retryingRoundTripper struct {
wrapped http.RoundTripper
}
func (rt *retryingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return rt.roundTrip(req, 1, nil)
}
func (rt *retryingRoundTripper) roundTrip(req *http.Request, retry int, prevResp *http.Response) (*http.Response, error) {
if retry < 0 {
return prevResp, nil
}
resp, rtErr := rt.wrapped.RoundTrip(req)
if rtErr != nil {
return resp, rtErr
}
if resp.Header.Get("content-type") != "application/json" {
return resp, rtErr
}
b, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return resp, rtErr
}
var ke kubernetesError
r := bytes.NewReader(b)
err = json.NewDecoder(r).Decode(&ke)
r.Seek(0, io.SeekStart)
resp.Body = io.NopCloser(r)
if err != nil {
return resp, rtErr
}
if ke.Code < 500 {
return resp, rtErr
}
// Matches messages like "etcdserver: leader changed"
if strings.HasSuffix(ke.Message, "etcdserver: leader changed") {
return rt.roundTrip(req, retry-1, resp)
}
// Matches messages like "rpc error: code = Unknown desc = raft proposal dropped"
if strings.HasSuffix(ke.Message, "raft proposal dropped") {
return rt.roundTrip(req, retry-1, resp)
}
return resp, rtErr
}
type kubernetesError struct {
Message string `json:"message"`
Code int `json:"code"`
}

@ -29,17 +29,18 @@ import (
"helm.sh/helm/v3/pkg/strvals" "helm.sh/helm/v3/pkg/strvals"
) )
// Options captures the different ways to specify values
type Options struct { type Options struct {
ValueFiles []string ValueFiles []string // -f/--values
StringValues []string StringValues []string // --set-string
Values []string Values []string // --set
FileValues []string FileValues []string // --set-file
JSONValues []string JSONValues []string // --set-json
LiteralValues []string LiteralValues []string // --set-literal
} }
// MergeValues merges values from files specified via -f/--values and directly // MergeValues merges values from files specified via -f/--values and directly
// via --set, --set-string, or --set-file, marshaling them to YAML // via --set-json, --set, --set-string, or --set-file, marshaling them to YAML
func (opts *Options) MergeValues(p getter.Providers) (map[string]interface{}, error) { func (opts *Options) MergeValues(p getter.Providers) (map[string]interface{}, error) {
base := map[string]interface{}{} base := map[string]interface{}{}

@ -294,31 +294,13 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, er
} }
// TODO: Seems that picking first URL is not fully correct // TODO: Seems that picking first URL is not fully correct
u, err = url.Parse(cv.URLs[0]) resolvedURL, err := repo.ResolveReferenceURL(rc.URL, cv.URLs[0])
if err != nil { if err != nil {
return u, errors.Errorf("invalid chart URL format: %s", ref) return u, errors.Errorf("invalid chart URL format: %s", ref)
} }
// If the URL is relative (no scheme), prepend the chart repo's base URL return url.Parse(resolvedURL)
if !u.IsAbs() {
repoURL, err := url.Parse(rc.URL)
if err != nil {
return repoURL, err
}
q := repoURL.Query()
// We need a trailing slash for ResolveReference to work, but make sure there isn't already one
repoURL.Path = strings.TrimSuffix(repoURL.Path, "/") + "/"
u = repoURL.ResolveReference(u)
u.RawQuery = q.Encode()
// TODO add user-agent
if _, err := getter.NewHTTPGetter(getter.WithURL(rc.URL)); err != nil {
return repoURL, err
}
return u, err
}
// TODO add user-agent
return u, nil
} }
// VerifyChart takes a path to a chart archive and a keyring, and verifies the chart. // VerifyChart takes a path to a chart archive and a keyring, and verifies the chart.

@ -48,6 +48,7 @@ func TestResolveChartRef(t *testing.T) {
{name: "reference, testing-relative repo", ref: "testing-relative/bar", expect: "http://example.com/helm/bar-1.2.3.tgz"}, {name: "reference, testing-relative repo", ref: "testing-relative/bar", expect: "http://example.com/helm/bar-1.2.3.tgz"},
{name: "reference, testing-relative-trailing-slash repo", ref: "testing-relative-trailing-slash/foo", expect: "http://example.com/helm/charts/foo-1.2.3.tgz"}, {name: "reference, testing-relative-trailing-slash repo", ref: "testing-relative-trailing-slash/foo", expect: "http://example.com/helm/charts/foo-1.2.3.tgz"},
{name: "reference, testing-relative-trailing-slash repo", ref: "testing-relative-trailing-slash/bar", expect: "http://example.com/helm/bar-1.2.3.tgz"}, {name: "reference, testing-relative-trailing-slash repo", ref: "testing-relative-trailing-slash/bar", expect: "http://example.com/helm/bar-1.2.3.tgz"},
{name: "encoded URL", ref: "encoded-url/foobar", expect: "http://example.com/with%2Fslash/charts/foobar-4.2.1.tgz"},
{name: "full URL, HTTPS, irrelevant version", ref: "https://example.com/foo-1.2.3.tgz", version: "0.1.0", expect: "https://example.com/foo-1.2.3.tgz", fail: true}, {name: "full URL, HTTPS, irrelevant version", ref: "https://example.com/foo-1.2.3.tgz", version: "0.1.0", expect: "https://example.com/foo-1.2.3.tgz", fail: true},
{name: "full URL, file", ref: "file:///foo-1.2.3.tgz", fail: true}, {name: "full URL, file", ref: "file:///foo-1.2.3.tgz", fail: true},
{name: "invalid", ref: "invalid-1.2.3", fail: true}, {name: "invalid", ref: "invalid-1.2.3", fail: true},

@ -24,3 +24,5 @@ repositories:
- name: testing-https-insecureskip-tls-verify - name: testing-https-insecureskip-tls-verify
url: "https://example-https-insecureskiptlsverify.com" url: "https://example-https-insecureskiptlsverify.com"
insecure_skip_tls_verify: true insecure_skip_tls_verify: true
- name: encoded-url
url: "http://example.com/with%2Fslash"

@ -0,0 +1,15 @@
apiVersion: v1
entries:
foobar:
- name: foobar
description: Foo Chart With Encoded URL
home: https://helm.sh/helm
keywords: []
maintainers: []
sources:
- https://github.com/helm/charts
urls:
- charts/foobar-4.2.1.tgz
version: 4.2.1
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
apiVersion: v2

@ -77,7 +77,7 @@ func NewLookupFunction(config *rest.Config) lookupFunc {
} }
} }
// getDynamicClientOnUnstructured returns a dynamic client on an Unstructured type. This client can be further namespaced. // getDynamicClientOnKind returns a dynamic client on an Unstructured type. This client can be further namespaced.
func getDynamicClientOnKind(apiversion string, kind string, config *rest.Config) (dynamic.NamespaceableResourceInterface, bool, error) { func getDynamicClientOnKind(apiversion string, kind string, config *rest.Config) (dynamic.NamespaceableResourceInterface, bool, error) {
gvk := schema.FromAPIVersionAndKind(apiversion, kind) gvk := schema.FromAPIVersionAndKind(apiversion, kind)
apiRes, err := getAPIResourceForGVK(gvk, config) apiRes, err := getAPIResourceForGVK(gvk, config)

@ -17,12 +17,14 @@ limitations under the License.
package kube // import "helm.sh/helm/v3/pkg/kube" package kube // import "helm.sh/helm/v3/pkg/kube"
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"reflect"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -38,7 +40,9 @@ import (
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
"k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/apimachinery/pkg/util/strategicpatch"
@ -47,6 +51,7 @@ import (
"k8s.io/cli-runtime/pkg/resource" "k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
cachetools "k8s.io/client-go/tools/cache" cachetools "k8s.io/client-go/tools/cache"
watchtools "k8s.io/client-go/tools/watch" watchtools "k8s.io/client-go/tools/watch"
cmdutil "k8s.io/kubectl/pkg/cmd/util" cmdutil "k8s.io/kubectl/pkg/cmd/util"
@ -132,6 +137,111 @@ func (c *Client) Create(resources ResourceList) (*Result, error) {
return &Result{Created: resources}, nil return &Result{Created: resources}, nil
} }
func transformRequests(req *rest.Request) {
tableParam := strings.Join([]string{
fmt.Sprintf("application/json;as=Table;v=%s;g=%s", metav1.SchemeGroupVersion.Version, metav1.GroupName),
fmt.Sprintf("application/json;as=Table;v=%s;g=%s", metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName),
"application/json",
}, ",")
req.SetHeader("Accept", tableParam)
// if sorting, ensure we receive the full object in order to introspect its fields via jsonpath
req.Param("includeObject", "Object")
}
func (c *Client) Get(resources ResourceList, reader io.Reader) (map[string][]runtime.Object, error) {
buf := new(bytes.Buffer)
objs := make(map[string][]runtime.Object)
podSelectors := []map[string]string{}
err := resources.Visit(func(info *resource.Info, err error) error {
if err != nil {
return err
}
gvk := info.ResourceMapping().GroupVersionKind
vk := gvk.Version + "/" + gvk.Kind
obj, err := getResource(info)
if err != nil {
fmt.Fprintf(buf, "Get resource %s failed, err:%v\n", info.Name, err)
} else {
objs[vk] = append(objs[vk], obj)
objs, err = c.getSelectRelationPod(info, objs, &podSelectors)
if err != nil {
c.Log("Warning: get the relation pod is failed, err:%s", err.Error())
}
}
return nil
})
if err != nil {
return nil, err
}
return objs, nil
}
func (c *Client) getSelectRelationPod(info *resource.Info, objs map[string][]runtime.Object, podSelectors *[]map[string]string) (map[string][]runtime.Object, error) {
if info == nil {
return objs, nil
}
c.Log("get relation pod of object: %s/%s/%s", info.Namespace, info.Mapping.GroupVersionKind.Kind, info.Name)
selector, ok, _ := getSelectorFromObject(info.Object)
if !ok {
return objs, nil
}
for index := range *podSelectors {
if reflect.DeepEqual((*podSelectors)[index], selector) {
// check if pods for selectors are already added. This avoids duplicate printing of pods
return objs, nil
}
}
*podSelectors = append(*podSelectors, selector)
infos, err := c.Factory.NewBuilder().
Unstructured().
ContinueOnError().
NamespaceParam(info.Namespace).
DefaultNamespace().
ResourceTypes("pods").
LabelSelector(labels.Set(selector).AsSelector().String()).
TransformRequests(transformRequests).
Do().Infos()
if err != nil {
return objs, err
}
vk := "v1/Pod(related)"
for _, info := range infos {
objs[vk] = append(objs[vk], info.Object)
}
return objs, nil
}
func getSelectorFromObject(obj runtime.Object) (map[string]string, bool, error) {
typed := obj.(*unstructured.Unstructured)
kind := typed.Object["kind"]
switch kind {
case "ReplicaSet", "Deployment", "StatefulSet", "DaemonSet", "Job":
return unstructured.NestedStringMap(typed.Object, "spec", "selector", "matchLabels")
case "ReplicationController":
return unstructured.NestedStringMap(typed.Object, "spec", "selector")
default:
return nil, false, nil
}
}
func getResource(info *resource.Info) (runtime.Object, error) {
obj, err := resource.NewHelper(info.Client, info.Mapping).Get(info.Namespace, info.Name)
if err != nil {
return nil, err
}
return obj, nil
}
// Wait waits up to the given timeout for the specified resources to be ready. // Wait waits up to the given timeout for the specified resources to be ready.
func (c *Client) Wait(resources ResourceList, timeout time.Duration) error { func (c *Client) Wait(resources ResourceList, timeout time.Duration) error {
cs, err := c.getKubeClient() cs, err := c.getKubeClient()
@ -207,11 +317,21 @@ func (c *Client) Build(reader io.Reader, validate bool) (ResourceList, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
result, err := c.newBuilder(). var result ResourceList
Unstructured(). if validate {
Schema(schema). result, err = c.newBuilder().
Stream(reader, ""). Unstructured().
Do().Infos() Schema(schema).
Stream(reader, "").
Do().Infos()
} else {
result, err = c.newBuilder().
Unstructured().
Schema(schema).
Stream(reader, "").
TransformRequests(transformRequests).
Do().Infos()
}
return result, scrubValidationError(err) return result, scrubValidationError(err)
} }
@ -308,16 +428,20 @@ func (c *Client) Delete(resources ResourceList) (*Result, []error) {
mtx := sync.Mutex{} mtx := sync.Mutex{}
err := perform(resources, func(info *resource.Info) error { err := perform(resources, func(info *resource.Info) error {
c.Log("Starting delete for %q %s", info.Name, info.Mapping.GroupVersionKind.Kind) c.Log("Starting delete for %q %s", info.Name, info.Mapping.GroupVersionKind.Kind)
if err := c.skipIfNotFound(deleteResource(info)); err != nil { err := deleteResource(info)
mtx.Lock() if err == nil || apierrors.IsNotFound(err) {
defer mtx.Unlock() if err != nil {
// Collect the error and continue on c.Log("Ignoring delete failure for %q %s: %v", info.Name, info.Mapping.GroupVersionKind, err)
errs = append(errs, err) }
} else {
mtx.Lock() mtx.Lock()
defer mtx.Unlock() defer mtx.Unlock()
res.Deleted = append(res.Deleted, info) res.Deleted = append(res.Deleted, info)
return nil
} }
mtx.Lock()
defer mtx.Unlock()
// Collect the error and continue on
errs = append(errs, err)
return nil return nil
}) })
if err != nil { if err != nil {
@ -334,14 +458,6 @@ func (c *Client) Delete(resources ResourceList) (*Result, []error) {
return res, nil return res, nil
} }
func (c *Client) skipIfNotFound(err error) error {
if apierrors.IsNotFound(err) {
c.Log("%v", err)
return nil
}
return err
}
func (c *Client) watchTimeout(t time.Duration) func(*resource.Info) error { func (c *Client) watchTimeout(t time.Duration) func(*resource.Info) error {
return func(info *resource.Info) error { return func(info *resource.Info) error {
return c.watchUntilReady(t, info) return c.watchUntilReady(t, info)

@ -22,6 +22,7 @@ import (
"time" "time"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/resource" "k8s.io/cli-runtime/pkg/resource"
"helm.sh/helm/v3/pkg/kube" "helm.sh/helm/v3/pkg/kube"
@ -47,6 +48,14 @@ func (p *PrintingKubeClient) Create(resources kube.ResourceList) (*kube.Result,
return &kube.Result{Created: resources}, nil return &kube.Result{Created: resources}, nil
} }
func (p *PrintingKubeClient) Get(resources kube.ResourceList, reader io.Reader) (map[string][]runtime.Object, error) {
_, err := io.Copy(p.Out, bufferize(resources))
if err != nil {
return nil, err
}
return make(map[string][]runtime.Object), nil
}
func (p *PrintingKubeClient) Wait(resources kube.ResourceList, _ time.Duration) error { func (p *PrintingKubeClient) Wait(resources kube.ResourceList, _ time.Duration) error {
_, err := io.Copy(p.Out, bufferize(resources)) _, err := io.Copy(p.Out, bufferize(resources))
return err return err

@ -21,6 +21,7 @@ import (
"time" "time"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
) )
// Interface represents a client capable of communicating with the Kubernetes API. // Interface represents a client capable of communicating with the Kubernetes API.
@ -78,5 +79,14 @@ type InterfaceExt interface {
WaitForDelete(resources ResourceList, timeout time.Duration) error WaitForDelete(resources ResourceList, timeout time.Duration) error
} }
// InterfaceResources is introduced to avoid breaking backwards compatibility for Interface implementers.
//
// TODO Helm 4: Remove InterfaceResources and integrate its method(s) into the Interface.
type InterfaceResources interface {
// Get details of deployed resources in ResourceList to be printed.
Get(resources ResourceList, reader io.Reader) (map[string][]runtime.Object, error)
}
var _ Interface = (*Client)(nil) var _ Interface = (*Client)(nil)
var _ InterfaceExt = (*Client)(nil) var _ InterfaceExt = (*Client)(nil)
var _ InterfaceResources = (*Client)(nil)

@ -162,13 +162,13 @@ func (i HTTPInstaller) Path() string {
return helmpath.DataPath("plugins", i.PluginName) return helmpath.DataPath("plugins", i.PluginName)
} }
// CleanJoin resolves dest as a subpath of root. // cleanJoin resolves dest as a subpath of root.
// //
// This function runs several security checks on the path, generating an error if // This function runs several security checks on the path, generating an error if
// the supplied `dest` looks suspicious or would result in dubious behavior on the // the supplied `dest` looks suspicious or would result in dubious behavior on the
// filesystem. // filesystem.
// //
// CleanJoin assumes that any attempt by `dest` to break out of the CWD is an attempt // cleanJoin assumes that any attempt by `dest` to break out of the CWD is an attempt
// to be malicious. (If you don't care about this, use the securejoin-filepath library.) // to be malicious. (If you don't care about this, use the securejoin-filepath library.)
// It will emit an error if it detects paths that _look_ malicious, operating on the // It will emit an error if it detects paths that _look_ malicious, operating on the
// assumption that we don't actually want to do anything with files that already // assumption that we don't actually want to do anything with files that already

@ -22,6 +22,9 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
// ErrPluginNotAFolder indicates that the plugin path is not a folder.
var ErrPluginNotAFolder = errors.New("expected plugin to be a folder")
// LocalInstaller installs plugins from the filesystem. // LocalInstaller installs plugins from the filesystem.
type LocalInstaller struct { type LocalInstaller struct {
base base
@ -43,6 +46,14 @@ func NewLocalInstaller(source string) (*LocalInstaller, error) {
// //
// Implements Installer. // Implements Installer.
func (i *LocalInstaller) Install() error { func (i *LocalInstaller) Install() error {
stat, err := os.Stat(i.Source)
if err != nil {
return err
}
if !stat.IsDir() {
return ErrPluginNotAFolder
}
if !isPlugin(i.Source) { if !isPlugin(i.Source) {
return ErrMissingMetadata return ErrMissingMetadata
} }

@ -48,3 +48,19 @@ func TestLocalInstaller(t *testing.T) {
} }
defer os.RemoveAll(filepath.Dir(helmpath.DataPath())) // helmpath.DataPath is like /tmp/helm013130971/helm defer os.RemoveAll(filepath.Dir(helmpath.DataPath())) // helmpath.DataPath is like /tmp/helm013130971/helm
} }
func TestLocalInstallerNotAFolder(t *testing.T) {
source := "../testdata/plugdir/good/echo/plugin.yaml"
i, err := NewForSource(source, "")
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
err = Install(i)
if err == nil {
t.Fatal("expected error")
}
if err != ErrPluginNotAFolder {
t.Fatalf("expected error to equal: %q", err)
}
}

@ -16,6 +16,8 @@ limitations under the License.
package release package release
import ( import (
"k8s.io/apimachinery/pkg/runtime"
"helm.sh/helm/v3/pkg/time" "helm.sh/helm/v3/pkg/time"
) )
@ -33,4 +35,6 @@ type Info struct {
Status Status `json:"status,omitempty"` Status Status `json:"status,omitempty"`
// Contains the rendered templates/NOTES.txt if available // Contains the rendered templates/NOTES.txt if available
Notes string `json:"notes,omitempty"` Notes string `json:"notes,omitempty"`
// Contains the deployed resources information
Resources map[string][]runtime.Object `json:"resources,omitempty"`
} }

@ -25,7 +25,6 @@ import (
"log" "log"
"net/url" "net/url"
"os" "os"
"path"
"path/filepath" "path/filepath"
"strings" "strings"
@ -116,14 +115,11 @@ func (r *ChartRepository) Load() error {
// DownloadIndexFile fetches the index from a repository. // DownloadIndexFile fetches the index from a repository.
func (r *ChartRepository) DownloadIndexFile() (string, error) { func (r *ChartRepository) DownloadIndexFile() (string, error) {
parsedURL, err := url.Parse(r.Config.URL) indexURL, err := ResolveReferenceURL(r.Config.URL, "index.yaml")
if err != nil { if err != nil {
return "", err return "", err
} }
parsedURL.RawPath = path.Join(parsedURL.RawPath, "index.yaml")
parsedURL.Path = path.Join(parsedURL.Path, "index.yaml")
indexURL := parsedURL.String()
// TODO add user-agent // TODO add user-agent
resp, err := r.Client.Get(indexURL, resp, err := r.Client.Get(indexURL,
getter.WithURL(r.Config.URL), getter.WithURL(r.Config.URL),
@ -219,7 +215,7 @@ func FindChartInAuthRepoURL(repoURL, username, password, chartName, chartVersion
// but it also receives credentials and TLS verify flag for the chart repository. // but it also receives credentials and TLS verify flag for the chart repository.
// TODO Helm 4, FindChartInAuthAndTLSRepoURL should be integrated into FindChartInAuthRepoURL. // TODO Helm 4, FindChartInAuthAndTLSRepoURL should be integrated into FindChartInAuthRepoURL.
func FindChartInAuthAndTLSRepoURL(repoURL, username, password, chartName, chartVersion, certFile, keyFile, caFile string, insecureSkipTLSverify bool, getters getter.Providers) (string, error) { func FindChartInAuthAndTLSRepoURL(repoURL, username, password, chartName, chartVersion, certFile, keyFile, caFile string, insecureSkipTLSverify bool, getters getter.Providers) (string, error) {
return FindChartInAuthAndTLSAndPassRepoURL(repoURL, username, password, chartName, chartVersion, certFile, keyFile, caFile, false, false, getters) return FindChartInAuthAndTLSAndPassRepoURL(repoURL, username, password, chartName, chartVersion, certFile, keyFile, caFile, insecureSkipTLSverify, false, getters)
} }
// FindChartInAuthAndTLSAndPassRepoURL finds chart in chart repository pointed by repoURL // FindChartInAuthAndTLSAndPassRepoURL finds chart in chart repository pointed by repoURL
@ -253,6 +249,10 @@ func FindChartInAuthAndTLSAndPassRepoURL(repoURL, username, password, chartName,
if err != nil { if err != nil {
return "", errors.Wrapf(err, "looks like %q is not a valid chart repository or cannot be reached", repoURL) return "", errors.Wrapf(err, "looks like %q is not a valid chart repository or cannot be reached", repoURL)
} }
defer func() {
os.RemoveAll(filepath.Join(r.CachePath, helmpath.CacheChartsFile(r.Config.Name)))
os.RemoveAll(filepath.Join(r.CachePath, helmpath.CacheIndexFile(r.Config.Name)))
}()
// Read the index file for the repository to get chart information and return chart URL // Read the index file for the repository to get chart information and return chart URL
repoIndex, err := LoadIndexFile(idx) repoIndex, err := LoadIndexFile(idx)
@ -286,18 +286,27 @@ func FindChartInAuthAndTLSAndPassRepoURL(repoURL, username, password, chartName,
// ResolveReferenceURL resolves refURL relative to baseURL. // ResolveReferenceURL resolves refURL relative to baseURL.
// If refURL is absolute, it simply returns refURL. // If refURL is absolute, it simply returns refURL.
func ResolveReferenceURL(baseURL, refURL string) (string, error) { func ResolveReferenceURL(baseURL, refURL string) (string, error) {
// We need a trailing slash for ResolveReference to work, but make sure there isn't already one parsedRefURL, err := url.Parse(refURL)
parsedBaseURL, err := url.Parse(strings.TrimSuffix(baseURL, "/") + "/")
if err != nil { if err != nil {
return "", errors.Wrapf(err, "failed to parse %s as URL", baseURL) return "", errors.Wrapf(err, "failed to parse %s as URL", refURL)
} }
parsedRefURL, err := url.Parse(refURL) if parsedRefURL.IsAbs() {
return refURL, nil
}
parsedBaseURL, err := url.Parse(baseURL)
if err != nil { if err != nil {
return "", errors.Wrapf(err, "failed to parse %s as URL", refURL) return "", errors.Wrapf(err, "failed to parse %s as URL", baseURL)
} }
return parsedBaseURL.ResolveReference(parsedRefURL).String(), nil // We need a trailing slash for ResolveReference to work, but make sure there isn't already one
parsedBaseURL.RawPath = strings.TrimSuffix(parsedBaseURL.RawPath, "/") + "/"
parsedBaseURL.Path = strings.TrimSuffix(parsedBaseURL.Path, "/") + "/"
resolvedURL := parsedBaseURL.ResolveReference(parsedRefURL)
resolvedURL.RawQuery = parsedBaseURL.RawQuery
return resolvedURL.String(), nil
} }
func (e *Entry) String() string { func (e *Entry) String() string {

@ -385,35 +385,21 @@ func TestErrorFindChartInRepoURL(t *testing.T) {
} }
func TestResolveReferenceURL(t *testing.T) { func TestResolveReferenceURL(t *testing.T) {
chartURL, err := ResolveReferenceURL("http://localhost:8123/charts/", "nginx-0.2.0.tgz") for _, tt := range []struct {
if err != nil { baseURL, refURL, chartURL string
t.Errorf("%s", err) }{
} {"http://localhost:8123/charts/", "nginx-0.2.0.tgz", "http://localhost:8123/charts/nginx-0.2.0.tgz"},
if chartURL != "http://localhost:8123/charts/nginx-0.2.0.tgz" { {"http://localhost:8123/charts-with-no-trailing-slash", "nginx-0.2.0.tgz", "http://localhost:8123/charts-with-no-trailing-slash/nginx-0.2.0.tgz"},
t.Errorf("%s", chartURL) {"http://localhost:8123", "https://charts.helm.sh/stable/nginx-0.2.0.tgz", "https://charts.helm.sh/stable/nginx-0.2.0.tgz"},
} {"http://localhost:8123/charts%2fwith%2fescaped%2fslash", "nginx-0.2.0.tgz", "http://localhost:8123/charts%2fwith%2fescaped%2fslash/nginx-0.2.0.tgz"},
{"http://localhost:8123/charts?with=queryparameter", "nginx-0.2.0.tgz", "http://localhost:8123/charts/nginx-0.2.0.tgz?with=queryparameter"},
chartURL, err = ResolveReferenceURL("http://localhost:8123/charts-with-no-trailing-slash", "nginx-0.2.0.tgz") } {
if err != nil { chartURL, err := ResolveReferenceURL(tt.baseURL, tt.refURL)
t.Errorf("%s", err) if err != nil {
} t.Errorf("unexpected error in ResolveReferenceURL(%q, %q): %s", tt.baseURL, tt.refURL, err)
if chartURL != "http://localhost:8123/charts-with-no-trailing-slash/nginx-0.2.0.tgz" { }
t.Errorf("%s", chartURL) if chartURL != tt.chartURL {
} t.Errorf("expected ResolveReferenceURL(%q, %q) to equal %q, got %q", tt.baseURL, tt.refURL, tt.chartURL, chartURL)
}
chartURL, err = ResolveReferenceURL("http://localhost:8123", "https://charts.helm.sh/stable/nginx-0.2.0.tgz")
if err != nil {
t.Errorf("%s", err)
}
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)
} }
} }

@ -118,6 +118,10 @@ func LoadIndexFile(path string) (*IndexFile, error) {
// MustAdd adds a file to the index // MustAdd adds a file to the index
// This can leave the index in an unsorted state // This can leave the index in an unsorted state
func (i IndexFile) MustAdd(md *chart.Metadata, filename, baseURL, digest string) error { func (i IndexFile) MustAdd(md *chart.Metadata, filename, baseURL, digest string) error {
if i.Entries == nil {
return errors.New("entries not initialized")
}
if md.APIVersion == "" { if md.APIVersion == "" {
md.APIVersion = chart.APIVersionV1 md.APIVersion = chart.APIVersionV1
} }
@ -339,6 +343,10 @@ func loadIndex(data []byte, source string) (*IndexFile, error) {
for name, cvs := range i.Entries { for name, cvs := range i.Entries {
for idx := len(cvs) - 1; idx >= 0; idx-- { for idx := len(cvs) - 1; idx >= 0; idx-- {
if cvs[idx] == nil {
log.Printf("skipping loading invalid entry for chart %q from %s: empty entry", name, source)
continue
}
if cvs[idx].APIVersion == "" { if cvs[idx].APIVersion == "" {
cvs[idx].APIVersion = chart.APIVersionV1 cvs[idx].APIVersion = chart.APIVersionV1
} }

@ -59,6 +59,15 @@ entries:
version: 1.0.0 version: 1.0.0
home: https://github.com/something home: https://github.com/something
digest: "sha256:1234567890abcdef" digest: "sha256:1234567890abcdef"
`
indexWithEmptyEntry = `
apiVersion: v1
entries:
grafana:
- apiVersion: v2
name: grafana
foo:
-
` `
) )
@ -152,6 +161,12 @@ func TestLoadIndex_Duplicates(t *testing.T) {
} }
} }
func TestLoadIndex_EmptyEntry(t *testing.T) {
if _, err := loadIndex([]byte(indexWithEmptyEntry), "indexWithEmptyEntry"); err != nil {
t.Errorf("unexpected error: %s", err)
}
}
func TestLoadIndex_Empty(t *testing.T) { func TestLoadIndex_Empty(t *testing.T) {
if _, err := loadIndex([]byte(""), "indexWithEmpty"); err == nil { if _, err := loadIndex([]byte(""), "indexWithEmpty"); err == nil {
t.Errorf("Expected an error when index.yaml is empty.") t.Errorf("Expected an error when index.yaml is empty.")
@ -526,3 +541,21 @@ func TestIndexWrite(t *testing.T) {
t.Fatal("Index files doesn't contain expected content") t.Fatal("Index files doesn't contain expected content")
} }
} }
func TestAddFileIndexEntriesNil(t *testing.T) {
i := NewIndexFile()
i.APIVersion = chart.APIVersionV1
i.Entries = nil
for _, x := range []struct {
md *chart.Metadata
filename string
baseURL string
digest string
}{
{&chart.Metadata{APIVersion: "v2", Name: " ", Version: "8033-5.apinie+s.r"}, "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("expected err to be non-nil when entries not initialized")
}
}
}

@ -100,6 +100,9 @@ func (r *File) Remove(name string) bool {
cp := []*Entry{} cp := []*Entry{}
found := false found := false
for _, rf := range r.Repositories { for _, rf := range r.Repositories {
if rf == nil {
continue
}
if rf.Name == name { if rf.Name == name {
found = true found = true
continue continue

@ -225,3 +225,34 @@ func TestRepoNotExists(t *testing.T) {
t.Errorf("expected prompt `couldn't load repositories file`") t.Errorf("expected prompt `couldn't load repositories file`")
} }
} }
func TestRemoveRepositoryInvalidEntries(t *testing.T) {
sampleRepository := NewFile()
sampleRepository.Add(
&Entry{
Name: "stable",
URL: "https://example.com/stable/charts",
},
&Entry{
Name: "incubator",
URL: "https://example.com/incubator",
},
&Entry{},
nil,
&Entry{
Name: "test",
URL: "https://example.com/test",
},
)
removeRepository := "stable"
found := sampleRepository.Remove(removeRepository)
if !found {
t.Errorf("expected repository %s not found", removeRepository)
}
found = sampleRepository.Has(removeRepository)
if found {
t.Errorf("repository %s not deleted", removeRepository)
}
}

@ -36,6 +36,10 @@ var ErrNotList = errors.New("not a list")
// The default value 65536 = 1024 * 64 // The default value 65536 = 1024 * 64
var MaxIndex = 65536 var MaxIndex = 65536
// MaxNestedNameLevel is the maximum level of nesting for a value name that
// will be allowed.
var MaxNestedNameLevel = 30
// ToYAML takes a string of arguments and converts to a YAML document. // ToYAML takes a string of arguments and converts to a YAML document.
func ToYAML(s string) (string, error) { func ToYAML(s string) (string, error) {
m, err := Parse(s) m, err := Parse(s)
@ -155,7 +159,7 @@ func newFileParser(sc *bytes.Buffer, data map[string]interface{}, reader RunesVa
func (t *parser) parse() error { func (t *parser) parse() error {
for { for {
err := t.key(t.data) err := t.key(t.data, 0)
if err == nil { if err == nil {
continue continue
} }
@ -174,7 +178,7 @@ func runeSet(r []rune) map[rune]bool {
return s return s
} }
func (t *parser) key(data map[string]interface{}) (reterr error) { func (t *parser) key(data map[string]interface{}, nestedNameLevel int) (reterr error) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
reterr = fmt.Errorf("unable to parse key: %s", r) reterr = fmt.Errorf("unable to parse key: %s", r)
@ -204,7 +208,7 @@ func (t *parser) key(data map[string]interface{}) (reterr error) {
} }
// Now we need to get the value after the ]. // Now we need to get the value after the ].
list, err = t.listItem(list, i) list, err = t.listItem(list, i, nestedNameLevel)
set(data, kk, list) set(data, kk, list)
return err return err
case last == '=': case last == '=':
@ -261,6 +265,12 @@ func (t *parser) key(data map[string]interface{}) (reterr error) {
set(data, string(k), "") set(data, string(k), "")
return errors.Errorf("key %q has no value (cannot end with ,)", string(k)) return errors.Errorf("key %q has no value (cannot end with ,)", string(k))
case last == '.': case last == '.':
// Check value name is within the maximum nested name level
nestedNameLevel++
if nestedNameLevel > MaxNestedNameLevel {
return fmt.Errorf("value name nested level is greater than maximum supported nested level of %d", MaxNestedNameLevel)
}
// First, create or find the target map. // First, create or find the target map.
inner := map[string]interface{}{} inner := map[string]interface{}{}
if _, ok := data[string(k)]; ok { if _, ok := data[string(k)]; ok {
@ -268,11 +278,13 @@ func (t *parser) key(data map[string]interface{}) (reterr error) {
} }
// Recurse // Recurse
e := t.key(inner) e := t.key(inner, nestedNameLevel)
if len(inner) == 0 { if e == nil && len(inner) == 0 {
return errors.Errorf("key map %q has no value", string(k)) return errors.Errorf("key map %q has no value", string(k))
} }
set(data, string(k), inner) if len(inner) != 0 {
set(data, string(k), inner)
}
return e return e
} }
} }
@ -322,7 +334,7 @@ func (t *parser) keyIndex() (int, error) {
return strconv.Atoi(string(v)) return strconv.Atoi(string(v))
} }
func (t *parser) listItem(list []interface{}, i int) ([]interface{}, error) { func (t *parser) listItem(list []interface{}, i, nestedNameLevel int) ([]interface{}, error) {
if i < 0 { if i < 0 {
return list, fmt.Errorf("negative %d index not allowed", i) return list, fmt.Errorf("negative %d index not allowed", i)
} }
@ -395,7 +407,7 @@ func (t *parser) listItem(list []interface{}, i int) ([]interface{}, error) {
} }
} }
// Now we need to get the value after the ]. // Now we need to get the value after the ].
list2, err := t.listItem(crtList, nextI) list2, err := t.listItem(crtList, nextI, nestedNameLevel)
if err != nil { if err != nil {
return list, err return list, err
} }
@ -414,7 +426,7 @@ func (t *parser) listItem(list []interface{}, i int) ([]interface{}, error) {
} }
// Recurse // Recurse
e := t.key(inner) e := t.key(inner, nestedNameLevel)
if e != nil { if e != nil {
return list, e return list, e
} }

@ -16,6 +16,7 @@ limitations under the License.
package strvals package strvals
import ( import (
"fmt"
"testing" "testing"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
@ -754,3 +755,64 @@ func TestToYAML(t *testing.T) {
t.Errorf("Expected %q, got %q", expect, o) t.Errorf("Expected %q, got %q", expect, o)
} }
} }
func TestParseSetNestedLevels(t *testing.T) {
var keyMultipleNestedLevels string
for i := 1; i <= MaxNestedNameLevel+2; i++ {
tmpStr := fmt.Sprintf("name%d", i)
if i <= MaxNestedNameLevel+1 {
tmpStr = tmpStr + "."
}
keyMultipleNestedLevels += tmpStr
}
tests := []struct {
str string
expect map[string]interface{}
err bool
errStr string
}{
{
"outer.middle.inner=value",
map[string]interface{}{"outer": map[string]interface{}{"middle": map[string]interface{}{"inner": "value"}}},
false,
"",
},
{
str: keyMultipleNestedLevels + "=value",
err: true,
errStr: fmt.Sprintf("value name nested level is greater than maximum supported nested level of %d",
MaxNestedNameLevel),
},
}
for _, tt := range tests {
got, err := Parse(tt.str)
if err != nil {
if tt.err {
if tt.errStr != "" {
if err.Error() != tt.errStr {
t.Errorf("Expected error: %s. Got error: %s", tt.errStr, err.Error())
}
}
continue
}
t.Fatalf("%s: %s", tt.str, err)
}
if tt.err {
t.Errorf("%s: Expected error. Got nil", tt.str)
}
y1, err := yaml.Marshal(tt.expect)
if err != nil {
t.Fatal(err)
}
y2, err := yaml.Marshal(got)
if err != nil {
t.Fatalf("Error serializing parsed value: %s", err)
}
if string(y1) != string(y2) {
t.Errorf("%s: Expected:\n%s\nGot:\n%s", tt.str, y1, y2)
}
}
}

@ -29,6 +29,7 @@ HAS_CURL="$(type "curl" &> /dev/null && echo true || echo false)"
HAS_WGET="$(type "wget" &> /dev/null && echo true || echo false)" HAS_WGET="$(type "wget" &> /dev/null && echo true || echo false)"
HAS_OPENSSL="$(type "openssl" &> /dev/null && echo true || echo false)" HAS_OPENSSL="$(type "openssl" &> /dev/null && echo true || echo false)"
HAS_GPG="$(type "gpg" &> /dev/null && echo true || echo false)" HAS_GPG="$(type "gpg" &> /dev/null && echo true || echo false)"
HAS_GIT="$(type "git" &> /dev/null && echo true || echo false)"
# initArch discovers the architecture for this system. # initArch discovers the architecture for this system.
initArch() { initArch() {
@ -97,6 +98,10 @@ verifySupported() {
exit 1 exit 1
fi fi
fi fi
if [ "${HAS_GIT}" != "true" ]; then
echo "[WARNING] Could not find git. It is required for plugin installation."
fi
} }
# checkDesiredVersion checks if the desired version is available. # checkDesiredVersion checks if the desired version is available.

Loading…
Cancel
Save