Merge branch 'main' into main

Signed-off-by: igorushi <igor.altman@gmail.com>
pull/11945/head
igorushi 2 years ago committed by GitHub
commit 16229f8014
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -5,3 +5,7 @@ updates:
directory: "/" directory: "/"
schedule: schedule:
interval: "daily" interval: "daily"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"

@ -13,9 +13,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # pin@v3.2.0 uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # pin@v3.5.3
- name: Setup Go - name: Setup Go
uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # pin@3.5.0 uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # pin@4.0.1
with: with:
go-version: '1.20' go-version: '1.20'
- name: Install golangci-lint - name: Install golangci-lint
@ -33,4 +33,4 @@ jobs:
- name: Run unit tests - name: Run unit tests
run: make test-coverage run: make test-coverage
- name: Test build - name: Test build
run: make test build run: make build

@ -35,11 +35,11 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # pin@v3.2.0 uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # pin@v3.5.3
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@959cbb7472c4d4ad70cdfe6f4976053fe48ab394 # pinv2.1.37 uses: github/codeql-action/init@1813ca74c3faaa3a2da2070b9b8a0b3e7373a0d8 # pinv2.21.0
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@ -50,7 +50,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@959cbb7472c4d4ad70cdfe6f4976053fe48ab394 # pinv2.1.37 uses: github/codeql-action/autobuild@1813ca74c3faaa3a2da2070b9b8a0b3e7373a0d8 # pinv2.21.0
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@ -64,4 +64,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@959cbb7472c4d4ad70cdfe6f4976053fe48ab394 # pinv2.1.37 uses: github/codeql-action/analyze@1813ca74c3faaa3a2da2070b9b8a0b3e7373a0d8 # pinv2.21.0

@ -18,10 +18,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # pin@v3.2.0 uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # pin@v3.5.3
- name: Setup Go - name: Setup Go
uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # pin@3.5.0 uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # pin@4.0.1
with: with:
go-version: '1.20' go-version: '1.20'
@ -49,10 +49,10 @@ jobs:
if: github.ref == 'refs/heads/main' if: github.ref == 'refs/heads/main'
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # pin@v3.2.0 uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # pin@v3.5.3
- name: Setup Go - name: Setup Go
uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # pin@3.5.0 uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # pin@4.0.1
with: with:
go-version: '1.20' go-version: '1.20'

@ -1,19 +1,19 @@
maintainers: maintainers:
- adamreese
- bacongobbler
- hickeyma - hickeyma
- joejulian
- jdolitsky - jdolitsky
- marckhouzam - marckhouzam
- mattfarina - mattfarina
- sabre1041 - sabre1041
- scottrigby - scottrigby
- SlickNik
- technosophos - technosophos
triage: triage:
- joejulian
- yxxhero - yxxhero
- zonggen - zonggen
- gjenkins8
emeritus: emeritus:
- adamreese
- bacongobbler
- fibonacci1729 - fibonacci1729
- jascott1 - jascott1
- michelleN - michelleN
@ -22,6 +22,7 @@ emeritus:
- prydonius - prydonius
- rimusz - rimusz
- seh - seh
- SlickNik
- thomastaylor312 - thomastaylor312
- vaikas-google - vaikas-google
- viglesiasce - viglesiasce

@ -48,6 +48,7 @@ func addValueOptionsFlags(f *pflag.FlagSet, v *values.Options) {
f.StringArrayVar(&v.StringValues, "set-string", []string{}, "set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") f.StringArrayVar(&v.StringValues, "set-string", []string{}, "set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")
f.StringArrayVar(&v.FileValues, "set-file", []string{}, "set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2)") f.StringArrayVar(&v.FileValues, "set-file", []string{}, "set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2)")
f.StringArrayVar(&v.JSONValues, "set-json", []string{}, "set JSON values on the command line (can specify multiple or separate values with commas: key1=jsonval1,key2=jsonval2)") f.StringArrayVar(&v.JSONValues, "set-json", []string{}, "set JSON values on the command line (can specify multiple or separate values with commas: key1=jsonval1,key2=jsonval2)")
f.StringArrayVar(&v.LiteralValues, "set-literal", []string{}, "set a literal STRING value on the command line")
} }
func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) { func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) {

@ -142,6 +142,12 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} }
client.SetRegistryClient(registryClient) client.SetRegistryClient(registryClient)
// This is for the case where "" is specifically passed in as a
// value. When there is no value passed in NoOptDefVal will be used
// and it is set to client. See addInstallFlags.
if client.DryRunOption == "" {
client.DryRunOption = "none"
}
rel, err := runInstall(args, client, valueOpts, out) rel, err := runInstall(args, client, valueOpts, out)
if err != nil { if err != nil {
return errors.Wrap(err, "INSTALLATION FAILED") return errors.Wrap(err, "INSTALLATION FAILED")
@ -160,7 +166,13 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Install, valueOpts *values.Options) { func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Install, valueOpts *values.Options) {
f.BoolVar(&client.CreateNamespace, "create-namespace", false, "create the release namespace if not present") f.BoolVar(&client.CreateNamespace, "create-namespace", false, "create the release namespace if not present")
f.BoolVar(&client.DryRun, "dry-run", false, "simulate an install") // --dry-run options with expected outcome:
// - Not set means no dry run and server is contacted.
// - Set with no value, a value of client, or a value of true and the server is not contacted
// - Set with a value of false, none, or false and the server is contacted
// The true/false part is meant to reflect some legacy behavior while none is equal to "".
f.StringVar(&client.DryRunOption, "dry-run", "", "simulate an install. If --dry-run is set with no option being specified or as '--dry-run=client', it will not attempt cluster connections. Setting '--dry-run=server' allows attempting cluster connections.")
f.Lookup("dry-run").NoOptDefVal = "client"
f.BoolVar(&client.Force, "force", false, "force resource updates through a replacement strategy") f.BoolVar(&client.Force, "force", false, "force resource updates through a replacement strategy")
f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during install") f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during install")
f.BoolVar(&client.Replace, "replace", false, "re-use the given name, only if that name is a deleted release which remains in the history. This is unsafe in production") f.BoolVar(&client.Replace, "replace", false, "re-use the given name, only if that name is a deleted release which remains in the history. This is unsafe in production")
@ -252,6 +264,7 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
RepositoryConfig: settings.RepositoryConfig, RepositoryConfig: settings.RepositoryConfig,
RepositoryCache: settings.RepositoryCache, RepositoryCache: settings.RepositoryCache,
Debug: settings.Debug, Debug: settings.Debug,
RegistryClient: client.GetRegistryClient(),
} }
if err := man.Update(); err != nil { if err := man.Update(); err != nil {
return nil, err return nil, err
@ -268,6 +281,11 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
client.Namespace = settings.Namespace() client.Namespace = settings.Namespace()
// Validate DryRunOption member is one of the allowed values
if err := validateDryRunOptionFlag(client.DryRunOption); err != nil {
return nil, err
}
// Create context and prepare the handle of SIGTERM // Create context and prepare the handle of SIGTERM
ctx := context.Background() ctx := context.Background()
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
@ -308,3 +326,19 @@ func compInstall(args []string, toComplete string, client *action.Install) ([]st
} }
return nil, cobra.ShellCompDirectiveNoFileComp return nil, cobra.ShellCompDirectiveNoFileComp
} }
func validateDryRunOptionFlag(dryRunOptionFlagValue string) error {
// Validate dry-run flag value with a set of allowed value
allowedDryRunValues := []string{"false", "true", "none", "client", "server"}
isAllowed := false
for _, v := range allowedDryRunValues {
if dryRunOptionFlagValue == v {
isAllowed = true
break
}
}
if !isAllowed {
return errors.New("Invalid dry-run flag. Flag must one of the following: false, true, none, client, server")
}
return nil
}

@ -53,6 +53,11 @@ func TestLintCmdWithQuietFlag(t *testing.T) {
name: "lint chart with warning using --quiet flag", name: "lint chart with warning using --quiet flag",
cmd: "lint --quiet testdata/testcharts/chart-with-only-crds", cmd: "lint --quiet testdata/testcharts/chart-with-only-crds",
golden: "output/lint-quiet-with-warning.txt", golden: "output/lint-quiet-with-warning.txt",
}, {
name: "lint non-existent chart using --quiet flag",
cmd: "lint --quiet thischartdoesntexist/",
golden: "",
wantError: true,
}} }}
runTestCmd(t, tests) runTestCmd(t, tests)

@ -212,7 +212,7 @@ func (o *repoAddOptions) run(out io.Writer) error {
f.Update(&c) f.Update(&c)
if err := f.WriteFile(o.repoFile, 0644); err != nil { if err := f.WriteFile(o.repoFile, 0600); err != nil {
return err return err
} }
fmt.Fprintf(out, "%q has been added to your repositories\n", o.name) fmt.Fprintf(out, "%q has been added to your repositories\n", o.name)

@ -67,7 +67,7 @@ func (o *repoRemoveOptions) run(out io.Writer) error {
if !r.Remove(name) { if !r.Remove(name) {
return errors.Errorf("no repo named %q found", name) return errors.Errorf("no repo named %q found", name)
} }
if err := r.WriteFile(o.repoFile, 0644); err != nil { if err := r.WriteFile(o.repoFile, 0600); err != nil {
return err return err
} }

@ -147,11 +147,10 @@ func (i *Index) SearchLiteral(term string, threshold int) []*Result {
term = strings.ToLower(term) term = strings.ToLower(term)
buf := []*Result{} buf := []*Result{}
for k, v := range i.lines { for k, v := range i.lines {
lk := strings.ToLower(k)
lv := strings.ToLower(v) lv := strings.ToLower(v)
res := strings.Index(lv, term) res := strings.Index(lv, term)
if score := i.calcScore(res, lv); res != -1 && score < threshold { if score := i.calcScore(res, lv); res != -1 && score < threshold {
parts := strings.Split(lk, verSep) // Remove version, if it is there. parts := strings.Split(k, verSep) // Remove version, if it is there.
buf = append(buf, &Result{Name: parts[0], Score: score, Chart: i.charts[k]}) buf = append(buf, &Result{Name: parts[0], Score: score, Chart: i.charts[k]})
} }
} }

@ -105,11 +105,11 @@ func loadTestIndex(t *testing.T, all bool) *Index {
i := NewIndex() i := NewIndex()
i.AddRepo("testing", &repo.IndexFile{Entries: indexfileEntries}, all) i.AddRepo("testing", &repo.IndexFile{Entries: indexfileEntries}, all)
i.AddRepo("ztesting", &repo.IndexFile{Entries: map[string]repo.ChartVersions{ i.AddRepo("ztesting", &repo.IndexFile{Entries: map[string]repo.ChartVersions{
"pinta": { "Pinta": {
{ {
URLs: []string{"http://example.com/charts/pinta-2.0.0.tgz"}, URLs: []string{"http://example.com/charts/pinta-2.0.0.tgz"},
Metadata: &chart.Metadata{ Metadata: &chart.Metadata{
Name: "pinta", Name: "Pinta",
Version: "2.0.0", Version: "2.0.0",
Description: "Two ship, version two", Description: "Two ship, version two",
}, },
@ -170,14 +170,14 @@ func TestSearchByName(t *testing.T) {
query: "pinta", query: "pinta",
expect: []*Result{ expect: []*Result{
{Name: "testing/pinta"}, {Name: "testing/pinta"},
{Name: "ztesting/pinta"}, {Name: "ztesting/Pinta"},
}, },
}, },
{ {
name: "repo-specific search for one result", name: "repo-specific search for one result",
query: "ztesting/pinta", query: "ztesting/pinta",
expect: []*Result{ expect: []*Result{
{Name: "ztesting/pinta"}, {Name: "ztesting/Pinta"},
}, },
}, },
{ {
@ -199,7 +199,15 @@ func TestSearchByName(t *testing.T) {
query: "two", query: "two",
expect: []*Result{ expect: []*Result{
{Name: "testing/pinta"}, {Name: "testing/pinta"},
{Name: "ztesting/pinta"}, {Name: "ztesting/Pinta"},
},
},
{
name: "search mixedCase and result should be mixedCase too",
query: "pinta",
expect: []*Result{
{Name: "testing/pinta"},
{Name: "ztesting/Pinta"},
}, },
}, },
{ {
@ -207,7 +215,7 @@ func TestSearchByName(t *testing.T) {
query: "TWO", query: "TWO",
expect: []*Result{ expect: []*Result{
{Name: "testing/pinta"}, {Name: "testing/pinta"},
{Name: "ztesting/pinta"}, {Name: "ztesting/Pinta"},
}, },
}, },
{ {

@ -79,6 +79,12 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} }
client.SetRegistryClient(registryClient) client.SetRegistryClient(registryClient)
// This is for the case where "" is specifically passed in as a
// value. When there is no value passed in NoOptDefVal will be used
// and it is set to client. See addInstallFlags.
if client.DryRunOption == "" {
client.DryRunOption = "true"
}
client.DryRun = true client.DryRun = true
client.ReleaseName = "release-name" client.ReleaseName = "release-name"
client.Replace = true // Skip the name check client.Replace = true // Skip the name check

@ -25,6 +25,8 @@ import (
var chartPath = "testdata/testcharts/subchart" var chartPath = "testdata/testcharts/subchart"
func TestTemplateCmd(t *testing.T) { func TestTemplateCmd(t *testing.T) {
deletevalchart := "testdata/testcharts/issue-9027"
tests := []cmdTestCase{ tests := []cmdTestCase{
{ {
name: "check name", name: "check name",
@ -131,6 +133,34 @@ func TestTemplateCmd(t *testing.T) {
cmd: fmt.Sprintf(`template '%s' --skip-tests`, chartPath), cmd: fmt.Sprintf(`template '%s' --skip-tests`, chartPath),
golden: "output/template-skip-tests.txt", golden: "output/template-skip-tests.txt",
}, },
{
// This test case is to ensure the case where specified dependencies
// in the Chart.yaml and those where the Chart.yaml don't have them
// specified are the same.
name: "ensure nil/null values pass to subcharts delete values",
cmd: fmt.Sprintf("template '%s'", deletevalchart),
golden: "output/issue-9027.txt",
},
{
// Ensure that imported values take precedence over parent chart values
name: "template with imported subchart values ensuring import",
cmd: fmt.Sprintf("template '%s' --set configmap.enabled=true --set subchartb.enabled=true", chartPath),
golden: "output/template-subchart-cm.txt",
},
{
// Ensure that user input values take precedence over imported
// values from sub-charts.
name: "template with imported subchart values set with --set",
cmd: fmt.Sprintf("template '%s' --set configmap.enabled=true --set subchartb.enabled=true --set configmap.value=baz", chartPath),
golden: "output/template-subchart-cm-set.txt",
},
{
// Ensure that user input values take precedence over imported
// values from sub-charts when passed by file
name: "template with imported subchart values set with --set",
cmd: fmt.Sprintf("template '%s' -f %s/extra_values.yaml", chartPath, chartPath),
golden: "output/template-subchart-cm-set-file.txt",
},
} }
runTestCmd(t, tests) runTestCmd(t, tests)
} }

@ -0,0 +1,32 @@
---
# Source: issue-9027/charts/subchart/templates/values.yaml
global:
hash:
key3: 13
key4: 4
key5: 5
key6: 6
hash:
key3: 13
key4: 4
key5: 5
key6: 6
---
# Source: issue-9027/templates/values.yaml
global:
hash:
key1: null
key2: null
key3: 13
subchart:
global:
hash:
key3: 13
key4: 4
key5: 5
key6: 6
hash:
key3: 13
key4: 4
key5: 5
key6: 6

@ -0,0 +1,122 @@
---
# Source: subchart/templates/subdir/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: subchart-sa
---
# Source: subchart/templates/subdir/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: subchart-cm
data:
value: qux
---
# Source: subchart/templates/subdir/role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: subchart-role
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","list","watch"]
---
# Source: subchart/templates/subdir/rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: subchart-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: subchart-role
subjects:
- kind: ServiceAccount
name: subchart-sa
namespace: default
---
# Source: subchart/charts/subcharta/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: subcharta
labels:
helm.sh/chart: "subcharta-0.1.0"
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 80
protocol: TCP
name: apache
selector:
app.kubernetes.io/name: subcharta
---
# Source: subchart/charts/subchartb/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: subchartb
labels:
helm.sh/chart: "subchartb-0.1.0"
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 80
protocol: TCP
name: nginx
selector:
app.kubernetes.io/name: subchartb
---
# Source: subchart/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: subchart
labels:
helm.sh/chart: "subchart-0.1.0"
app.kubernetes.io/instance: "release-name"
kube-version/major: "1"
kube-version/minor: "20"
kube-version/version: "v1.20.0"
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 80
protocol: TCP
name: nginx
selector:
app.kubernetes.io/name: subchart
---
# Source: subchart/templates/tests/test-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: "release-name-testconfig"
annotations:
"helm.sh/hook": test
data:
message: Hello World
---
# Source: subchart/templates/tests/test-nothing.yaml
apiVersion: v1
kind: Pod
metadata:
name: "release-name-test"
annotations:
"helm.sh/hook": test
spec:
containers:
- name: test
image: "alpine:latest"
envFrom:
- configMapRef:
name: "release-name-testconfig"
command:
- echo
- "$message"
restartPolicy: Never

@ -0,0 +1,122 @@
---
# Source: subchart/templates/subdir/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: subchart-sa
---
# Source: subchart/templates/subdir/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: subchart-cm
data:
value: baz
---
# Source: subchart/templates/subdir/role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: subchart-role
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","list","watch"]
---
# Source: subchart/templates/subdir/rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: subchart-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: subchart-role
subjects:
- kind: ServiceAccount
name: subchart-sa
namespace: default
---
# Source: subchart/charts/subcharta/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: subcharta
labels:
helm.sh/chart: "subcharta-0.1.0"
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 80
protocol: TCP
name: apache
selector:
app.kubernetes.io/name: subcharta
---
# Source: subchart/charts/subchartb/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: subchartb
labels:
helm.sh/chart: "subchartb-0.1.0"
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 80
protocol: TCP
name: nginx
selector:
app.kubernetes.io/name: subchartb
---
# Source: subchart/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: subchart
labels:
helm.sh/chart: "subchart-0.1.0"
app.kubernetes.io/instance: "release-name"
kube-version/major: "1"
kube-version/minor: "20"
kube-version/version: "v1.20.0"
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 80
protocol: TCP
name: nginx
selector:
app.kubernetes.io/name: subchart
---
# Source: subchart/templates/tests/test-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: "release-name-testconfig"
annotations:
"helm.sh/hook": test
data:
message: Hello World
---
# Source: subchart/templates/tests/test-nothing.yaml
apiVersion: v1
kind: Pod
metadata:
name: "release-name-test"
annotations:
"helm.sh/hook": test
spec:
containers:
- name: test
image: "alpine:latest"
envFrom:
- configMapRef:
name: "release-name-testconfig"
command:
- echo
- "$message"
restartPolicy: Never

@ -0,0 +1,122 @@
---
# Source: subchart/templates/subdir/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: subchart-sa
---
# Source: subchart/templates/subdir/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: subchart-cm
data:
value: bar
---
# Source: subchart/templates/subdir/role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: subchart-role
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","list","watch"]
---
# Source: subchart/templates/subdir/rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: subchart-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: subchart-role
subjects:
- kind: ServiceAccount
name: subchart-sa
namespace: default
---
# Source: subchart/charts/subcharta/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: subcharta
labels:
helm.sh/chart: "subcharta-0.1.0"
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 80
protocol: TCP
name: apache
selector:
app.kubernetes.io/name: subcharta
---
# Source: subchart/charts/subchartb/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: subchartb
labels:
helm.sh/chart: "subchartb-0.1.0"
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 80
protocol: TCP
name: nginx
selector:
app.kubernetes.io/name: subchartb
---
# Source: subchart/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: subchart
labels:
helm.sh/chart: "subchart-0.1.0"
app.kubernetes.io/instance: "release-name"
kube-version/major: "1"
kube-version/minor: "20"
kube-version/version: "v1.20.0"
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 80
protocol: TCP
name: nginx
selector:
app.kubernetes.io/name: subchart
---
# Source: subchart/templates/tests/test-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: "release-name-testconfig"
annotations:
"helm.sh/hook": test
data:
message: Hello World
---
# Source: subchart/templates/tests/test-nothing.yaml
apiVersion: v1
kind: Pod
metadata:
name: "release-name-test"
annotations:
"helm.sh/hook": test
spec:
containers:
- name: test
image: "alpine:latest"
envFrom:
- configMapRef:
name: "release-name-testconfig"
command:
- echo
- "$message"
restartPolicy: Never

@ -1,5 +1,5 @@
--- ---
# Source: crds/crdA.yaml # Source: subchart/crds/crdA.yaml
apiVersion: apiextensions.k8s.io/v1beta1 apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition kind: CustomResourceDefinition
metadata: metadata:

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

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

@ -1 +1 @@
Version: v3.11 Version: v3.12

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

@ -0,0 +1,6 @@
apiVersion: v2
name: issue-9027
version: 0.1.0
dependencies:
- name: subchart
version: 0.1.0

@ -0,0 +1,3 @@
apiVersion: v2
name: subchart
version: 0.1.0

@ -0,0 +1,17 @@
global:
hash:
key1: 1
key2: 2
key3: 3
key4: 4
key5: 5
key6: 6
hash:
key1: 1
key2: 2
key3: 3
key4: 4
key5: 5
key6: 6

@ -0,0 +1,11 @@
global:
hash:
key1: null
key2: null
key3: 13
subchart:
hash:
key1: null
key2: null
key3: 13

@ -29,6 +29,9 @@ dependencies:
parent: imported-chartA-B parent: imported-chartA-B
- child: exports.SCBexported2 - child: exports.SCBexported2
parent: exports.SCBexported2 parent: exports.SCBexported2
# - child: exports.configmap
# parent: configmap
- configmap
- SCBexported1 - SCBexported1
tags: tags:

@ -21,6 +21,10 @@ exports:
SCBexported2: SCBexported2:
SCBexported2A: "blaster" SCBexported2A: "blaster"
configmap:
configmap:
value: "bar"
global: global:
kolla: kolla:
nova: nova:

@ -0,0 +1,5 @@
# This file is used to test values passed by file at the command line
configmap:
enabled: true
value: "qux"

@ -0,0 +1,8 @@
{{ if .Values.configmap.enabled -}}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Chart.Name }}-cm
data:
value: {{ .Values.configmap.value }}
{{- end }}

@ -53,3 +53,7 @@ exports:
SC1exported2: SC1exported2:
all: all:
SC1exported3: "SC1expstr" SC1exported3: "SC1expstr"
configmap:
enabled: false
value: "foo"

@ -51,6 +51,10 @@ func newUninstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return compListReleases(toComplete, args, cfg) return compListReleases(toComplete, args, cfg)
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
validationErr := validateCascadeFlag(client)
if validationErr != nil {
return validationErr
}
for i := 0; i < len(args); i++ { for i := 0; i < len(args); i++ {
res, err := client.Run(args[i]) res, err := client.Run(args[i])
@ -72,8 +76,16 @@ func newUninstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during uninstallation") f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during uninstallation")
f.BoolVar(&client.KeepHistory, "keep-history", false, "remove all associated resources and mark the release as deleted, but retain the release history") f.BoolVar(&client.KeepHistory, "keep-history", false, "remove all associated resources and mark the release as deleted, but retain the release history")
f.BoolVar(&client.Wait, "wait", false, "if set, will wait until all the resources are deleted before returning. It will wait for as long as --timeout") f.BoolVar(&client.Wait, "wait", false, "if set, will wait until all the resources are deleted before returning. It will wait for as long as --timeout")
f.StringVar(&client.DeletionPropagation, "cascade", "background", "Must be \"background\", \"orphan\", or \"foreground\". Selects the deletion cascading strategy for the dependents. Defaults to background.")
f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)")
f.StringVar(&client.Description, "description", "", "add a custom description") f.StringVar(&client.Description, "description", "", "add a custom description")
return cmd return cmd
} }
func validateCascadeFlag(client *action.Uninstall) error {
if client.DeletionPropagation != "background" && client.DeletionPropagation != "foreground" && client.DeletionPropagation != "orphan" {
return fmt.Errorf("invalid cascade value (%s). Must be \"background\", \"foreground\", or \"orphan\"", client.DeletionPropagation)
}
return nil
}

@ -96,6 +96,12 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} }
client.SetRegistryClient(registryClient) client.SetRegistryClient(registryClient)
// This is for the case where "" is specifically passed in as a
// value. When there is no value passed in NoOptDefVal will be used
// and it is set to client. See addInstallFlags.
if client.DryRunOption == "" {
client.DryRunOption = "none"
}
// Fixes #7002 - Support reading values from STDIN for `upgrade` command // Fixes #7002 - Support reading values from STDIN for `upgrade` command
// Must load values AFTER determining if we have to call install so that values loaded from stdin are are not read twice // Must load values AFTER determining if we have to call install so that values loaded from stdin are are not read twice
if client.Install { if client.Install {
@ -112,6 +118,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
instClient.ChartPathOptions = client.ChartPathOptions instClient.ChartPathOptions = client.ChartPathOptions
instClient.Force = client.Force instClient.Force = client.Force
instClient.DryRun = client.DryRun instClient.DryRun = client.DryRun
instClient.DryRunOption = client.DryRunOption
instClient.DisableHooks = client.DisableHooks instClient.DisableHooks = client.DisableHooks
instClient.SkipCRDs = client.SkipCRDs instClient.SkipCRDs = client.SkipCRDs
instClient.Timeout = client.Timeout instClient.Timeout = client.Timeout
@ -146,6 +153,10 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
if err != nil { if err != nil {
return err return err
} }
// Validate dry-run flag value is one of the allowed values
if err := validateDryRunOptionFlag(client.DryRunOption); err != nil {
return err
}
p := getter.All(settings) p := getter.All(settings)
vals, err := valueOpts.MergeValues(p) vals, err := valueOpts.MergeValues(p)
@ -225,7 +236,8 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f.BoolVar(&createNamespace, "create-namespace", false, "if --install is set, create the release namespace if not present") f.BoolVar(&createNamespace, "create-namespace", false, "if --install is set, create the release namespace if not present")
f.BoolVarP(&client.Install, "install", "i", false, "if a release by this name doesn't already exist, run an install") f.BoolVarP(&client.Install, "install", "i", false, "if a release by this name doesn't already exist, run an install")
f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored") f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored")
f.BoolVar(&client.DryRun, "dry-run", false, "simulate an upgrade") f.StringVar(&client.DryRunOption, "dry-run", "", "simulate an install. If --dry-run is set with no option being specified or as '--dry-run=client', it will not attempt cluster connections. Setting '--dry-run=server' allows attempting cluster connections.")
f.Lookup("dry-run").NoOptDefVal = "client"
f.BoolVar(&client.Recreate, "recreate-pods", false, "performs pods restart for the resource if applicable") f.BoolVar(&client.Recreate, "recreate-pods", false, "performs pods restart for the resource if applicable")
f.MarkDeprecated("recreate-pods", "functionality will no longer be updated. Consult the documentation for other methods to recreate pods") f.MarkDeprecated("recreate-pods", "functionality will no longer be updated. Consult the documentation for other methods to recreate pods")
f.BoolVar(&client.Force, "force", false, "force resource updates through a replacement strategy") f.BoolVar(&client.Force, "force", false, "force resource updates through a replacement strategy")

102
go.mod

@ -5,12 +5,12 @@ go 1.19
require ( require (
github.com/BurntSushi/toml v1.2.1 github.com/BurntSushi/toml v1.2.1
github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/DATA-DOG/go-sqlmock v1.5.0
github.com/Masterminds/semver/v3 v3.2.0 github.com/Masterminds/semver/v3 v3.2.1
github.com/Masterminds/sprig/v3 v3.2.3 github.com/Masterminds/sprig/v3 v3.2.3
github.com/Masterminds/squirrel v1.5.3 github.com/Masterminds/squirrel v1.5.4
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
github.com/containerd/containerd v1.6.15 github.com/containerd/containerd v1.7.0
github.com/cyphar/filepath-securejoin v0.2.3 github.com/cyphar/filepath-securejoin v0.2.3
github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2 github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2
github.com/evanphx/json-patch v5.6.0+incompatible github.com/evanphx/json-patch v5.6.0+incompatible
@ -18,36 +18,38 @@ require (
github.com/gobwas/glob v0.2.3 github.com/gobwas/glob v0.2.3
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/hashicorp/go-multierror v1.1.1
github.com/jmoiron/sqlx v1.3.5 github.com/jmoiron/sqlx v1.3.5
github.com/lib/pq v1.10.7 github.com/lib/pq v1.10.9
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-20221205130635-1aeaba878587 github.com/moby/term v0.0.0-20221205130635-1aeaba878587
github.com/opencontainers/image-spec v1.1.0-rc2 github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b
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.3.1 github.com/rubenv/sql-migrate v1.5.1
github.com/sirupsen/logrus v1.9.0 github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.6.1 github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.1 github.com/stretchr/testify v1.8.4
github.com/xeipuuv/gojsonschema v1.2.0 github.com/xeipuuv/gojsonschema v1.2.0
golang.org/x/crypto v0.5.0 golang.org/x/crypto v0.11.0
golang.org/x/term v0.4.0 golang.org/x/term v0.10.0
golang.org/x/text v0.6.0 golang.org/x/text v0.11.0
k8s.io/api v0.26.0 k8s.io/api v0.27.3
k8s.io/apiextensions-apiserver v0.26.0 k8s.io/apiextensions-apiserver v0.27.3
k8s.io/apimachinery v0.26.0 k8s.io/apimachinery v0.27.3
k8s.io/apiserver v0.26.0 k8s.io/apiserver v0.27.3
k8s.io/cli-runtime v0.26.0 k8s.io/cli-runtime v0.27.3
k8s.io/client-go v0.26.0 k8s.io/client-go v0.27.3
k8s.io/klog/v2 v2.80.1 k8s.io/klog/v2 v2.100.1
k8s.io/kubectl v0.26.0 k8s.io/kubectl v0.27.3
oras.land/oras-go v1.2.2 oras.land/oras-go v1.2.3
sigs.k8s.io/yaml v1.3.0 sigs.k8s.io/yaml v1.3.0
) )
require ( require (
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect
@ -57,32 +59,33 @@ require (
github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd // indirect github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd // indirect
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b // indirect github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b // indirect
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 // indirect github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/cli v20.10.21+incompatible // indirect github.com/docker/cli v23.0.1+incompatible // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/docker/docker v20.10.21+incompatible // indirect github.com/docker/docker v23.0.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.7.0 // indirect github.com/docker/docker-credential-helpers v0.7.0 // indirect
github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/go-metrics v0.0.1 // indirect
github.com/docker/go-units v0.4.0 // indirect github.com/docker/go-units v0.5.0 // indirect
github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 // indirect github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 // indirect
github.com/emicklei/go-restful/v3 v3.9.0 // indirect github.com/emicklei/go-restful/v3 v3.10.1 // indirect
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.3 // indirect github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/fvbommel/sortorder 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.4.2 // indirect
github.com/go-gorp/gorp/v3 v3.0.5 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect
github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/logr v1.2.3 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/swag v0.19.14 // indirect github.com/go-openapi/jsonreference v0.20.1 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.3 // indirect
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
@ -93,17 +96,18 @@ require (
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/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/huandu/xstrings v1.4.0 // indirect github.com/huandu/xstrings v1.4.0 // indirect
github.com/imdario/mergo v0.3.13 // indirect github.com/imdario/mergo v0.3.13 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // 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.11.13 // indirect github.com/klauspost/compress v1.16.0 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/mailru/easyjson v0.7.6 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect
@ -128,30 +132,32 @@ require (
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect github.com/shopspring/decimal v1.3.1 // indirect
github.com/spf13/cast v1.5.0 // indirect github.com/spf13/cast v1.5.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xlab/treeprint v1.1.0 // indirect github.com/xlab/treeprint v1.1.0 // indirect
github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 // indirect github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 // indirect
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.opentelemetry.io/otel v1.14.0 // indirect
go.opentelemetry.io/otel/trace v1.14.0 // indirect
go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
golang.org/x/net v0.5.0 // indirect golang.org/x/net v0.10.0 // indirect
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect golang.org/x/oauth2 v0.4.0 // indirect
golang.org/x/sync v0.1.0 // indirect golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.4.0 // indirect golang.org/x/sys v0.10.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-20230306155012-7f2fa6fef1f4 // indirect
google.golang.org/grpc v1.49.0 // indirect google.golang.org/grpc v1.53.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect google.golang.org/protobuf v1.28.1 // indirect
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.26.0 // indirect k8s.io/component-base v0.27.3 // indirect
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect
k8s.io/utils v0.0.0-20221107191617-1a15be271d1d // indirect k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 // indirect
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/kustomize/api v0.12.1 // indirect sigs.k8s.io/kustomize/api v0.13.2 // indirect
sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect sigs.k8s.io/kustomize/kyaml v0.14.1 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
) )

511
go.sum

File diff suppressed because it is too large Load Diff

@ -114,7 +114,7 @@ func (c *Client) Search(term string) ([]SearchResult, error) {
p.RawQuery = "q=" + url.QueryEscape(term) p.RawQuery = "q=" + url.QueryEscape(term)
// Create request // Create request
req, err := http.NewRequest("GET", p.String(), nil) req, err := http.NewRequest(http.MethodGet, p.String(), nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }

@ -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.11" version = "v3.12"
// metadata is extra build time data // metadata is extra build time data
metadata = "" metadata = ""

@ -103,7 +103,7 @@ type Configuration struct {
// TODO: As part of the refactor the duplicate code in cmd/helm/template.go should be removed // TODO: As part of the refactor the duplicate code in cmd/helm/template.go should be removed
// //
// This code has to do with writing files to disk. // This code has to do with writing files to disk.
func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrender.PostRenderer, dryRun, enableDNS bool) ([]*release.Hook, *bytes.Buffer, string, error) { func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrender.PostRenderer, interactWithRemote, enableDNS bool) ([]*release.Hook, *bytes.Buffer, string, error) {
hs := []*release.Hook{} hs := []*release.Hook{}
b := bytes.NewBuffer(nil) b := bytes.NewBuffer(nil)
@ -121,12 +121,10 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Valu
var files map[string]string var files map[string]string
var err2 error var err2 error
// A `helm template` or `helm install --dry-run` should not talk to the remote cluster. // A `helm template` should not talk to the remote cluster. However, commands with the flag
// It will break in interesting and exotic ways because other data (e.g. discovery) //`--dry-run` with the value of `false`, `none`, or `server` should try to interact with the cluster.
// is mocked. It is not up to the template author to decide when the user wants to // It may break in interesting and exotic ways because other data (e.g. discovery) is mocked.
// connect to the cluster. So when the user says to dry run, respect the user's if interactWithRemote && cfg.RESTClientGetter != nil {
// wishes and do not connect to the cluster.
if !dryRun && cfg.RESTClientGetter != nil {
restConfig, err := cfg.RESTClientGetter.ToRESTConfig() restConfig, err := cfg.RESTClientGetter.ToRESTConfig()
if err != nil { if err != nil {
return hs, b, "", err return hs, b, "", err
@ -189,13 +187,13 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Valu
if includeCrds { if includeCrds {
for _, crd := range ch.CRDObjects() { for _, crd := range ch.CRDObjects() {
if outputDir == "" { if outputDir == "" {
fmt.Fprintf(b, "---\n# Source: %s\n%s\n", crd.Name, string(crd.File.Data[:])) fmt.Fprintf(b, "---\n# Source: %s\n%s\n", crd.Filename, string(crd.File.Data[:]))
} else { } else {
err = writeToFile(outputDir, crd.Filename, string(crd.File.Data[:]), fileWritten[crd.Name]) err = writeToFile(outputDir, crd.Filename, string(crd.File.Data[:]), fileWritten[crd.Filename])
if err != nil { if err != nil {
return hs, b, "", err return hs, b, "", err
} }
fileWritten[crd.Name] = true fileWritten[crd.Filename] = true
} }
} }
} }

@ -73,6 +73,7 @@ type Install struct {
Force bool Force bool
CreateNamespace bool CreateNamespace bool
DryRun bool DryRun bool
DryRunOption string
DisableHooks bool DisableHooks bool
Replace bool Replace bool
Wait bool Wait bool
@ -142,6 +143,11 @@ func (i *Install) SetRegistryClient(registryClient *registry.Client) {
i.ChartPathOptions.registryClient = registryClient i.ChartPathOptions.registryClient = registryClient
} }
// GetRegistryClient get the registry client.
func (i *Install) GetRegistryClient() *registry.Client {
return i.ChartPathOptions.registryClient
}
func (i *Install) installCRDs(crds []chart.CRD) error { func (i *Install) installCRDs(crds []chart.CRD) error {
// We do these one file at a time in the order they were read. // We do these one file at a time in the order they were read.
totalItems := []*resource.Info{} totalItems := []*resource.Info{}
@ -223,15 +229,20 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
return nil, err return nil, err
} }
if err := chartutil.ProcessDependencies(chrt, vals); err != nil { if err := chartutil.ProcessDependenciesWithMerge(chrt, vals); err != nil {
return nil, err return nil, err
} }
var interactWithRemote bool
if !i.isDryRun() || i.DryRunOption == "server" || i.DryRunOption == "none" || i.DryRunOption == "false" {
interactWithRemote = true
}
// Pre-install anything in the crd/ directory. We do this before Helm // Pre-install anything in the crd/ directory. We do this before Helm
// contacts the upstream server and builds the capabilities object. // contacts the upstream server and builds the capabilities object.
if crds := chrt.CRDObjects(); !i.ClientOnly && !i.SkipCRDs && len(crds) > 0 { if crds := chrt.CRDObjects(); !i.ClientOnly && !i.SkipCRDs && len(crds) > 0 {
// On dry run, bail here // On dry run, bail here
if i.DryRun { if i.isDryRun() {
i.cfg.Log("WARNING: This chart or one of its subcharts contains CRDs. Rendering may fail or contain inaccuracies.") i.cfg.Log("WARNING: This chart or one of its subcharts contains CRDs. Rendering may fail or contain inaccuracies.")
} else if err := i.installCRDs(crds); err != nil { } else if err := i.installCRDs(crds); err != nil {
return nil, err return nil, err
@ -265,7 +276,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
} }
// special case for helm template --is-upgrade // special case for helm template --is-upgrade
isUpgrade := i.IsUpgrade && i.DryRun isUpgrade := i.IsUpgrade && i.isDryRun()
options := chartutil.ReleaseOptions{ options := chartutil.ReleaseOptions{
Name: i.ReleaseName, Name: i.ReleaseName,
Namespace: i.Namespace, Namespace: i.Namespace,
@ -281,7 +292,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
rel := i.createRelease(chrt, vals) rel := i.createRelease(chrt, vals)
var manifestDoc *bytes.Buffer var manifestDoc *bytes.Buffer
rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, i.DryRun, i.EnableDNS) rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithRemote, i.EnableDNS)
// Even for errors, attach this if available // Even for errors, attach this if available
if manifestDoc != nil { if manifestDoc != nil {
rel.Manifest = manifestDoc.String() rel.Manifest = manifestDoc.String()
@ -317,12 +328,12 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
if !i.ClientOnly && !isUpgrade && len(resources) > 0 { if !i.ClientOnly && !isUpgrade && len(resources) > 0 {
toBeAdopted, err = existingResourceConflict(resources, rel.Name, rel.Namespace) toBeAdopted, err = existingResourceConflict(resources, rel.Name, rel.Namespace)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "rendered manifests contain a resource that already exists. Unable to continue with install") return nil, errors.Wrap(err, "Unable to continue with install")
} }
} }
// Bail out here if it is a dry run // Bail out here if it is a dry run
if i.DryRun { if i.isDryRun() {
rel.Info.Description = "Dry run complete" rel.Info.Description = "Dry run complete"
return rel, nil return rel, nil
} }
@ -369,13 +380,25 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
return rel, err return rel, err
} }
rChan := make(chan resultMessage) rChan := make(chan resultMessage)
ctxChan := make(chan resultMessage)
doneChan := make(chan struct{}) doneChan := make(chan struct{})
defer close(doneChan) defer close(doneChan)
go i.performInstall(rChan, rel, toBeAdopted, resources) go i.performInstall(rChan, rel, toBeAdopted, resources)
go i.handleContext(ctx, rChan, doneChan, rel) go i.handleContext(ctx, ctxChan, doneChan, rel)
result := <-rChan select {
//start preformInstall go routine case result := <-rChan:
return result.r, result.e return result.r, result.e
case result := <-ctxChan:
return result.r, result.e
}
}
// isDryRun returns true if Upgrade is set to run as a DryRun
func (i *Install) isDryRun() bool {
if i.DryRun || i.DryRunOption == "client" || i.DryRunOption == "server" || i.DryRunOption == "true" {
return true
}
return false
} }
func (i *Install) performInstall(c chan<- resultMessage, rel *release.Release, toBeAdopted kube.ResourceList, resources kube.ResourceList) { func (i *Install) performInstall(c chan<- resultMessage, rel *release.Release, toBeAdopted kube.ResourceList, resources kube.ResourceList) {
@ -491,7 +514,8 @@ func (i *Install) availableName() error {
if err := chartutil.ValidateReleaseName(start); err != nil { if err := chartutil.ValidateReleaseName(start); err != nil {
return errors.Wrapf(err, "release name %q", start) return errors.Wrapf(err, "release name %q", start)
} }
if i.DryRun { // On dry run, bail here
if i.isDryRun() {
return nil return nil
} }

@ -254,7 +254,7 @@ func TestInstallRelease_DryRun(t *testing.T) {
is.Equal(res.Info.Description, "Dry run complete") is.Equal(res.Info.Description, "Dry run complete")
} }
// Regression test for #7955: Lookup must not connect to Kubernetes on a dry-run. // Regression test for #7955
func TestInstallRelease_DryRun_Lookup(t *testing.T) { func TestInstallRelease_DryRun_Lookup(t *testing.T) {
is := assert.New(t) is := assert.New(t)
instAction := installAction(t) instAction := installAction(t)

@ -82,7 +82,7 @@ func HasWarningsOrErrors(result *LintResult) bool {
return true return true
} }
} }
return false return len(result.Errors) > 0
} }
func lintChart(path string, vals map[string]interface{}, namespace string, strict bool) (support.Linter, error) { func lintChart(path string, vals map[string]interface{}, namespace string, strict bool) (support.Linter, error) {

@ -21,6 +21,7 @@ import (
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/kube" "helm.sh/helm/v3/pkg/kube"
@ -35,12 +36,13 @@ import (
type Uninstall struct { type Uninstall struct {
cfg *Configuration cfg *Configuration
DisableHooks bool DisableHooks bool
DryRun bool DryRun bool
KeepHistory bool KeepHistory bool
Wait bool Wait bool
Timeout time.Duration DeletionPropagation string
Description string Timeout time.Duration
Description string
} }
// NewUninstall creates a new Uninstall object with the given configuration. // NewUninstall creates a new Uninstall object with the given configuration.
@ -220,7 +222,25 @@ func (u *Uninstall) deleteRelease(rel *release.Release) (kube.ResourceList, stri
return nil, "", []error{errors.Wrap(err, "unable to build kubernetes objects for delete")} return nil, "", []error{errors.Wrap(err, "unable to build kubernetes objects for delete")}
} }
if len(resources) > 0 { if len(resources) > 0 {
if kubeClient, ok := u.cfg.KubeClient.(kube.InterfaceDeletionPropagation); ok {
_, errs = kubeClient.DeleteWithPropagationPolicy(resources, parseCascadingFlag(u.cfg, u.DeletionPropagation))
return resources, kept, errs
}
_, errs = u.cfg.KubeClient.Delete(resources) _, errs = u.cfg.KubeClient.Delete(resources)
} }
return resources, kept, errs return resources, kept, errs
} }
func parseCascadingFlag(cfg *Configuration, cascadingFlag string) v1.DeletionPropagation {
switch cascadingFlag {
case "orphan":
return v1.DeletePropagationOrphan
case "foreground":
return v1.DeletePropagationForeground
case "background":
return v1.DeletePropagationBackground
default:
cfg.Log("uninstall: given cascade value: %s, defaulting to delete propagation background", cascadingFlag)
return v1.DeletePropagationBackground
}
}

@ -95,3 +95,35 @@ func TestUninstallRelease_Wait(t *testing.T) {
is.Contains(err.Error(), "U timed out") is.Contains(err.Error(), "U timed out")
is.Equal(res.Release.Info.Status, release.StatusUninstalled) is.Equal(res.Release.Info.Status, release.StatusUninstalled)
} }
func TestUninstallRelease_Cascade(t *testing.T) {
is := assert.New(t)
unAction := uninstallAction(t)
unAction.DisableHooks = true
unAction.DryRun = false
unAction.Wait = false
unAction.DeletionPropagation = "foreground"
rel := releaseStub()
rel.Name = "come-fail-away"
rel.Manifest = `{
"apiVersion": "v1",
"kind": "Secret",
"metadata": {
"name": "secret"
},
"type": "Opaque",
"data": {
"password": "password"
}
}`
unAction.cfg.Releases.Create(rel)
failer := unAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.DeleteWithPropagationError = fmt.Errorf("Uninstall with cascade failed")
failer.BuildDummy = true
unAction.cfg.KubeClient = failer
_, err := unAction.Run(rel.Name)
is.Error(err)
is.Contains(err.Error(), "failed to delete release: come-fail-away")
}

@ -71,8 +71,9 @@ type Upgrade struct {
// DisableHooks disables hook processing if set to true. // DisableHooks disables hook processing if set to true.
DisableHooks bool DisableHooks bool
// DryRun controls whether the operation is prepared, but not executed. // DryRun controls whether the operation is prepared, but not executed.
// If `true`, the upgrade is prepared but not performed.
DryRun bool DryRun bool
// DryRunOption controls whether the operation is prepared, but not executed with options on whether or not to interact with the remote cluster.
DryRunOption string
// Force will, if set to `true`, ignore certain warnings and perform the upgrade anyway. // Force will, if set to `true`, ignore certain warnings and perform the upgrade anyway.
// //
// This should be used with caution. // This should be used with caution.
@ -149,6 +150,7 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart.
if err := chartutil.ValidateReleaseName(name); err != nil { if err := chartutil.ValidateReleaseName(name); err != nil {
return nil, errors.Errorf("release name is invalid: %s", name) return nil, errors.Errorf("release name is invalid: %s", name)
} }
u.cfg.Log("preparing upgrade for %s", name) u.cfg.Log("preparing upgrade for %s", name)
currentRelease, upgradedRelease, err := u.prepareUpgrade(name, chart, vals) currentRelease, upgradedRelease, err := u.prepareUpgrade(name, chart, vals)
if err != nil { if err != nil {
@ -165,7 +167,8 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart.
if upgradedRelease.Skipped { if upgradedRelease.Skipped {
u.cfg.Log("upgrade release for %s was skipped", name) u.cfg.Log("upgrade release for %s was skipped", name)
} else if !u.DryRun { } else if !u.isDryRun() {
// Do not update for dry runs
u.cfg.Log("updating status for upgraded release for %s", name) u.cfg.Log("updating status for upgraded release for %s", name)
if err := u.cfg.Releases.Update(upgradedRelease); err != nil { if err := u.cfg.Releases.Update(upgradedRelease); err != nil {
return res, err return res, err
@ -175,6 +178,14 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart.
return res, nil return res, nil
} }
// isDryRun returns true if Upgrade is set to run as a DryRun
func (u *Upgrade) isDryRun() bool {
if u.DryRun || u.DryRunOption == "client" || u.DryRunOption == "server" || u.DryRunOption == "true" {
return true
}
return false
}
// prepareUpgrade builds an upgraded release for an upgrade operation. // prepareUpgrade builds an upgraded release for an upgrade operation.
func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, *release.Release, error) { func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, *release.Release, error) {
if chart == nil { if chart == nil {
@ -219,7 +230,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin
return nil, nil, err return nil, nil, err
} }
if err := chartutil.ProcessDependencies(chart, vals); err != nil { if err := chartutil.ProcessDependenciesWithMerge(chart, vals); err != nil {
return nil, nil, err return nil, nil, err
} }
@ -243,7 +254,13 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin
return nil, nil, err return nil, nil, err
} }
hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, u.DryRun, u.EnableDNS) // Determine whether or not to interact with remote
var interactWithRemote bool
if !u.isDryRun() || u.DryRunOption == "server" || u.DryRunOption == "none" || u.DryRunOption == "false" {
interactWithRemote = true
}
hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, interactWithRemote, u.EnableDNS)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -310,7 +327,7 @@ func (u *Upgrade) performUpgrade(ctx context.Context, originalRelease, upgradedR
toBeUpdated, err := existingResourceConflict(toBeCreated, upgradedRelease.Name, upgradedRelease.Namespace) toBeUpdated, err := existingResourceConflict(toBeCreated, upgradedRelease.Name, upgradedRelease.Namespace)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "rendered manifests contain a resource that already exists. Unable to continue with update") return nil, errors.Wrap(err, "Unable to continue with update")
} }
toBeUpdated.Visit(func(r *resource.Info, err error) error { toBeUpdated.Visit(func(r *resource.Info, err error) error {
@ -328,7 +345,8 @@ func (u *Upgrade) performUpgrade(ctx context.Context, originalRelease, upgradedR
} }
} }
if u.DryRun { // Run if it is a dry run
if u.isDryRun() {
u.cfg.Log("dry run for %s", upgradedRelease.Name) u.cfg.Log("dry run for %s", upgradedRelease.Name)
if len(u.Description) > 0 { if len(u.Description) > 0 {
upgradedRelease.Info.Description = u.Description upgradedRelease.Info.Description = u.Description

@ -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.11" { if hv.Version != "v3.12" {
t.Errorf("Expected default HelmVersion to be v3.11, got %q", hv.Version) t.Errorf("Expected default HelmVersion to be v3.12, got %q", hv.Version)
} }
} }

@ -43,6 +43,36 @@ func concatPrefix(a, b string) string {
// - A chart has access to all of the variables for it, as well as all of // - A chart has access to all of the variables for it, as well as all of
// the values destined for its dependencies. // the values destined for its dependencies.
func CoalesceValues(chrt *chart.Chart, vals map[string]interface{}) (Values, error) { func CoalesceValues(chrt *chart.Chart, vals map[string]interface{}) (Values, error) {
valsCopy, err := copyValues(vals)
if err != nil {
return vals, err
}
return coalesce(log.Printf, chrt, valsCopy, "", false)
}
// MergeValues is used to merge the values in a chart and its subcharts. This
// is different from Coalescing as nil/null values are preserved.
//
// Values are coalesced together using the following rules:
//
// - Values in a higher level chart always override values in a lower-level
// dependency chart
// - Scalar values and arrays are replaced, maps are merged
// - A chart has access to all of the variables for it, as well as all of
// the values destined for its dependencies.
//
// Retaining Nils is useful when processes early in a Helm action or business
// logic need to retain them for when Coalescing will happen again later in the
// business logic.
func MergeValues(chrt *chart.Chart, vals map[string]interface{}) (Values, error) {
valsCopy, err := copyValues(vals)
if err != nil {
return vals, err
}
return coalesce(log.Printf, chrt, valsCopy, "", true)
}
func copyValues(vals map[string]interface{}) (Values, error) {
v, err := copystructure.Copy(vals) v, err := copystructure.Copy(vals)
if err != nil { if err != nil {
return vals, err return vals, err
@ -53,21 +83,26 @@ func CoalesceValues(chrt *chart.Chart, vals map[string]interface{}) (Values, err
if valsCopy == nil { if valsCopy == nil {
valsCopy = make(map[string]interface{}) valsCopy = make(map[string]interface{})
} }
return coalesce(log.Printf, chrt, valsCopy, "")
return valsCopy, nil
} }
type printFn func(format string, v ...interface{}) type printFn func(format string, v ...interface{})
// coalesce coalesces the dest values and the chart values, giving priority to the dest values. // coalesce coalesces the dest values and the chart values, giving priority to the dest values.
// //
// This is a helper function for CoalesceValues. // This is a helper function for CoalesceValues and MergeValues.
func coalesce(printf printFn, ch *chart.Chart, dest map[string]interface{}, prefix string) (map[string]interface{}, error) { //
coalesceValues(printf, ch, dest, prefix) // Note, the merge argument specifies whether this is being used by MergeValues
return coalesceDeps(printf, ch, dest, prefix) // or CoalesceValues. Coalescing removes null values and their keys in some
// situations while merging keeps the null values.
func coalesce(printf printFn, ch *chart.Chart, dest map[string]interface{}, prefix string, merge bool) (map[string]interface{}, error) {
coalesceValues(printf, ch, dest, prefix, merge)
return coalesceDeps(printf, ch, dest, prefix, merge)
} }
// coalesceDeps coalesces the dependencies of the given chart. // coalesceDeps coalesces the dependencies of the given chart.
func coalesceDeps(printf printFn, chrt *chart.Chart, dest map[string]interface{}, prefix string) (map[string]interface{}, error) { func coalesceDeps(printf printFn, chrt *chart.Chart, dest map[string]interface{}, prefix string, merge bool) (map[string]interface{}, error) {
for _, subchart := range chrt.Dependencies() { for _, subchart := range chrt.Dependencies() {
if c, ok := dest[subchart.Name()]; !ok { if c, ok := dest[subchart.Name()]; !ok {
// If dest doesn't already have the key, create it. // If dest doesn't already have the key, create it.
@ -78,13 +113,11 @@ func coalesceDeps(printf printFn, chrt *chart.Chart, dest map[string]interface{}
if dv, ok := dest[subchart.Name()]; ok { if dv, ok := dest[subchart.Name()]; ok {
dvmap := dv.(map[string]interface{}) dvmap := dv.(map[string]interface{})
subPrefix := concatPrefix(prefix, chrt.Metadata.Name) subPrefix := concatPrefix(prefix, chrt.Metadata.Name)
// Get globals out of dest and merge them into dvmap. // Get globals out of dest and merge them into dvmap.
coalesceGlobals(printf, dvmap, dest, subPrefix) coalesceGlobals(printf, dvmap, dest, subPrefix, merge)
// Now coalesce the rest of the values. // Now coalesce the rest of the values.
var err error var err error
dest[subchart.Name()], err = coalesce(printf, subchart, dvmap, subPrefix) dest[subchart.Name()], err = coalesce(printf, subchart, dvmap, subPrefix, merge)
if err != nil { if err != nil {
return dest, err return dest, err
} }
@ -96,7 +129,7 @@ func coalesceDeps(printf printFn, chrt *chart.Chart, dest map[string]interface{}
// coalesceGlobals copies the globals out of src and merges them into dest. // coalesceGlobals copies the globals out of src and merges them into dest.
// //
// For convenience, returns dest. // For convenience, returns dest.
func coalesceGlobals(printf printFn, dest, src map[string]interface{}, prefix string) { func coalesceGlobals(printf printFn, dest, src map[string]interface{}, prefix string, merge bool) {
var dg, sg map[string]interface{} var dg, sg map[string]interface{}
if destglob, ok := dest[GlobalKey]; !ok { if destglob, ok := dest[GlobalKey]; !ok {
@ -130,7 +163,10 @@ func coalesceGlobals(printf printFn, dest, src map[string]interface{}, prefix st
// Basically, we reverse order of coalesce here to merge // Basically, we reverse order of coalesce here to merge
// top-down. // top-down.
subPrefix := concatPrefix(prefix, key) subPrefix := concatPrefix(prefix, key)
coalesceTablesFullKey(printf, vv, destvmap, subPrefix) // In this location coalesceTablesFullKey should always have
// merge set to true. The output of coalesceGlobals is run
// through coalesce where any nils will be removed.
coalesceTablesFullKey(printf, vv, destvmap, subPrefix, true)
dg[key] = vv dg[key] = vv
} }
} }
@ -156,12 +192,38 @@ func copyMap(src map[string]interface{}) map[string]interface{} {
// coalesceValues builds up a values map for a particular chart. // coalesceValues builds up a values map for a particular chart.
// //
// Values in v will override the values in the chart. // Values in v will override the values in the chart.
func coalesceValues(printf printFn, c *chart.Chart, v map[string]interface{}, prefix string) { func coalesceValues(printf printFn, c *chart.Chart, v map[string]interface{}, prefix string, merge bool) {
subPrefix := concatPrefix(prefix, c.Metadata.Name) subPrefix := concatPrefix(prefix, c.Metadata.Name)
for key, val := range c.Values {
// Using c.Values directly when coalescing a table can cause problems where
// the original c.Values is altered. Creating a deep copy stops the problem.
// This section is fault-tolerant as there is no ability to return an error.
valuesCopy, err := copystructure.Copy(c.Values)
var vc map[string]interface{}
var ok bool
if err != nil {
// If there is an error something is wrong with copying c.Values it
// means there is a problem in the deep copying package or something
// wrong with c.Values. In this case we will use c.Values and report
// an error.
printf("warning: unable to copy values, err: %s", err)
vc = c.Values
} else {
vc, ok = valuesCopy.(map[string]interface{})
if !ok {
// c.Values has a map[string]interface{} structure. If the copy of
// it cannot be treated as map[string]interface{} there is something
// strangely wrong. Log it and use c.Values
printf("warning: unable to convert values copy to values type")
vc = c.Values
}
}
for key, val := range vc {
if value, ok := v[key]; ok { if value, ok := v[key]; ok {
if value == nil { if value == nil && !merge {
// When the YAML value is null, we remove the value's key. // When the YAML value is null and we are coalescing instead of
// merging, we remove the value's key.
// This allows Helm's various sources of values (value files or --set) to // This allows Helm's various sources of values (value files or --set) to
// remove incompatible keys from any previous chart, file, or set values. // remove incompatible keys from any previous chart, file, or set values.
delete(v, key) delete(v, key)
@ -177,7 +239,7 @@ func coalesceValues(printf printFn, c *chart.Chart, v map[string]interface{}, pr
} else { } else {
// Because v has higher precedence than nv, dest values override src // Because v has higher precedence than nv, dest values override src
// values. // values.
coalesceTablesFullKey(printf, dest, src, concatPrefix(subPrefix, key)) coalesceTablesFullKey(printf, dest, src, concatPrefix(subPrefix, key), merge)
} }
} }
} else { } else {
@ -191,13 +253,17 @@ func coalesceValues(printf printFn, c *chart.Chart, v map[string]interface{}, pr
// //
// dest is considered authoritative. // dest is considered authoritative.
func CoalesceTables(dst, src map[string]interface{}) map[string]interface{} { func CoalesceTables(dst, src map[string]interface{}) map[string]interface{} {
return coalesceTablesFullKey(log.Printf, dst, src, "") return coalesceTablesFullKey(log.Printf, dst, src, "", false)
}
func MergeTables(dst, src map[string]interface{}) map[string]interface{} {
return coalesceTablesFullKey(log.Printf, dst, src, "", true)
} }
// coalesceTablesFullKey merges a source map into a destination map. // coalesceTablesFullKey merges a source map into a destination map.
// //
// dest is considered authoritative. // dest is considered authoritative.
func coalesceTablesFullKey(printf printFn, dst, src map[string]interface{}, prefix string) map[string]interface{} { func coalesceTablesFullKey(printf printFn, dst, src map[string]interface{}, prefix string, merge bool) map[string]interface{} {
// When --reuse-values is set but there are no modifications yet, return new values // When --reuse-values is set but there are no modifications yet, return new values
if src == nil { if src == nil {
return dst return dst
@ -209,13 +275,13 @@ func coalesceTablesFullKey(printf printFn, dst, src map[string]interface{}, pref
// values. // values.
for key, val := range src { for key, val := range src {
fullkey := concatPrefix(prefix, key) fullkey := concatPrefix(prefix, key)
if dv, ok := dst[key]; ok && dv == nil { if dv, ok := dst[key]; ok && !merge && dv == nil {
delete(dst, key) delete(dst, key)
} else if !ok { } else if !ok {
dst[key] = val dst[key] = val
} else if istable(val) { } else if istable(val) {
if istable(dv) { if istable(dv) {
coalesceTablesFullKey(printf, dv.(map[string]interface{}), val.(map[string]interface{}), fullkey) coalesceTablesFullKey(printf, dv.(map[string]interface{}), val.(map[string]interface{}), fullkey, merge)
} else { } else {
printf("warning: cannot overwrite table with non table for %s (%v)", fullkey, val) printf("warning: cannot overwrite table with non table for %s (%v)", fullkey, val)
} }

@ -213,6 +213,160 @@ func TestCoalesceValues(t *testing.T) {
is.Equal(valsCopy, vals) is.Equal(valsCopy, vals)
} }
func TestMergeValues(t *testing.T) {
is := assert.New(t)
c := withDeps(&chart.Chart{
Metadata: &chart.Metadata{Name: "moby"},
Values: map[string]interface{}{
"back": "exists",
"bottom": "exists",
"front": "exists",
"left": "exists",
"name": "moby",
"nested": map[string]interface{}{"boat": true},
"override": "bad",
"right": "exists",
"scope": "moby",
"top": "nope",
"global": map[string]interface{}{
"nested2": map[string]interface{}{"l0": "moby"},
},
},
},
withDeps(&chart.Chart{
Metadata: &chart.Metadata{Name: "pequod"},
Values: map[string]interface{}{
"name": "pequod",
"scope": "pequod",
"global": map[string]interface{}{
"nested2": map[string]interface{}{"l1": "pequod"},
},
},
},
&chart.Chart{
Metadata: &chart.Metadata{Name: "ahab"},
Values: map[string]interface{}{
"global": map[string]interface{}{
"nested": map[string]interface{}{"foo": "bar"},
"nested2": map[string]interface{}{"l2": "ahab"},
},
"scope": "ahab",
"name": "ahab",
"boat": true,
"nested": map[string]interface{}{"foo": false, "bar": true},
},
},
),
&chart.Chart{
Metadata: &chart.Metadata{Name: "spouter"},
Values: map[string]interface{}{
"scope": "spouter",
"global": map[string]interface{}{
"nested2": map[string]interface{}{"l1": "spouter"},
},
},
},
)
vals, err := ReadValues(testCoalesceValuesYaml)
if err != nil {
t.Fatal(err)
}
// taking a copy of the values before passing it
// to MergeValues as argument, so that we can
// use it for asserting later
valsCopy := make(Values, len(vals))
for key, value := range vals {
valsCopy[key] = value
}
v, err := MergeValues(c, vals)
if err != nil {
t.Fatal(err)
}
j, _ := json.MarshalIndent(v, "", " ")
t.Logf("Coalesced Values: %s", string(j))
tests := []struct {
tpl string
expect string
}{
{"{{.top}}", "yup"},
{"{{.back}}", ""},
{"{{.name}}", "moby"},
{"{{.global.name}}", "Ishmael"},
{"{{.global.subject}}", "Queequeg"},
{"{{.global.harpooner}}", "<no value>"},
{"{{.pequod.name}}", "pequod"},
{"{{.pequod.ahab.name}}", "ahab"},
{"{{.pequod.ahab.scope}}", "whale"},
{"{{.pequod.ahab.nested.foo}}", "true"},
{"{{.pequod.ahab.global.name}}", "Ishmael"},
{"{{.pequod.ahab.global.nested.foo}}", "bar"},
{"{{.pequod.ahab.global.subject}}", "Queequeg"},
{"{{.pequod.ahab.global.harpooner}}", "Tashtego"},
{"{{.pequod.global.name}}", "Ishmael"},
{"{{.pequod.global.nested.foo}}", "<no value>"},
{"{{.pequod.global.subject}}", "Queequeg"},
{"{{.spouter.global.name}}", "Ishmael"},
{"{{.spouter.global.harpooner}}", "<no value>"},
{"{{.global.nested.boat}}", "true"},
{"{{.pequod.global.nested.boat}}", "true"},
{"{{.spouter.global.nested.boat}}", "true"},
{"{{.pequod.global.nested.sail}}", "true"},
{"{{.spouter.global.nested.sail}}", "<no value>"},
{"{{.global.nested2.l0}}", "moby"},
{"{{.global.nested2.l1}}", "<no value>"},
{"{{.global.nested2.l2}}", "<no value>"},
{"{{.pequod.global.nested2.l0}}", "moby"},
{"{{.pequod.global.nested2.l1}}", "pequod"},
{"{{.pequod.global.nested2.l2}}", "<no value>"},
{"{{.pequod.ahab.global.nested2.l0}}", "moby"},
{"{{.pequod.ahab.global.nested2.l1}}", "pequod"},
{"{{.pequod.ahab.global.nested2.l2}}", "ahab"},
{"{{.spouter.global.nested2.l0}}", "moby"},
{"{{.spouter.global.nested2.l1}}", "spouter"},
{"{{.spouter.global.nested2.l2}}", "<no value>"},
}
for _, tt := range tests {
if o, err := ttpl(tt.tpl, v); err != nil || o != tt.expect {
t.Errorf("Expected %q to expand to %q, got %q", tt.tpl, tt.expect, o)
}
}
// nullKeys is different from coalescing. Here the null/nil values are not
// removed.
nullKeys := []string{"bottom", "right", "left", "front"}
for _, nullKey := range nullKeys {
if vv, ok := v[nullKey]; !ok {
t.Errorf("Expected key %q to be present but it was removed", nullKey)
} else if vv != nil {
t.Errorf("Expected key %q to be null but it has a value of %v", nullKey, vv)
}
}
if _, ok := v["nested"].(map[string]interface{})["boat"]; !ok {
t.Error("Expected nested boat key to be present but it was removed")
}
subchart := v["pequod"].(map[string]interface{})["ahab"].(map[string]interface{})
if _, ok := subchart["boat"]; !ok {
t.Error("Expected subchart boat key to be present but it was removed")
}
if _, ok := subchart["nested"].(map[string]interface{})["bar"]; !ok {
t.Error("Expected subchart nested bar key to be present but it was removed")
}
// CoalesceValues should not mutate the passed arguments
is.Equal(valsCopy, vals)
}
func TestCoalesceTables(t *testing.T) { func TestCoalesceTables(t *testing.T) {
dst := map[string]interface{}{ dst := map[string]interface{}{
"name": "Ishmael", "name": "Ishmael",
@ -341,6 +495,143 @@ func TestCoalesceTables(t *testing.T) {
} }
} }
func TestMergeTables(t *testing.T) {
dst := map[string]interface{}{
"name": "Ishmael",
"address": map[string]interface{}{
"street": "123 Spouter Inn Ct.",
"city": "Nantucket",
"country": nil,
},
"details": map[string]interface{}{
"friends": []string{"Tashtego"},
},
"boat": "pequod",
"hole": nil,
}
src := map[string]interface{}{
"occupation": "whaler",
"address": map[string]interface{}{
"state": "MA",
"street": "234 Spouter Inn Ct.",
"country": "US",
},
"details": "empty",
"boat": map[string]interface{}{
"mast": true,
},
"hole": "black",
}
// What we expect is that anything in dst overrides anything in src, but that
// otherwise the values are coalesced.
MergeTables(dst, src)
if dst["name"] != "Ishmael" {
t.Errorf("Unexpected name: %s", dst["name"])
}
if dst["occupation"] != "whaler" {
t.Errorf("Unexpected occupation: %s", dst["occupation"])
}
addr, ok := dst["address"].(map[string]interface{})
if !ok {
t.Fatal("Address went away.")
}
if addr["street"].(string) != "123 Spouter Inn Ct." {
t.Errorf("Unexpected address: %v", addr["street"])
}
if addr["city"].(string) != "Nantucket" {
t.Errorf("Unexpected city: %v", addr["city"])
}
if addr["state"].(string) != "MA" {
t.Errorf("Unexpected state: %v", addr["state"])
}
// This is one test that is different from CoalesceTables. Because country
// is a nil value and it's not removed it's still present.
if _, ok = addr["country"]; !ok {
t.Error("The country is left out.")
}
if det, ok := dst["details"].(map[string]interface{}); !ok {
t.Fatalf("Details is the wrong type: %v", dst["details"])
} else if _, ok := det["friends"]; !ok {
t.Error("Could not find your friends. Maybe you don't have any. :-(")
}
if dst["boat"].(string) != "pequod" {
t.Errorf("Expected boat string, got %v", dst["boat"])
}
// This is one test that is different from CoalesceTables. Because hole
// is a nil value and it's not removed it's still present.
if _, ok = dst["hole"]; !ok {
t.Error("The hole no longer exists.")
}
dst2 := map[string]interface{}{
"name": "Ishmael",
"address": map[string]interface{}{
"street": "123 Spouter Inn Ct.",
"city": "Nantucket",
"country": "US",
},
"details": map[string]interface{}{
"friends": []string{"Tashtego"},
},
"boat": "pequod",
"hole": "black",
"nilval": nil,
}
// What we expect is that anything in dst should have all values set,
// this happens when the --reuse-values flag is set but the chart has no modifications yet
MergeTables(dst2, nil)
if dst2["name"] != "Ishmael" {
t.Errorf("Unexpected name: %s", dst2["name"])
}
addr2, ok := dst2["address"].(map[string]interface{})
if !ok {
t.Fatal("Address went away.")
}
if addr2["street"].(string) != "123 Spouter Inn Ct." {
t.Errorf("Unexpected address: %v", addr2["street"])
}
if addr2["city"].(string) != "Nantucket" {
t.Errorf("Unexpected city: %v", addr2["city"])
}
if addr2["country"].(string) != "US" {
t.Errorf("Unexpected Country: %v", addr2["country"])
}
if det2, ok := dst2["details"].(map[string]interface{}); !ok {
t.Fatalf("Details is the wrong type: %v", dst2["details"])
} else if _, ok := det2["friends"]; !ok {
t.Error("Could not find your friends. Maybe you don't have any. :-(")
}
if dst2["boat"].(string) != "pequod" {
t.Errorf("Expected boat string, got %v", dst2["boat"])
}
if dst2["hole"].(string) != "black" {
t.Errorf("Expected hole string, got %v", dst2["boat"])
}
if dst2["nilval"] != nil {
t.Error("Expected nilvalue to have nil value but it does not")
}
}
func TestCoalesceValuesWarnings(t *testing.T) { func TestCoalesceValuesWarnings(t *testing.T) {
c := withDeps(&chart.Chart{ c := withDeps(&chart.Chart{
@ -391,7 +682,7 @@ func TestCoalesceValuesWarnings(t *testing.T) {
warnings = append(warnings, fmt.Sprintf(format, v...)) warnings = append(warnings, fmt.Sprintf(format, v...))
} }
_, err := coalesce(printf, c, vals, "") _, err := coalesce(printf, c, vals, "", false)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

@ -369,7 +369,7 @@ metadata:
` `
const defaultHorizontalPodAutoscaler = `{{- if .Values.autoscaling.enabled }} const defaultHorizontalPodAutoscaler = `{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2beta1 apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler kind: HorizontalPodAutoscaler
metadata: metadata:
name: {{ include "<CHARTNAME>.fullname" . }} name: {{ include "<CHARTNAME>.fullname" . }}
@ -387,13 +387,17 @@ spec:
- type: Resource - type: Resource
resource: resource:
name: cpu name: cpu
targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }} {{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource - type: Resource
resource: resource:
name: memory name: memory
targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }} {{- end }}
{{- end }} {{- end }}
` `

@ -19,15 +19,29 @@ import (
"log" "log"
"strings" "strings"
"github.com/mitchellh/copystructure"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart"
) )
// ProcessDependencies checks through this chart's dependencies, processing accordingly. // ProcessDependencies checks through this chart's dependencies, processing accordingly.
//
// TODO: For Helm v4 this can be combined with or turned into ProcessDependenciesWithMerge
func ProcessDependencies(c *chart.Chart, v Values) error { func ProcessDependencies(c *chart.Chart, v Values) error {
if err := processDependencyEnabled(c, v, ""); err != nil { if err := processDependencyEnabled(c, v, ""); err != nil {
return err return err
} }
return processDependencyImportValues(c) return processDependencyImportValues(c, false)
}
// ProcessDependenciesWithMerge checks through this chart's dependencies, processing accordingly.
// It is similar to ProcessDependencies but it does not remove nil values during
// the import/export handling process.
func ProcessDependenciesWithMerge(c *chart.Chart, v Values) error {
if err := processDependencyEnabled(c, v, ""); err != nil {
return err
}
return processDependencyImportValues(c, true)
} }
// processDependencyConditions disables charts based on condition path value in values // processDependencyConditions disables charts based on condition path value in values
@ -217,12 +231,18 @@ func set(path []string, data map[string]interface{}) map[string]interface{} {
} }
// processImportValues merges values from child to parent based on the chart's dependencies' ImportValues field. // processImportValues merges values from child to parent based on the chart's dependencies' ImportValues field.
func processImportValues(c *chart.Chart) error { func processImportValues(c *chart.Chart, merge bool) error {
if c.Metadata.Dependencies == nil { if c.Metadata.Dependencies == nil {
return nil return nil
} }
// combine chart values and empty config to get Values // combine chart values and empty config to get Values
cvals, err := CoalesceValues(c, nil) var cvals Values
var err error
if merge {
cvals, err = MergeValues(c, nil)
} else {
cvals, err = CoalesceValues(c, nil)
}
if err != nil { if err != nil {
return err return err
} }
@ -248,7 +268,11 @@ func processImportValues(c *chart.Chart) error {
continue continue
} }
// create value map from child to be merged into parent // create value map from child to be merged into parent
b = CoalesceTables(cvals, pathToMap(parent, vv.AsMap())) if merge {
b = MergeTables(b, pathToMap(parent, vv.AsMap()))
} else {
b = CoalesceTables(b, pathToMap(parent, vv.AsMap()))
}
case string: case string:
child := "exports." + iv child := "exports." + iv
outiv = append(outiv, map[string]string{ outiv = append(outiv, map[string]string{
@ -260,26 +284,71 @@ func processImportValues(c *chart.Chart) error {
log.Printf("Warning: ImportValues missing table: %v", err) log.Printf("Warning: ImportValues missing table: %v", err)
continue continue
} }
b = CoalesceTables(b, vm.AsMap()) if merge {
b = MergeTables(b, vm.AsMap())
} else {
b = CoalesceTables(b, vm.AsMap())
}
} }
} }
// set our formatted import values
r.ImportValues = outiv r.ImportValues = outiv
} }
// set the new values // Imported values from a child to a parent chart have a higher priority than
c.Values = CoalesceTables(cvals, b) // values specified in the parent chart.
if merge {
// deep copying the cvals as there are cases where pointers can end
// up in the cvals when they are copied onto b in ways that break things.
cvals = deepCopyMap(cvals)
c.Values = MergeTables(b, cvals)
} else {
// Trimming the nil values from cvals is needed for backwards compatibility.
// Previously, the b value had been populated with cvals along with some
// overrides. This caused the coalescing functionality to remove the
// nil/null values. This trimming is for backwards compat.
cvals = trimNilValues(cvals)
c.Values = CoalesceTables(b, cvals)
}
return nil return nil
} }
func deepCopyMap(vals map[string]interface{}) map[string]interface{} {
valsCopy, err := copystructure.Copy(vals)
if err != nil {
return vals
}
return valsCopy.(map[string]interface{})
}
func trimNilValues(vals map[string]interface{}) map[string]interface{} {
valsCopy, err := copystructure.Copy(vals)
if err != nil {
return vals
}
valsCopyMap := valsCopy.(map[string]interface{})
for key, val := range valsCopyMap {
if val == nil {
log.Printf("trim deleting %q", key)
// Iterate over the values and remove nil keys
delete(valsCopyMap, key)
} else if istable(val) {
log.Printf("trim copying %q", key)
// Recursively call into ourselves to remove keys from inner tables
valsCopyMap[key] = trimNilValues(val.(map[string]interface{}))
}
}
return valsCopyMap
}
// processDependencyImportValues imports specified chart values from child to parent. // processDependencyImportValues imports specified chart values from child to parent.
func processDependencyImportValues(c *chart.Chart) error { func processDependencyImportValues(c *chart.Chart, merge bool) error {
for _, d := range c.Dependencies() { for _, d := range c.Dependencies() {
// recurse // recurse
if err := processDependencyImportValues(d); err != nil { if err := processDependencyImportValues(d, merge); err != nil {
return err return err
} }
} }
return processImportValues(c) return processImportValues(c, merge)
} }

@ -181,10 +181,13 @@ func TestProcessDependencyImportValues(t *testing.T) {
e["imported-chartA-B.SPextra5"] = "k8s" e["imported-chartA-B.SPextra5"] = "k8s"
e["imported-chartA-B.SC1extra5"] = "tiller" e["imported-chartA-B.SC1extra5"] = "tiller"
e["overridden-chart1.SC1bool"] = "false" // These values are imported from the child chart to the parent. Imported
e["overridden-chart1.SC1float"] = "3.141592" // values take precedence over those in the parent so these should be the
e["overridden-chart1.SC1int"] = "99" // values from the child chart.
e["overridden-chart1.SC1string"] = "pollywog" e["overridden-chart1.SC1bool"] = "true"
e["overridden-chart1.SC1float"] = "3.14"
e["overridden-chart1.SC1int"] = "100"
e["overridden-chart1.SC1string"] = "dollywood"
e["overridden-chart1.SPextra2"] = "42" e["overridden-chart1.SPextra2"] = "42"
e["overridden-chartA.SCAbool"] = "true" e["overridden-chartA.SCAbool"] = "true"
@ -193,14 +196,17 @@ func TestProcessDependencyImportValues(t *testing.T) {
e["overridden-chartA.SCAstring"] = "jabberwocky" e["overridden-chartA.SCAstring"] = "jabberwocky"
e["overridden-chartA.SPextra4"] = "true" e["overridden-chartA.SPextra4"] = "true"
// These values are imported from the child chart to the parent. Imported
// values take precedence over those in the parent so these should be the
// values from the child chart.
e["overridden-chartA-B.SCAbool"] = "true" e["overridden-chartA-B.SCAbool"] = "true"
e["overridden-chartA-B.SCAfloat"] = "41.3" e["overridden-chartA-B.SCAfloat"] = "3.33"
e["overridden-chartA-B.SCAint"] = "808" e["overridden-chartA-B.SCAint"] = "555"
e["overridden-chartA-B.SCAstring"] = "jabberwocky" e["overridden-chartA-B.SCAstring"] = "wormwood"
e["overridden-chartA-B.SCBbool"] = "false" e["overridden-chartA-B.SCBbool"] = "true"
e["overridden-chartA-B.SCBfloat"] = "1.99" e["overridden-chartA-B.SCBfloat"] = "0.25"
e["overridden-chartA-B.SCBint"] = "77" e["overridden-chartA-B.SCBint"] = "98"
e["overridden-chartA-B.SCBstring"] = "jango" e["overridden-chartA-B.SCBstring"] = "murkwood"
e["overridden-chartA-B.SPextra6"] = "111" e["overridden-chartA-B.SPextra6"] = "111"
e["overridden-chartA-B.SCAextra1"] = "23" e["overridden-chartA-B.SCAextra1"] = "23"
e["overridden-chartA-B.SCBextra1"] = "13" e["overridden-chartA-B.SCBextra1"] = "13"
@ -212,7 +218,7 @@ func TestProcessDependencyImportValues(t *testing.T) {
e["SCBexported2A"] = "blaster" e["SCBexported2A"] = "blaster"
e["global.SC1exported2.all.SC1exported3"] = "SC1expstr" e["global.SC1exported2.all.SC1exported3"] = "SC1expstr"
if err := processDependencyImportValues(c); err != nil { if err := processDependencyImportValues(c, false); err != nil {
t.Fatalf("processing import values dependencies %v", err) t.Fatalf("processing import values dependencies %v", err)
} }
cc := Values(c.Values) cc := Values(c.Values)
@ -225,18 +231,44 @@ func TestProcessDependencyImportValues(t *testing.T) {
switch pv := pv.(type) { switch pv := pv.(type) {
case float64: case float64:
if s := strconv.FormatFloat(pv, 'f', -1, 64); s != vv { if s := strconv.FormatFloat(pv, 'f', -1, 64); s != vv {
t.Errorf("failed to match imported float value %v with expected %v", s, vv) t.Errorf("failed to match imported float value %v with expected %v for key %q", s, vv, kk)
} }
case bool: case bool:
if b := strconv.FormatBool(pv); b != vv { if b := strconv.FormatBool(pv); b != vv {
t.Errorf("failed to match imported bool value %v with expected %v", b, vv) t.Errorf("failed to match imported bool value %v with expected %v for key %q", b, vv, kk)
} }
default: default:
if pv != vv { if pv != vv {
t.Errorf("failed to match imported string value %q with expected %q", pv, vv) t.Errorf("failed to match imported string value %q with expected %q for key %q", pv, vv, kk)
} }
} }
} }
// Since this was processed with coalescing there should be no null values.
// Here we verify that.
_, err := cc.PathValue("ensurenull")
if err == nil {
t.Error("expect nil value not found but found it")
}
switch xerr := err.(type) {
case ErrNoValue:
// We found what we expected
default:
t.Errorf("expected an ErrNoValue but got %q instead", xerr)
}
c = loadChart(t, "testdata/subpop")
if err := processDependencyImportValues(c, true); err != nil {
t.Fatalf("processing import values dependencies %v", err)
}
cc = Values(c.Values)
val, err := cc.PathValue("ensurenull")
if err != nil {
t.Error("expect value but ensurenull was not found")
}
if val != nil {
t.Errorf("expect nil value but got %q instead", val)
}
} }
func TestProcessDependencyImportValuesMultiLevelPrecedence(t *testing.T) { func TestProcessDependencyImportValuesMultiLevelPrecedence(t *testing.T) {
@ -244,10 +276,25 @@ func TestProcessDependencyImportValuesMultiLevelPrecedence(t *testing.T) {
e := make(map[string]string) e := make(map[string]string)
// The order of precedence should be:
// 1. User specified values (e.g CLI)
// 2. Imported values
// 3. Parent chart values
// 4. Sub-chart values
// The 4 app charts here deal with things differently:
// - app1 has a port value set in the umbrella chart. It does not import any
// values so the value from the umbrella chart should be used.
// - app2 has a value in the app chart and imports from the library. The
// library chart value should take precedence.
// - app3 has no value in the app chart and imports the value from the library
// chart. The library chart value should be used.
// - app4 has a value in the app chart and does not import the value from the
// library chart. The app charts value should be used.
e["app1.service.port"] = "3456" e["app1.service.port"] = "3456"
e["app2.service.port"] = "8080" e["app2.service.port"] = "9090"
e["app3.service.port"] = "9090"
if err := processDependencyImportValues(c); err != nil { e["app4.service.port"] = "1234"
if err := processDependencyImportValues(c, true); err != nil {
t.Fatalf("processing import values dependencies %v", err) t.Fatalf("processing import values dependencies %v", err)
} }
cc := Values(c.Values) cc := Values(c.Values)
@ -274,7 +321,7 @@ func TestProcessDependencyImportValuesForEnabledCharts(t *testing.T) {
c := loadChart(t, "testdata/import-values-from-enabled-subchart/parent-chart") c := loadChart(t, "testdata/import-values-from-enabled-subchart/parent-chart")
nameOverride := "parent-chart-prod" nameOverride := "parent-chart-prod"
if err := processDependencyImportValues(c); err != nil { if err := processDependencyImportValues(c, true); err != nil {
t.Fatalf("processing import values dependencies %v", err) t.Fatalf("processing import values dependencies %v", err)
} }

@ -38,7 +38,7 @@ func TestSave(t *testing.T) {
tmp := ensure.TempDir(t) tmp := ensure.TempDir(t)
defer os.RemoveAll(tmp) defer os.RemoveAll(tmp)
for _, dest := range []string{tmp, path.Join(tmp, "newdir")} { for _, dest := range []string{tmp, filepath.Join(tmp, "newdir")} {
t.Run("outDir="+dest, func(t *testing.T) { t.Run("outDir="+dest, func(t *testing.T) {
c := &chart.Chart{ c := &chart.Chart{
Metadata: &chart.Metadata{ Metadata: &chart.Metadata{
@ -210,7 +210,7 @@ func TestSaveDir(t *testing.T) {
{Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")},
}, },
Templates: []*chart.File{ Templates: []*chart.File{
{Name: filepath.Join(TemplatesDir, "nested", "dir", "thing.yaml"), Data: []byte("abc: {{ .Values.abc }}")}, {Name: path.Join(TemplatesDir, "nested", "dir", "thing.yaml"), Data: []byte("abc: {{ .Values.abc }}")},
}, },
} }
@ -227,11 +227,11 @@ func TestSaveDir(t *testing.T) {
t.Fatalf("Expected chart archive to have %q, got %q", c.Name(), c2.Name()) t.Fatalf("Expected chart archive to have %q, got %q", c.Name(), c2.Name())
} }
if len(c2.Templates) != 1 || c2.Templates[0].Name != filepath.Join(TemplatesDir, "nested", "dir", "thing.yaml") { if len(c2.Templates) != 1 || c2.Templates[0].Name != c.Templates[0].Name {
t.Fatal("Templates data did not match") t.Fatal("Templates data did not match")
} }
if len(c2.Files) != 1 || c2.Files[0].Name != "scheherazade/shahryar.txt" { if len(c2.Files) != 1 || c2.Files[0].Name != c.Files[0].Name {
t.Fatal("Files data did not match") t.Fatal("Files data did not match")
} }
} }

@ -41,3 +41,5 @@ tags:
subchart2alias: subchart2alias:
enabled: false enabled: false
ensurenull: null

@ -11,3 +11,9 @@ dependencies:
- name: app2 - name: app2
version: 0.1.0 version: 0.1.0
condition: app2.enabled condition: app2.enabled
- name: app3
version: 0.1.0
condition: app3.enabled
- name: app4
version: 0.1.0
condition: app4.enabled

@ -0,0 +1,11 @@
apiVersion: v2
name: app3
description: A Helm chart for Kubernetes
type: application
version: 0.1.0
dependencies:
- name: library
version: 0.1.0
import-values:
- defaults

@ -0,0 +1,5 @@
apiVersion: v2
name: library
description: A Helm chart for Kubernetes
type: library
version: 0.1.0

@ -0,0 +1,9 @@
apiVersion: v1
kind: Service
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http

@ -0,0 +1,5 @@
exports:
defaults:
service:
type: ClusterIP
port: 9090

@ -0,0 +1,9 @@
apiVersion: v2
name: app4
description: A Helm chart for Kubernetes
type: application
version: 0.1.0
dependencies:
- name: library
version: 0.1.0

@ -0,0 +1,5 @@
apiVersion: v2
name: library
description: A Helm chart for Kubernetes
type: library
version: 0.1.0

@ -0,0 +1,9 @@
apiVersion: v1
kind: Service
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http

@ -0,0 +1,5 @@
exports:
defaults:
service:
type: ClusterIP
port: 9090

@ -6,3 +6,9 @@ app1:
app2: app2:
enabled: true enabled: true
app3:
enabled: true
app4:
enabled: true

@ -31,11 +31,12 @@ import (
// Options captures the different ways to specify values // Options captures the different ways to specify values
type Options struct { type Options struct {
ValueFiles []string // -f/--values ValueFiles []string // -f/--values
StringValues []string // --set-string StringValues []string // --set-string
Values []string // --set Values []string // --set
FileValues []string // --set-file FileValues []string // --set-file
JSONValues []string // --set-json JSONValues []string // --set-json
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
@ -94,6 +95,13 @@ func (opts *Options) MergeValues(p getter.Providers) (map[string]interface{}, er
} }
} }
// User specified a value via --set-literal
for _, value := range opts.LiteralValues {
if err := strvals.ParseLiteralInto(value, base); err != nil {
return nil, errors.Wrap(err, "failed parsing --set-literal data")
}
}
return base, nil return base, nil
} }

@ -248,7 +248,7 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
destPath := filepath.Join(m.ChartPath, "charts") destPath := filepath.Join(m.ChartPath, "charts")
tmpPath := filepath.Join(m.ChartPath, "tmpcharts") tmpPath := filepath.Join(m.ChartPath, "tmpcharts")
// Check if 'charts' directory is not actally a directory. If it does not exist, create it. // Check if 'charts' directory is not actually a directory. If it does not exist, create it.
if fi, err := os.Stat(destPath); err == nil { if fi, err := os.Stat(destPath); err == nil {
if !fi.IsDir() { if !fi.IsDir() {
return errors.Errorf("%q is not a directory", destPath) return errors.Errorf("%q is not a directory", destPath)

@ -22,6 +22,7 @@ import (
"strings" "strings"
"sync" "sync"
"testing" "testing"
"text/template"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/chartutil"
@ -869,3 +870,242 @@ func TestRenderRecursionLimit(t *testing.T) {
} }
} }
func TestRenderLoadTemplateForTplFromFile(t *testing.T) {
c := &chart.Chart{
Metadata: &chart.Metadata{Name: "TplLoadFromFile"},
Templates: []*chart.File{
{Name: "templates/base", Data: []byte(`{{ tpl (.Files.Get .Values.filename) . }}`)},
{Name: "templates/_function", Data: []byte(`{{define "test-function"}}test-function{{end}}`)},
},
Files: []*chart.File{
{Name: "test", Data: []byte(`{{ tpl (.Files.Get .Values.filename2) .}}`)},
{Name: "test2", Data: []byte(`{{include "test-function" .}}{{define "nested-define"}}nested-define-content{{end}} {{include "nested-define" .}}`)},
},
}
v := chartutil.Values{
"Values": chartutil.Values{
"filename": "test",
"filename2": "test2",
},
"Chart": c.Metadata,
"Release": chartutil.Values{
"Name": "TestRelease",
},
}
out, err := Render(c, v)
if err != nil {
t.Fatal(err)
}
expect := "test-function nested-define-content"
if got := out["TplLoadFromFile/templates/base"]; got != expect {
t.Fatalf("Expected %q, got %q", expect, got)
}
}
func TestRenderTplEmpty(t *testing.T) {
c := &chart.Chart{
Metadata: &chart.Metadata{Name: "TplEmpty"},
Templates: []*chart.File{
{Name: "templates/empty-string", Data: []byte(`{{tpl "" .}}`)},
{Name: "templates/empty-action", Data: []byte(`{{tpl "{{ \"\"}}" .}}`)},
{Name: "templates/only-defines", Data: []byte(`{{tpl "{{define \"not-invoked\"}}not-rendered{{end}}" .}}`)},
},
}
v := chartutil.Values{
"Chart": c.Metadata,
"Release": chartutil.Values{
"Name": "TestRelease",
},
}
out, err := Render(c, v)
if err != nil {
t.Fatal(err)
}
expects := map[string]string{
"TplEmpty/templates/empty-string": "",
"TplEmpty/templates/empty-action": "",
"TplEmpty/templates/only-defines": "",
}
for file, expect := range expects {
if out[file] != expect {
t.Errorf("Expected %q, got %q", expect, out[file])
}
}
}
func TestRenderTplTemplateNames(t *testing.T) {
// .Template.BasePath and .Name make it through
c := &chart.Chart{
Metadata: &chart.Metadata{Name: "TplTemplateNames"},
Templates: []*chart.File{
{Name: "templates/default-basepath", Data: []byte(`{{tpl "{{ .Template.BasePath }}" .}}`)},
{Name: "templates/default-name", Data: []byte(`{{tpl "{{ .Template.Name }}" .}}`)},
{Name: "templates/modified-basepath", Data: []byte(`{{tpl "{{ .Template.BasePath }}" .Values.dot}}`)},
{Name: "templates/modified-name", Data: []byte(`{{tpl "{{ .Template.Name }}" .Values.dot}}`)},
// Current implementation injects the 'tpl' template as if it were a template file, and
// so only BasePath and Name make it through.
{Name: "templates/modified-field", Data: []byte(`{{tpl "{{ .Template.Field }}" .Values.dot}}`)},
},
}
v := chartutil.Values{
"Values": chartutil.Values{
"dot": chartutil.Values{
"Template": chartutil.Values{
"BasePath": "path/to/template",
"Name": "name-of-template",
"Field": "extra-field",
},
},
},
"Chart": c.Metadata,
"Release": chartutil.Values{
"Name": "TestRelease",
},
}
out, err := Render(c, v)
if err != nil {
t.Fatal(err)
}
expects := map[string]string{
"TplTemplateNames/templates/default-basepath": "TplTemplateNames/templates",
"TplTemplateNames/templates/default-name": "TplTemplateNames/templates/default-name",
"TplTemplateNames/templates/modified-basepath": "path/to/template",
"TplTemplateNames/templates/modified-name": "name-of-template",
"TplTemplateNames/templates/modified-field": "",
}
for file, expect := range expects {
if out[file] != expect {
t.Errorf("Expected %q, got %q", expect, out[file])
}
}
}
func TestRenderTplRedefines(t *testing.T) {
// Redefining a template inside 'tpl' does not affect the outer definition
c := &chart.Chart{
Metadata: &chart.Metadata{Name: "TplRedefines"},
Templates: []*chart.File{
{Name: "templates/_partials", Data: []byte(`{{define "partial"}}original-in-partial{{end}}`)},
{Name: "templates/partial", Data: []byte(
`before: {{include "partial" .}}\n{{tpl .Values.partialText .}}\nafter: {{include "partial" .}}`,
)},
{Name: "templates/manifest", Data: []byte(
`{{define "manifest"}}original-in-manifest{{end}}` +
`before: {{include "manifest" .}}\n{{tpl .Values.manifestText .}}\nafter: {{include "manifest" .}}`,
)},
// The current implementation replaces the manifest text and re-parses, so a
// partial template defined only in the manifest invoking tpl cannot be accessed
// by that tpl call.
//{Name: "templates/manifest-only", Data: []byte(
// `{{define "manifest-only"}}only-in-manifest{{end}}` +
// `before: {{include "manifest-only" .}}\n{{tpl .Values.manifestOnlyText .}}\nafter: {{include "manifest-only" .}}`,
//)},
},
}
v := chartutil.Values{
"Values": chartutil.Values{
"partialText": `{{define "partial"}}redefined-in-tpl{{end}}tpl: {{include "partial" .}}`,
"manifestText": `{{define "manifest"}}redefined-in-tpl{{end}}tpl: {{include "manifest" .}}`,
"manifestOnlyText": `tpl: {{include "manifest-only" .}}`,
},
"Chart": c.Metadata,
"Release": chartutil.Values{
"Name": "TestRelease",
},
}
out, err := Render(c, v)
if err != nil {
t.Fatal(err)
}
expects := map[string]string{
"TplRedefines/templates/partial": `before: original-in-partial\ntpl: original-in-partial\nafter: original-in-partial`,
"TplRedefines/templates/manifest": `before: original-in-manifest\ntpl: redefined-in-tpl\nafter: original-in-manifest`,
//"TplRedefines/templates/manifest-only": `before: only-in-manifest\ntpl: only-in-manifest\nafter: only-in-manifest`,
}
for file, expect := range expects {
if out[file] != expect {
t.Errorf("Expected %q, got %q", expect, out[file])
}
}
}
func TestRenderTplMissingKey(t *testing.T) {
// Rendering a missing key results in empty/zero output.
c := &chart.Chart{
Metadata: &chart.Metadata{Name: "TplMissingKey"},
Templates: []*chart.File{
{Name: "templates/manifest", Data: []byte(
`missingValue: {{tpl "{{.Values.noSuchKey}}" .}}`,
)},
},
}
v := chartutil.Values{
"Values": chartutil.Values{},
"Chart": c.Metadata,
"Release": chartutil.Values{
"Name": "TestRelease",
},
}
out, err := Render(c, v)
if err != nil {
t.Fatal(err)
}
expects := map[string]string{
"TplMissingKey/templates/manifest": `missingValue: `,
}
for file, expect := range expects {
if out[file] != expect {
t.Errorf("Expected %q, got %q", expect, out[file])
}
}
}
func TestRenderTplMissingKeyString(t *testing.T) {
// Rendering a missing key results in error
c := &chart.Chart{
Metadata: &chart.Metadata{Name: "TplMissingKeyStrict"},
Templates: []*chart.File{
{Name: "templates/manifest", Data: []byte(
`missingValue: {{tpl "{{.Values.noSuchKey}}" .}}`,
)},
},
}
v := chartutil.Values{
"Values": chartutil.Values{},
"Chart": c.Metadata,
"Release": chartutil.Values{
"Name": "TestRelease",
},
}
e := new(Engine)
e.Strict = true
out, err := e.Render(c, v)
if err == nil {
t.Errorf("Expected error, got %v", out)
return
}
switch err.(type) {
case (template.ExecError):
errTxt := fmt.Sprint(err)
if !strings.Contains(errTxt, "noSuchKey") {
t.Errorf("Expected error to contain 'noSuchKey', got %s", errTxt)
}
default:
// Some unexpected error.
t.Fatal(err)
}
}

@ -131,7 +131,7 @@ func (f files) AsConfig() string {
// //
// data: // data:
// //
// {{ .Files.Glob("secrets/*").AsSecrets() }} // {{ .Files.Glob("secrets/*").AsSecrets() | indent 4 }}
func (f files) AsSecrets() string { func (f files) AsSecrets() string {
if f == nil { if f == nil {
return "" return ""
@ -157,6 +157,9 @@ func (f files) Lines(path string) []string {
if f == nil || f[path] == nil { if f == nil || f[path] == nil {
return []string{} return []string{}
} }
s := string(f[path])
return strings.Split(string(f[path]), "\n") if s[len(s)-1] == '\n' {
s = s[:len(s)-1]
}
return strings.Split(s, "\n")
} }

@ -28,7 +28,8 @@ var cases = []struct {
{"ship/stowaway.txt", "Legatt"}, {"ship/stowaway.txt", "Legatt"},
{"story/name.txt", "The Secret Sharer"}, {"story/name.txt", "The Secret Sharer"},
{"story/author.txt", "Joseph Conrad"}, {"story/author.txt", "Joseph Conrad"},
{"multiline/test.txt", "bar\nfoo"}, {"multiline/test.txt", "bar\nfoo\n"},
{"multiline/test_with_blank_lines.txt", "bar\nfoo\n\n\n"},
} }
func getTestFiles() files { func getTestFiles() files {
@ -96,3 +97,15 @@ func TestLines(t *testing.T) {
as.Equal("bar", out[0]) as.Equal("bar", out[0])
} }
func TestBlankLines(t *testing.T) {
as := assert.New(t)
f := getTestFiles()
out := f.Lines("multiline/test_with_blank_lines.txt")
as.Len(out, 4)
as.Equal("bar", out[0])
as.Equal("", out[3])
}

@ -37,6 +37,7 @@ import (
apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
apierrors "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
multierror "github.com/hashicorp/go-multierror"
"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"
@ -337,13 +338,7 @@ func (c *Client) Build(reader io.Reader, validate bool) (ResourceList, error) {
validationDirective = metav1.FieldValidationStrict validationDirective = metav1.FieldValidationStrict
} }
dynamicClient, err := c.Factory.DynamicClient() schema, err := c.Factory.Validator(validationDirective)
if err != nil {
return nil, err
}
verifier := resource.NewQueryParamVerifier(dynamicClient, c.Factory.OpenAPIGetter(), resource.QueryParamFieldValidation)
schema, err := c.Factory.Validator(validationDirective, verifier)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -363,13 +358,7 @@ func (c *Client) BuildTable(reader io.Reader, validate bool) (ResourceList, erro
validationDirective = metav1.FieldValidationStrict validationDirective = metav1.FieldValidationStrict
} }
dynamicClient, err := c.Factory.DynamicClient() schema, err := c.Factory.Validator(validationDirective)
if err != nil {
return nil, err
}
verifier := resource.NewQueryParamVerifier(dynamicClient, c.Factory.OpenAPIGetter(), resource.QueryParamFieldValidation)
schema, err := c.Factory.Validator(validationDirective, verifier)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -456,7 +445,7 @@ func (c *Client) Update(original, target ResourceList, force bool) (*Result, err
c.Log("Skipping delete of %q due to annotation [%s=%s]", info.Name, ResourcePolicyAnno, KeepPolicy) c.Log("Skipping delete of %q due to annotation [%s=%s]", info.Name, ResourcePolicyAnno, KeepPolicy)
continue continue
} }
if err := deleteResource(info); err != nil { if err := deleteResource(info, metav1.DeletePropagationBackground); err != nil {
c.Log("Failed to delete %q, err: %s", info.ObjectName(), err) c.Log("Failed to delete %q, err: %s", info.ObjectName(), err)
continue continue
} }
@ -465,17 +454,29 @@ func (c *Client) Update(original, target ResourceList, force bool) (*Result, err
return res, nil return res, nil
} }
// Delete deletes Kubernetes resources specified in the resources list. It will // Delete deletes Kubernetes resources specified in the resources list with
// attempt to delete all resources even if one or more fail and collect any // background cascade deletion. It will attempt to delete all resources even
// errors. All successfully deleted items will be returned in the `Deleted` // if one or more fail and collect any errors. All successfully deleted items
// ResourceList that is part of the result. // will be returned in the `Deleted` ResourceList that is part of the result.
func (c *Client) Delete(resources ResourceList) (*Result, []error) { func (c *Client) Delete(resources ResourceList) (*Result, []error) {
return delete(c, resources, metav1.DeletePropagationBackground)
}
// Delete deletes Kubernetes resources specified in the resources list with
// given deletion propagation policy. It will attempt to delete all resources even
// if one or more fail and collect any errors. All successfully deleted items
// will be returned in the `Deleted` ResourceList that is part of the result.
func (c *Client) DeleteWithPropagationPolicy(resources ResourceList, policy metav1.DeletionPropagation) (*Result, []error) {
return delete(c, resources, policy)
}
func delete(c *Client, resources ResourceList, propagation metav1.DeletionPropagation) (*Result, []error) {
var errs []error var errs []error
res := &Result{} res := &Result{}
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)
err := deleteResource(info) err := deleteResource(info, propagation)
if err == nil || apierrors.IsNotFound(err) { if err == nil || apierrors.IsNotFound(err) {
if err != nil { if err != nil {
c.Log("Ignoring delete failure for %q %s: %v", info.Name, info.Mapping.GroupVersionKind, err) c.Log("Ignoring delete failure for %q %s: %v", info.Name, info.Mapping.GroupVersionKind, err)
@ -532,6 +533,8 @@ func (c *Client) WatchUntilReady(resources ResourceList, timeout time.Duration)
} }
func perform(infos ResourceList, fn func(*resource.Info) error) error { func perform(infos ResourceList, fn func(*resource.Info) error) error {
var result error
if len(infos) == 0 { if len(infos) == 0 {
return ErrNoObjectsVisited return ErrNoObjectsVisited
} }
@ -542,10 +545,11 @@ func perform(infos ResourceList, fn func(*resource.Info) error) error {
for range infos { for range infos {
err := <-errs err := <-errs
if err != nil { if err != nil {
return err result = multierror.Append(result, err)
} }
} }
return nil
return result
} }
// getManagedFieldsManager returns the manager string. If one was set it will be returned. // getManagedFieldsManager returns the manager string. If one was set it will be returned.
@ -593,8 +597,7 @@ func createResource(info *resource.Info) error {
return info.Refresh(obj, true) return info.Refresh(obj, true)
} }
func deleteResource(info *resource.Info) error { func deleteResource(info *resource.Info, policy metav1.DeletionPropagation) error {
policy := metav1.DeletePropagationBackground
opts := &metav1.DeleteOptions{PropagationPolicy: &policy} opts := &metav1.DeleteOptions{PropagationPolicy: &policy}
_, err := resource.NewHelper(info.Client, info.Mapping).WithFieldManager(getManagedFieldsManager()).DeleteWithOptions(info.Namespace, info.Name, opts) _, err := resource.NewHelper(info.Client, info.Mapping).WithFieldManager(getManagedFieldsManager()).DeleteWithOptions(info.Namespace, info.Name, opts)
return err return err

@ -18,7 +18,6 @@ package kube // import "helm.sh/helm/v3/pkg/kube"
import ( import (
"k8s.io/cli-runtime/pkg/resource" "k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/discovery"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd"
@ -42,7 +41,5 @@ type Factory interface {
NewBuilder() *resource.Builder NewBuilder() *resource.Builder
// Returns a schema that can validate objects stored on disk. // Returns a schema that can validate objects stored on disk.
Validator(validationDirective string, verifier *resource.QueryParamVerifier) (validation.Schema, error) Validator(validationDirective string) (validation.Schema, error)
// OpenAPIGetter returns a getter for the openapi schema document
OpenAPIGetter() discovery.OpenAPISchemaInterface
} }

@ -22,6 +22,7 @@ import (
"time" "time"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/resource" "k8s.io/cli-runtime/pkg/resource"
@ -37,10 +38,12 @@ type FailingKubeClient struct {
GetError error GetError error
WaitError error WaitError error
DeleteError error DeleteError error
DeleteWithPropagationError error
WatchUntilReadyError error WatchUntilReadyError error
UpdateError error UpdateError error
BuildError error BuildError error
BuildTableError error BuildTableError error
BuildDummy bool
BuildUnstructuredError error BuildUnstructuredError error
WaitAndGetCompletedPodPhaseError error WaitAndGetCompletedPodPhaseError error
WaitDuration time.Duration WaitDuration time.Duration
@ -116,6 +119,9 @@ func (f *FailingKubeClient) Build(r io.Reader, _ bool) (kube.ResourceList, error
if f.BuildError != nil { if f.BuildError != nil {
return []*resource.Info{}, f.BuildError return []*resource.Info{}, f.BuildError
} }
if f.BuildDummy {
return createDummyResourceList(), nil
}
return f.PrintingKubeClient.Build(r, false) return f.PrintingKubeClient.Build(r, false)
} }
@ -134,3 +140,21 @@ func (f *FailingKubeClient) WaitAndGetCompletedPodPhase(s string, d time.Duratio
} }
return f.PrintingKubeClient.WaitAndGetCompletedPodPhase(s, d) return f.PrintingKubeClient.WaitAndGetCompletedPodPhase(s, d)
} }
// DeleteWithPropagationPolicy returns the configured error if set or prints
func (f *FailingKubeClient) DeleteWithPropagationPolicy(resources kube.ResourceList, policy metav1.DeletionPropagation) (*kube.Result, []error) {
if f.DeleteWithPropagationError != nil {
return nil, []error{f.DeleteWithPropagationError}
}
return f.PrintingKubeClient.DeleteWithPropagationPolicy(resources, policy)
}
func createDummyResourceList() kube.ResourceList {
var resInfo resource.Info
resInfo.Name = "dummyName"
resInfo.Namespace = "dummyNamespace"
var resourceList kube.ResourceList
resourceList.Append(&resInfo)
return resourceList
}

@ -22,6 +22,7 @@ import (
"time" "time"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/resource" "k8s.io/cli-runtime/pkg/resource"
@ -115,6 +116,17 @@ func (p *PrintingKubeClient) WaitAndGetCompletedPodPhase(_ string, _ time.Durati
return v1.PodSucceeded, nil return v1.PodSucceeded, nil
} }
// DeleteWithPropagationPolicy implements KubeClient delete.
//
// It only prints out the content to be deleted.
func (p *PrintingKubeClient) DeleteWithPropagationPolicy(resources kube.ResourceList, policy metav1.DeletionPropagation) (*kube.Result, []error) {
_, err := io.Copy(p.Out, bufferize(resources))
if err != nil {
return nil, []error{err}
}
return &kube.Result{Deleted: resources}, nil
}
func bufferize(resources kube.ResourceList) io.Reader { func bufferize(resources kube.ResourceList) io.Reader {
var builder strings.Builder var builder strings.Builder
for _, info := range resources { for _, info := range resources {

@ -21,6 +21,7 @@ import (
"time" "time"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
) )
@ -79,6 +80,14 @@ type InterfaceExt interface {
WaitForDelete(resources ResourceList, timeout time.Duration) error WaitForDelete(resources ResourceList, timeout time.Duration) error
} }
// InterfaceDeletionPropagation is introduced to avoid breaking backwards compatibility for Interface implementers.
//
// TODO Helm 4: Remove InterfaceDeletionPropagation and integrate its method(s) into the Interface.
type InterfaceDeletionPropagation interface {
// Delete destroys one or more resources. The deletion propagation is handled as per the given deletion propagation value.
DeleteWithPropagationPolicy(resources ResourceList, policy metav1.DeletionPropagation) (*Result, []error)
}
// InterfaceResources is introduced to avoid breaking backwards compatibility for Interface implementers. // InterfaceResources is introduced to avoid breaking backwards compatibility for Interface implementers.
// //
// TODO Helm 4: Remove InterfaceResources and integrate its method(s) into the Interface. // TODO Helm 4: Remove InterfaceResources and integrate its method(s) into the Interface.
@ -103,4 +112,5 @@ type InterfaceResources interface {
var _ Interface = (*Client)(nil) var _ Interface = (*Client)(nil)
var _ InterfaceExt = (*Client)(nil) var _ InterfaceExt = (*Client)(nil)
var _ InterfaceDeletionPropagation = (*Client)(nil)
var _ InterfaceResources = (*Client)(nil) var _ InterfaceResources = (*Client)(nil)

@ -18,6 +18,7 @@ package kube // import "helm.sh/helm/v3/pkg/kube"
import ( import (
"context" "context"
"fmt"
appsv1 "k8s.io/api/apps/v1" appsv1 "k8s.io/api/apps/v1"
appsv1beta1 "k8s.io/api/apps/v1beta1" appsv1beta1 "k8s.io/api/apps/v1beta1"
@ -83,8 +84,8 @@ type ReadyChecker struct {
// IsReady checks if v is ready. It supports checking readiness for pods, // IsReady checks if v is ready. It supports checking readiness for pods,
// deployments, persistent volume claims, services, daemon sets, custom // deployments, persistent volume claims, services, daemon sets, custom
// resource definitions, stateful sets, replication controllers, and replica // resource definitions, stateful sets, replication controllers, jobs (optional),
// sets. All other resource kinds are always considered ready. // and replica sets. All other resource kinds are always considered ready.
// //
// IsReady will fetch the latest state of the object from the server prior to // IsReady will fetch the latest state of the object from the server prior to
// performing readiness checks, and it will return any error encountered. // performing readiness checks, and it will return any error encountered.
@ -105,9 +106,11 @@ func (c *ReadyChecker) IsReady(ctx context.Context, v *resource.Info) (bool, err
case *batchv1.Job: case *batchv1.Job:
if c.checkJobs { if c.checkJobs {
job, err := c.client.BatchV1().Jobs(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{}) job, err := c.client.BatchV1().Jobs(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{})
if err != nil || !c.jobReady(job) { if err != nil {
return false, err return false, err
} }
ready, err := c.jobReady(job)
return ready, err
} }
case *appsv1.Deployment, *appsv1beta1.Deployment, *appsv1beta2.Deployment, *extensionsv1beta1.Deployment: case *appsv1.Deployment, *appsv1beta1.Deployment, *appsv1beta2.Deployment, *extensionsv1beta1.Deployment:
currentDeployment, err := c.client.AppsV1().Deployments(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{}) currentDeployment, err := c.client.AppsV1().Deployments(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{})
@ -222,16 +225,17 @@ func (c *ReadyChecker) isPodReady(pod *corev1.Pod) bool {
return false return false
} }
func (c *ReadyChecker) jobReady(job *batchv1.Job) bool { func (c *ReadyChecker) jobReady(job *batchv1.Job) (bool, error) {
if job.Status.Failed > *job.Spec.BackoffLimit { if job.Status.Failed > *job.Spec.BackoffLimit {
c.log("Job is failed: %s/%s", job.GetNamespace(), job.GetName()) c.log("Job is failed: %s/%s", job.GetNamespace(), job.GetName())
return false // If a job is failed, it can't recover, so throw an error
return false, fmt.Errorf("job is failed: %s/%s", job.GetNamespace(), job.GetName())
} }
if job.Spec.Completions != nil && 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()) c.log("Job is not completed: %s/%s", job.GetNamespace(), job.GetName())
return false return false, nil
} }
return true return true, nil
} }
func (c *ReadyChecker) serviceReady(s *corev1.Service) bool { func (c *ReadyChecker) serviceReady(s *corev1.Service) bool {

@ -4,7 +4,6 @@ Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
@ -270,44 +269,52 @@ func Test_ReadyChecker_jobReady(t *testing.T) {
job *batchv1.Job job *batchv1.Job
} }
tests := []struct { tests := []struct {
name string name string
args args args args
want bool want bool
wantErr bool
}{ }{
{ {
name: "job is completed", name: "job is completed",
args: args{job: newJob("foo", 1, intToInt32(1), 1, 0)}, args: args{job: newJob("foo", 1, intToInt32(1), 1, 0)},
want: true, want: true,
wantErr: false,
}, },
{ {
name: "job is incomplete", name: "job is incomplete",
args: args{job: newJob("foo", 1, intToInt32(1), 0, 0)}, args: args{job: newJob("foo", 1, intToInt32(1), 0, 0)},
want: false, want: false,
wantErr: false,
}, },
{ {
name: "job is failed", name: "job is failed but within BackoffLimit",
args: args{job: newJob("foo", 1, intToInt32(1), 0, 1)}, args: args{job: newJob("foo", 1, intToInt32(1), 0, 1)},
want: false, want: false,
wantErr: false,
}, },
{ {
name: "job is completed with retry", name: "job is completed with retry",
args: args{job: newJob("foo", 1, intToInt32(1), 1, 1)}, args: args{job: newJob("foo", 1, intToInt32(1), 1, 1)},
want: true, want: true,
wantErr: false,
}, },
{ {
name: "job is failed with retry", name: "job is failed and beyond BackoffLimit",
args: args{job: newJob("foo", 1, intToInt32(1), 0, 2)}, args: args{job: newJob("foo", 1, intToInt32(1), 0, 2)},
want: false, want: false,
wantErr: true,
}, },
{ {
name: "job is completed single run", name: "job is completed single run",
args: args{job: newJob("foo", 0, intToInt32(1), 1, 0)}, args: args{job: newJob("foo", 0, intToInt32(1), 1, 0)},
want: true, want: true,
wantErr: false,
}, },
{ {
name: "job is failed single run", name: "job is failed single run",
args: args{job: newJob("foo", 0, intToInt32(1), 0, 1)}, args: args{job: newJob("foo", 0, intToInt32(1), 0, 1)},
want: false, want: false,
wantErr: true,
}, },
{ {
name: "job with null completions", name: "job with null completions",
@ -318,7 +325,12 @@ func Test_ReadyChecker_jobReady(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
c := NewReadyChecker(fake.NewSimpleClientset(), nil) c := NewReadyChecker(fake.NewSimpleClientset(), nil)
if got := c.jobReady(tt.args.job); got != tt.want { got, err := c.jobReady(tt.args.job)
if (err != nil) != tt.wantErr {
t.Errorf("jobReady() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("jobReady() = %v, want %v", got, tt.want) t.Errorf("jobReady() = %v, want %v", got, tt.want)
} }
}) })

@ -50,7 +50,7 @@ func (w *waiter) waitForResources(created ResourceList) error {
ctx, cancel := context.WithTimeout(context.Background(), w.timeout) ctx, cancel := context.WithTimeout(context.Background(), w.timeout)
defer cancel() defer cancel()
return wait.PollImmediateUntil(2*time.Second, func() (bool, error) { return wait.PollUntilContextCancel(ctx, 2*time.Second, true, func(ctx context.Context) (bool, error) {
for _, v := range created { for _, v := range created {
ready, err := w.c.IsReady(ctx, v) ready, err := w.c.IsReady(ctx, v)
if !ready || err != nil { if !ready || err != nil {
@ -58,7 +58,7 @@ func (w *waiter) waitForResources(created ResourceList) error {
} }
} }
return true, nil return true, nil
}, ctx.Done()) })
} }
// waitForDeletedResources polls to check if all the resources are deleted or a timeout is reached // waitForDeletedResources polls to check if all the resources are deleted or a timeout is reached
@ -68,7 +68,7 @@ func (w *waiter) waitForDeletedResources(deleted ResourceList) error {
ctx, cancel := context.WithTimeout(context.Background(), w.timeout) ctx, cancel := context.WithTimeout(context.Background(), w.timeout)
defer cancel() defer cancel()
return wait.PollImmediateUntil(2*time.Second, func() (bool, error) { return wait.PollUntilContextCancel(ctx, 2*time.Second, true, func(ctx context.Context) (bool, error) {
for _, v := range deleted { for _, v := range deleted {
err := v.Get() err := v.Get()
if err == nil || !apierrors.IsNotFound(err) { if err == nil || !apierrors.IsNotFound(err) {
@ -76,7 +76,7 @@ func (w *waiter) waitForDeletedResources(deleted ResourceList) error {
} }
} }
return true, nil return true, nil
}, ctx.Done()) })
} }
// SelectorsForObject returns the pod label selector for a given object // SelectorsForObject returns the pod label selector for a given object

@ -19,6 +19,7 @@ package lint
import ( import (
"strings" "strings"
"testing" "testing"
"time"
"helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/lint/support" "helm.sh/helm/v3/pkg/lint/support"
@ -34,6 +35,7 @@ const badValuesFileDir = "rules/testdata/badvaluesfile"
const badYamlFileDir = "rules/testdata/albatross" const badYamlFileDir = "rules/testdata/albatross"
const goodChartDir = "rules/testdata/goodone" const goodChartDir = "rules/testdata/goodone"
const subChartValuesDir = "rules/testdata/withsubchart" const subChartValuesDir = "rules/testdata/withsubchart"
const malformedTemplate = "rules/testdata/malformed-template"
func TestBadChart(t *testing.T) { func TestBadChart(t *testing.T) {
m := All(badChartDir, values, namespace, strict).Messages m := All(badChartDir, values, namespace, strict).Messages
@ -151,3 +153,26 @@ func TestSubChartValuesChart(t *testing.T) {
} }
} }
} }
// lint stuck with malformed template object
// See https://github.com/helm/helm/issues/11391
func TestMalformedTemplate(t *testing.T) {
c := time.After(3 * time.Second)
ch := make(chan int, 1)
var m []support.Message
go func() {
m = All(malformedTemplate, values, namespace, strict).Messages
ch <- 1
}()
select {
case <-c:
t.Fatalf("lint malformed template timeout")
case <-ch:
if len(m) != 1 {
t.Fatalf("All didn't fail with expected errors, got %#v", m)
}
if !strings.Contains(m[0].Err.Error(), "invalid character '{'") {
t.Errorf("All didn't have the error for invalid character '{'")
}
}
}

@ -72,7 +72,7 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace
// lint ignores import-values // lint ignores import-values
// See https://github.com/helm/helm/issues/9658 // See https://github.com/helm/helm/issues/9658
if err := chartutil.ProcessDependencies(chart, values); err != nil { if err := chartutil.ProcessDependenciesWithMerge(chart, values); err != nil {
return return
} }
@ -141,10 +141,11 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace
break break
} }
// If YAML linting fails, we sill progress. So we don't capture the returned state // If YAML linting fails here, it will always fail in the next block as well, so we should return here.
// on this linter run. // fix https://github.com/helm/helm/issues/11391
linter.RunLinterRule(support.ErrorSev, fpath, validateYamlContent(err)) if !linter.RunLinterRule(support.ErrorSev, fpath, validateYamlContent(err)) {
return
}
if yamlStruct != nil { if yamlStruct != nil {
// NOTE: set to warnings to allow users to support out-of-date kubernetes // NOTE: set to warnings to allow users to support out-of-date kubernetes
// Refs https://github.com/helm/helm/issues/8596 // Refs https://github.com/helm/helm/issues/8596

@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

@ -0,0 +1,25 @@
apiVersion: v2
name: test
description: A Helm chart for Kubernetes
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "1.16.0"
icon: https://riverrun.io

@ -0,0 +1 @@
{ {- $relname := .Release.Name -}}

@ -0,0 +1,82 @@
# Default values for test.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: nginx
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: ""
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
# Specifies whether a service account should be created
create: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
podAnnotations: {}
podSecurityContext: {}
# fsGroup: 2000
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
service:
type: ClusterIP
port: 80
ingress:
enabled: false
className: ""
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: chart-example.local
paths:
- path: /
pathType: ImplementationSpecific
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 100
targetCPUUtilizationPercentage: 80
# targetMemoryUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity: {}

@ -86,7 +86,7 @@ func TestPlatformPrepareCommand(t *testing.T) {
Name: "test", Name: "test",
Command: "echo -n os-arch", Command: "echo -n os-arch",
PlatformCommand: []PlatformCommand{ PlatformCommand: []PlatformCommand{
{OperatingSystem: "linux", Architecture: "i386", Command: "echo -n linux-i386"}, {OperatingSystem: "linux", Architecture: "386", Command: "echo -n linux-386"},
{OperatingSystem: "linux", Architecture: "amd64", Command: "echo -n linux-amd64"}, {OperatingSystem: "linux", Architecture: "amd64", Command: "echo -n linux-amd64"},
{OperatingSystem: "linux", Architecture: "arm64", Command: "echo -n linux-arm64"}, {OperatingSystem: "linux", Architecture: "arm64", Command: "echo -n linux-arm64"},
{OperatingSystem: "linux", Architecture: "ppc64le", Command: "echo -n linux-ppc64le"}, {OperatingSystem: "linux", Architecture: "ppc64le", Command: "echo -n linux-ppc64le"},
@ -98,8 +98,8 @@ func TestPlatformPrepareCommand(t *testing.T) {
var osStrCmp string var osStrCmp string
os := runtime.GOOS os := runtime.GOOS
arch := runtime.GOARCH arch := runtime.GOARCH
if os == "linux" && arch == "i386" { if os == "linux" && arch == "386" {
osStrCmp = "linux-i386" osStrCmp = "linux-386"
} else if os == "linux" && arch == "amd64" { } else if os == "linux" && arch == "amd64" {
osStrCmp = "linux-amd64" osStrCmp = "linux-amd64"
} else if os == "linux" && arch == "arm64" { } else if os == "linux" && arch == "arm64" {
@ -125,7 +125,7 @@ func TestPartialPlatformPrepareCommand(t *testing.T) {
Name: "test", Name: "test",
Command: "echo -n os-arch", Command: "echo -n os-arch",
PlatformCommand: []PlatformCommand{ PlatformCommand: []PlatformCommand{
{OperatingSystem: "linux", Architecture: "i386", Command: "echo -n linux-i386"}, {OperatingSystem: "linux", Architecture: "386", Command: "echo -n linux-386"},
{OperatingSystem: "windows", Architecture: "amd64", Command: "echo -n win-64"}, {OperatingSystem: "windows", Architecture: "amd64", Command: "echo -n win-64"},
}, },
}, },
@ -134,7 +134,7 @@ func TestPartialPlatformPrepareCommand(t *testing.T) {
os := runtime.GOOS os := runtime.GOOS
arch := runtime.GOARCH arch := runtime.GOARCH
if os == "linux" { if os == "linux" {
osStrCmp = "linux-i386" osStrCmp = "linux-386"
} else if os == "windows" && arch == "amd64" { } else if os == "windows" && arch == "amd64" {
osStrCmp = "win-64" osStrCmp = "win-64"
} else { } else {
@ -166,7 +166,7 @@ func TestNoMatchPrepareCommand(t *testing.T) {
Metadata: &Metadata{ Metadata: &Metadata{
Name: "test", Name: "test",
PlatformCommand: []PlatformCommand{ PlatformCommand: []PlatformCommand{
{OperatingSystem: "no-os", Architecture: "amd64", Command: "echo -n linux-i386"}, {OperatingSystem: "no-os", Architecture: "amd64", Command: "echo -n linux-386"},
}, },
}, },
} }

@ -498,6 +498,7 @@ type (
pushOperation struct { pushOperation struct {
provData []byte provData []byte
strictMode bool strictMode bool
test bool
} }
) )
@ -551,7 +552,9 @@ func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResu
descriptors = append(descriptors, provDescriptor) descriptors = append(descriptors, provDescriptor)
} }
manifestData, manifest, err := content.GenerateManifest(&configDescriptor, nil, descriptors...) ociAnnotations := generateOCIAnnotations(meta, operation.test)
manifestData, manifest, err := content.GenerateManifest(&configDescriptor, ociAnnotations, descriptors...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -614,6 +617,13 @@ func PushOptStrictMode(strictMode bool) PushOption {
} }
} }
// PushOptTest returns a function that sets whether test setting on push
func PushOptTest(test bool) PushOption {
return func(operation *pushOperation) {
operation.test = test
}
}
// Tags provides a sorted list all semver compliant tags for a given repository // Tags provides a sorted list all semver compliant tags for a given repository
func (c *Client) Tags(ref string) ([]string, error) { func (c *Client) Tags(ref string) ([]string, error) {
parsedReference, err := registry.ParseReference(ref) parsedReference, err := registry.ParseReference(ref)

@ -23,8 +23,12 @@ import (
"io" "io"
"net/http" "net/http"
"strings" "strings"
"time"
helmtime "helm.sh/helm/v3/pkg/time"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
orascontext "oras.land/oras-go/pkg/context" orascontext "oras.land/oras-go/pkg/context"
@ -35,6 +39,11 @@ import (
"helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/chart/loader"
) )
var immutableOciAnnotations = []string{
ocispec.AnnotationVersion,
ocispec.AnnotationTitle,
}
// IsOCI determines whether or not a URL is to be treated as an OCI URL // IsOCI determines whether or not a URL is to be treated as an OCI URL
func IsOCI(url string) bool { func IsOCI(url string) bool {
return strings.HasPrefix(url, fmt.Sprintf("%s://", OCIScheme)) return strings.HasPrefix(url, fmt.Sprintf("%s://", OCIScheme))
@ -155,3 +164,84 @@ func NewRegistryClientWithTLS(out io.Writer, certFile, keyFile, caFile string, i
} }
return registryClient, nil return registryClient, nil
} }
// generateOCIAnnotations will generate OCI annotations to include within the OCI manifest
func generateOCIAnnotations(meta *chart.Metadata, test bool) map[string]string {
// Get annotations from Chart attributes
ociAnnotations := generateChartOCIAnnotations(meta, test)
// Copy Chart annotations
annotations:
for chartAnnotationKey, chartAnnotationValue := range meta.Annotations {
// Avoid overriding key properties
for _, immutableOciKey := range immutableOciAnnotations {
if immutableOciKey == chartAnnotationKey {
continue annotations
}
}
// Add chart annotation
ociAnnotations[chartAnnotationKey] = chartAnnotationValue
}
return ociAnnotations
}
// getChartOCIAnnotations will generate OCI annotations from the provided chart
func generateChartOCIAnnotations(meta *chart.Metadata, test bool) map[string]string {
chartOCIAnnotations := map[string]string{}
chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationDescription, meta.Description)
chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationTitle, meta.Name)
chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationVersion, meta.Version)
chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationURL, meta.Home)
if !test {
chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationCreated, helmtime.Now().UTC().Format(time.RFC3339))
}
if len(meta.Sources) > 0 {
chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationSource, meta.Sources[0])
}
if meta.Maintainers != nil && len(meta.Maintainers) > 0 {
var maintainerSb strings.Builder
for maintainerIdx, maintainer := range meta.Maintainers {
if len(maintainer.Name) > 0 {
maintainerSb.WriteString(maintainer.Name)
}
if len(maintainer.Email) > 0 {
maintainerSb.WriteString(" (")
maintainerSb.WriteString(maintainer.Email)
maintainerSb.WriteString(")")
}
if maintainerIdx < len(meta.Maintainers)-1 {
maintainerSb.WriteString(", ")
}
}
chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationAuthors, maintainerSb.String())
}
return chartOCIAnnotations
}
// addToMap takes an existing map and adds an item if the value is not empty
func addToMap(inputMap map[string]string, newKey string, newValue string) map[string]string {
// Add item to map if its
if len(strings.TrimSpace(newValue)) > 0 {
inputMap[newKey] = newValue
}
return inputMap
}

@ -0,0 +1,240 @@
/*
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 registry // import "helm.sh/helm/v3/pkg/registry"
import (
"reflect"
"testing"
"time"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"helm.sh/helm/v3/pkg/chart"
helmtime "helm.sh/helm/v3/pkg/time"
)
func TestGenerateOCIChartAnnotations(t *testing.T) {
tests := []struct {
name string
chart *chart.Metadata
expect map[string]string
}{
{
"Baseline chart",
&chart.Metadata{
Name: "oci",
Version: "0.0.1",
},
map[string]string{
"org.opencontainers.image.title": "oci",
"org.opencontainers.image.version": "0.0.1",
},
},
{
"Simple chart values",
&chart.Metadata{
Name: "oci",
Version: "0.0.1",
Description: "OCI Helm Chart",
Home: "https://helm.sh",
},
map[string]string{
"org.opencontainers.image.title": "oci",
"org.opencontainers.image.version": "0.0.1",
"org.opencontainers.image.description": "OCI Helm Chart",
"org.opencontainers.image.url": "https://helm.sh",
},
},
{
"Maintainer without email",
&chart.Metadata{
Name: "oci",
Version: "0.0.1",
Description: "OCI Helm Chart",
Home: "https://helm.sh",
Maintainers: []*chart.Maintainer{
{
Name: "John Snow",
},
},
},
map[string]string{
"org.opencontainers.image.title": "oci",
"org.opencontainers.image.version": "0.0.1",
"org.opencontainers.image.description": "OCI Helm Chart",
"org.opencontainers.image.url": "https://helm.sh",
"org.opencontainers.image.authors": "John Snow",
},
},
{
"Maintainer with email",
&chart.Metadata{
Name: "oci",
Version: "0.0.1",
Description: "OCI Helm Chart",
Home: "https://helm.sh",
Maintainers: []*chart.Maintainer{
{Name: "John Snow", Email: "john@winterfell.com"},
},
},
map[string]string{
"org.opencontainers.image.title": "oci",
"org.opencontainers.image.version": "0.0.1",
"org.opencontainers.image.description": "OCI Helm Chart",
"org.opencontainers.image.url": "https://helm.sh",
"org.opencontainers.image.authors": "John Snow (john@winterfell.com)",
},
},
{
"Multiple Maintainers",
&chart.Metadata{
Name: "oci",
Version: "0.0.1",
Description: "OCI Helm Chart",
Home: "https://helm.sh",
Maintainers: []*chart.Maintainer{
{Name: "John Snow", Email: "john@winterfell.com"},
{Name: "Jane Snow"},
},
},
map[string]string{
"org.opencontainers.image.title": "oci",
"org.opencontainers.image.version": "0.0.1",
"org.opencontainers.image.description": "OCI Helm Chart",
"org.opencontainers.image.url": "https://helm.sh",
"org.opencontainers.image.authors": "John Snow (john@winterfell.com), Jane Snow",
},
},
{
"Chart with Sources",
&chart.Metadata{
Name: "oci",
Version: "0.0.1",
Description: "OCI Helm Chart",
Sources: []string{
"https://github.com/helm/helm",
},
},
map[string]string{
"org.opencontainers.image.title": "oci",
"org.opencontainers.image.version": "0.0.1",
"org.opencontainers.image.description": "OCI Helm Chart",
"org.opencontainers.image.source": "https://github.com/helm/helm",
},
},
}
for _, tt := range tests {
result := generateChartOCIAnnotations(tt.chart, true)
if !reflect.DeepEqual(tt.expect, result) {
t.Errorf("%s: expected map %v, got %v", tt.name, tt.expect, result)
}
}
}
func TestGenerateOCIAnnotations(t *testing.T) {
tests := []struct {
name string
chart *chart.Metadata
expect map[string]string
}{
{
"Baseline chart",
&chart.Metadata{
Name: "oci",
Version: "0.0.1",
},
map[string]string{
"org.opencontainers.image.title": "oci",
"org.opencontainers.image.version": "0.0.1",
},
},
{
"Simple chart values with custom Annotations",
&chart.Metadata{
Name: "oci",
Version: "0.0.1",
Description: "OCI Helm Chart",
Annotations: map[string]string{
"extrakey": "extravlue",
"anotherkey": "anothervalue",
},
},
map[string]string{
"org.opencontainers.image.title": "oci",
"org.opencontainers.image.version": "0.0.1",
"org.opencontainers.image.description": "OCI Helm Chart",
"extrakey": "extravlue",
"anotherkey": "anothervalue",
},
},
{
"Verify Chart Name and Version cannot be overridden from annotations",
&chart.Metadata{
Name: "oci",
Version: "0.0.1",
Description: "OCI Helm Chart",
Annotations: map[string]string{
"org.opencontainers.image.title": "badchartname",
"org.opencontainers.image.version": "1.0.0",
"extrakey": "extravlue",
},
},
map[string]string{
"org.opencontainers.image.title": "oci",
"org.opencontainers.image.version": "0.0.1",
"org.opencontainers.image.description": "OCI Helm Chart",
"extrakey": "extravlue",
},
},
}
for _, tt := range tests {
result := generateOCIAnnotations(tt.chart, true)
if !reflect.DeepEqual(tt.expect, result) {
t.Errorf("%s: expected map %v, got %v", tt.name, tt.expect, result)
}
}
}
func TestGenerateOCICreatedAnnotations(t *testing.T) {
chart := &chart.Metadata{
Name: "oci",
Version: "0.0.1",
}
result := generateOCIAnnotations(chart, false)
// Check that created annotation exists
if _, ok := result[ocispec.AnnotationCreated]; !ok {
t.Errorf("%s annotation not created", ocispec.AnnotationCreated)
}
// Verify value of created artifact in RFC3339 format
if _, err := helmtime.Parse(time.RFC3339, result[ocispec.AnnotationCreated]); err != nil {
t.Errorf("%s annotation with value '%s' not in RFC3339 format", ocispec.AnnotationCreated, result[ocispec.AnnotationCreated])
}
}

@ -216,7 +216,7 @@ func initCompromisedRegistryTestServer() string {
func testPush(suite *TestSuite) { func testPush(suite *TestSuite) {
// Bad bytes // Bad bytes
ref := fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost) ref := fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost)
_, err := suite.RegistryClient.Push([]byte("hello"), ref) _, err := suite.RegistryClient.Push([]byte("hello"), ref, PushOptTest(true))
suite.NotNil(err, "error pushing non-chart bytes") suite.NotNil(err, "error pushing non-chart bytes")
// Load a test chart // Load a test chart
@ -227,20 +227,20 @@ func testPush(suite *TestSuite) {
// non-strict ref (chart name) // non-strict ref (chart name)
ref = fmt.Sprintf("%s/testrepo/boop:%s", suite.DockerRegistryHost, meta.Version) ref = fmt.Sprintf("%s/testrepo/boop:%s", suite.DockerRegistryHost, meta.Version)
_, err = suite.RegistryClient.Push(chartData, ref) _, err = suite.RegistryClient.Push(chartData, ref, PushOptTest(true))
suite.NotNil(err, "error pushing non-strict ref (bad basename)") suite.NotNil(err, "error pushing non-strict ref (bad basename)")
// non-strict ref (chart name), with strict mode disabled // non-strict ref (chart name), with strict mode disabled
_, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false)) _, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false), PushOptTest(true))
suite.Nil(err, "no error pushing non-strict ref (bad basename), with strict mode disabled") suite.Nil(err, "no error pushing non-strict ref (bad basename), with strict mode disabled")
// non-strict ref (chart version) // non-strict ref (chart version)
ref = fmt.Sprintf("%s/testrepo/%s:latest", suite.DockerRegistryHost, meta.Name) ref = fmt.Sprintf("%s/testrepo/%s:latest", suite.DockerRegistryHost, meta.Name)
_, err = suite.RegistryClient.Push(chartData, ref) _, err = suite.RegistryClient.Push(chartData, ref, PushOptTest(true))
suite.NotNil(err, "error pushing non-strict ref (bad tag)") suite.NotNil(err, "error pushing non-strict ref (bad tag)")
// non-strict ref (chart version), with strict mode disabled // non-strict ref (chart version), with strict mode disabled
_, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false)) _, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false), PushOptTest(true))
suite.Nil(err, "no error pushing non-strict ref (bad tag), with strict mode disabled") suite.Nil(err, "no error pushing non-strict ref (bad tag), with strict mode disabled")
// basic push, good ref // basic push, good ref
@ -249,7 +249,7 @@ func testPush(suite *TestSuite) {
meta, err = extractChartMeta(chartData) meta, err = extractChartMeta(chartData)
suite.Nil(err, "no error extracting chart meta") suite.Nil(err, "no error extracting chart meta")
ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version) ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
_, err = suite.RegistryClient.Push(chartData, ref) _, err = suite.RegistryClient.Push(chartData, ref, PushOptTest(true))
suite.Nil(err, "no error pushing good ref") suite.Nil(err, "no error pushing good ref")
_, err = suite.RegistryClient.Pull(ref) _, err = suite.RegistryClient.Pull(ref)
@ -267,7 +267,7 @@ func testPush(suite *TestSuite) {
// push with prov // push with prov
ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version) ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
result, err := suite.RegistryClient.Push(chartData, ref, PushOptProvData(provData)) result, err := suite.RegistryClient.Push(chartData, ref, PushOptProvData(provData), PushOptTest(true))
suite.Nil(err, "no error pushing good ref with prov") suite.Nil(err, "no error pushing good ref with prov")
_, err = suite.RegistryClient.Pull(ref) _, err = suite.RegistryClient.Pull(ref)
@ -279,12 +279,12 @@ func testPush(suite *TestSuite) {
suite.Equal(ref, result.Ref) suite.Equal(ref, result.Ref)
suite.Equal(meta.Name, result.Chart.Meta.Name) suite.Equal(meta.Name, result.Chart.Meta.Name)
suite.Equal(meta.Version, result.Chart.Meta.Version) suite.Equal(meta.Version, result.Chart.Meta.Version)
suite.Equal(int64(512), result.Manifest.Size) suite.Equal(int64(684), result.Manifest.Size)
suite.Equal(int64(99), result.Config.Size) suite.Equal(int64(99), result.Config.Size)
suite.Equal(int64(973), result.Chart.Size) suite.Equal(int64(973), result.Chart.Size)
suite.Equal(int64(695), result.Prov.Size) suite.Equal(int64(695), result.Prov.Size)
suite.Equal( suite.Equal(
"sha256:af4c20a1df1431495e673c14ecfa3a2ba24839a7784349d6787cd67957392e83", "sha256:b57e8ffd938c43253f30afedb3c209136288e6b3af3b33473e95ea3b805888e6",
result.Manifest.Digest) result.Manifest.Digest)
suite.Equal( suite.Equal(
"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580", "sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580",
@ -352,12 +352,12 @@ func testPull(suite *TestSuite) {
suite.Equal(ref, result.Ref) suite.Equal(ref, result.Ref)
suite.Equal(meta.Name, result.Chart.Meta.Name) suite.Equal(meta.Name, result.Chart.Meta.Name)
suite.Equal(meta.Version, result.Chart.Meta.Version) suite.Equal(meta.Version, result.Chart.Meta.Version)
suite.Equal(int64(512), result.Manifest.Size) suite.Equal(int64(684), result.Manifest.Size)
suite.Equal(int64(99), result.Config.Size) suite.Equal(int64(99), result.Config.Size)
suite.Equal(int64(973), result.Chart.Size) suite.Equal(int64(973), result.Chart.Size)
suite.Equal(int64(695), result.Prov.Size) suite.Equal(int64(695), result.Prov.Size)
suite.Equal( suite.Equal(
"sha256:af4c20a1df1431495e673c14ecfa3a2ba24839a7784349d6787cd67957392e83", "sha256:b57e8ffd938c43253f30afedb3c209136288e6b3af3b33473e95ea3b805888e6",
result.Manifest.Digest) result.Manifest.Digest)
suite.Equal( suite.Equal(
"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580", "sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580",
@ -368,7 +368,7 @@ func testPull(suite *TestSuite) {
suite.Equal( suite.Equal(
"sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256", "sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256",
result.Prov.Digest) result.Prov.Digest)
suite.Equal("{\"schemaVersion\":2,\"config\":{\"mediaType\":\"application/vnd.cncf.helm.config.v1+json\",\"digest\":\"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580\",\"size\":99},\"layers\":[{\"mediaType\":\"application/vnd.cncf.helm.chart.provenance.v1.prov\",\"digest\":\"sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256\",\"size\":695},{\"mediaType\":\"application/vnd.cncf.helm.chart.content.v1.tar+gzip\",\"digest\":\"sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55\",\"size\":973}]}", suite.Equal("{\"schemaVersion\":2,\"config\":{\"mediaType\":\"application/vnd.cncf.helm.config.v1+json\",\"digest\":\"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580\",\"size\":99},\"layers\":[{\"mediaType\":\"application/vnd.cncf.helm.chart.provenance.v1.prov\",\"digest\":\"sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256\",\"size\":695},{\"mediaType\":\"application/vnd.cncf.helm.chart.content.v1.tar+gzip\",\"digest\":\"sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55\",\"size\":973}],\"annotations\":{\"org.opencontainers.image.description\":\"A Helm chart for Kubernetes\",\"org.opencontainers.image.title\":\"signtest\",\"org.opencontainers.image.version\":\"0.1.0\"}}",
string(result.Manifest.Data)) string(result.Manifest.Data))
suite.Equal("{\"name\":\"signtest\",\"version\":\"0.1.0\",\"description\":\"A Helm chart for Kubernetes\",\"apiVersion\":\"v1\"}", suite.Equal("{\"name\":\"signtest\",\"version\":\"0.1.0\",\"description\":\"A Helm chart for Kubernetes\",\"apiVersion\":\"v1\"}",
string(result.Config.Data)) string(result.Config.Data))

@ -84,6 +84,8 @@ func TestIndexFile(t *testing.T) {
{&chart.Metadata{APIVersion: "v2", Name: "cutter", Version: "0.2.0"}, "cutter-0.2.0.tgz", "http://example.com/charts", "sha256:1234567890abc"}, {&chart.Metadata{APIVersion: "v2", Name: "cutter", Version: "0.2.0"}, "cutter-0.2.0.tgz", "http://example.com/charts", "sha256:1234567890abc"},
{&chart.Metadata{APIVersion: "v2", Name: "setter", Version: "0.1.9+alpha"}, "setter-0.1.9+alpha.tgz", "http://example.com/charts", "sha256:1234567890abc"}, {&chart.Metadata{APIVersion: "v2", Name: "setter", Version: "0.1.9+alpha"}, "setter-0.1.9+alpha.tgz", "http://example.com/charts", "sha256:1234567890abc"},
{&chart.Metadata{APIVersion: "v2", Name: "setter", Version: "0.1.9+beta"}, "setter-0.1.9+beta.tgz", "http://example.com/charts", "sha256:1234567890abc"}, {&chart.Metadata{APIVersion: "v2", Name: "setter", Version: "0.1.9+beta"}, "setter-0.1.9+beta.tgz", "http://example.com/charts", "sha256:1234567890abc"},
{&chart.Metadata{APIVersion: "v2", Name: "setter", Version: "0.1.8"}, "setter-0.1.8.tgz", "http://example.com/charts", "sha256:1234567890abc"},
{&chart.Metadata{APIVersion: "v2", Name: "setter", Version: "0.1.8+beta"}, "setter-0.1.8+beta.tgz", "http://example.com/charts", "sha256:1234567890abc"},
} { } {
if err := i.MustAdd(x.md, x.filename, x.baseURL, x.digest); err != nil { if err := i.MustAdd(x.md, x.filename, x.baseURL, x.digest); err != nil {
t.Errorf("unexpected error adding to index: %s", err) t.Errorf("unexpected error adding to index: %s", err)
@ -122,6 +124,11 @@ func TestIndexFile(t *testing.T) {
if err != nil || cv.Metadata.Version != "0.1.9+alpha" { if err != nil || cv.Metadata.Version != "0.1.9+alpha" {
t.Errorf("Expected version: 0.1.9+alpha") t.Errorf("Expected version: 0.1.9+alpha")
} }
cv, err = i.Get("setter", "0.1.8")
if err != nil || cv.Metadata.Version != "0.1.8" {
t.Errorf("Expected version: 0.1.8")
}
} }
func TestLoadIndex(t *testing.T) { func TestLoadIndex(t *testing.T) {

@ -202,7 +202,7 @@ func TestWriteFile(t *testing.T) {
t.Errorf("failed to create test-file (%v)", err) t.Errorf("failed to create test-file (%v)", err)
} }
defer os.Remove(file.Name()) defer os.Remove(file.Name())
if err := sampleRepository.WriteFile(file.Name(), 0644); err != nil { if err := sampleRepository.WriteFile(file.Name(), 0600); err != nil {
t.Errorf("failed to write file (%v)", err) t.Errorf("failed to write file (%v)", err)
} }

@ -385,7 +385,7 @@ func (s *Server) StartTLS() {
CAFile: filepath.Join("../../testdata", "rootca.crt"), CAFile: filepath.Join("../../testdata", "rootca.crt"),
}) })
if err := r.WriteFile(repoConfig, 0644); err != nil { if err := r.WriteFile(repoConfig, 0600); err != nil {
panic(err) panic(err)
} }
} }
@ -422,5 +422,5 @@ func setTestingRepository(url, fname string) error {
Name: "test", Name: "test",
URL: url, URL: url,
}) })
return r.WriteFile(fname, 0644) return r.WriteFile(fname, 0640)
} }

@ -0,0 +1,244 @@
/*
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 strvals
import (
"bytes"
"fmt"
"io"
"strconv"
"github.com/pkg/errors"
)
// ParseLiteral parses a set line interpreting the value as a literal string.
//
// A set line is of the form name1=value1
func ParseLiteral(s string) (map[string]interface{}, error) {
vals := map[string]interface{}{}
scanner := bytes.NewBufferString(s)
t := newLiteralParser(scanner, vals)
err := t.parse()
return vals, err
}
// ParseLiteralInto parses a strvals line and merges the result into dest.
// The value is interpreted as a literal string.
//
// If the strval string has a key that exists in dest, it overwrites the
// dest version.
func ParseLiteralInto(s string, dest map[string]interface{}) error {
scanner := bytes.NewBufferString(s)
t := newLiteralParser(scanner, dest)
return t.parse()
}
// literalParser is a simple parser that takes a strvals line and parses
// it into a map representation.
//
// Values are interpreted as a literal string.
//
// where sc is the source of the original data being parsed
// where data is the final parsed data from the parses with correct types
type literalParser struct {
sc *bytes.Buffer
data map[string]interface{}
}
func newLiteralParser(sc *bytes.Buffer, data map[string]interface{}) *literalParser {
return &literalParser{sc: sc, data: data}
}
func (t *literalParser) parse() error {
for {
err := t.key(t.data, 0)
if err == nil {
continue
}
if err == io.EOF {
return nil
}
return err
}
}
func runesUntilLiteral(in io.RuneReader, stop map[rune]bool) ([]rune, rune, error) {
v := []rune{}
for {
switch r, _, e := in.ReadRune(); {
case e != nil:
return v, r, e
case inMap(r, stop):
return v, r, nil
default:
v = append(v, r)
}
}
}
func (t *literalParser) key(data map[string]interface{}, nestedNameLevel int) (reterr error) {
defer func() {
if r := recover(); r != nil {
reterr = fmt.Errorf("unable to parse key: %s", r)
}
}()
stop := runeSet([]rune{'=', '[', '.'})
for {
switch key, lastRune, err := runesUntilLiteral(t.sc, stop); {
case err != nil:
if len(key) == 0 {
return err
}
return errors.Errorf("key %q has no value", string(key))
case lastRune == '=':
// found end of key: swallow the '=' and get the value
value, err := t.val()
if err == nil && err != io.EOF {
return err
}
set(data, string(key), string(value))
return nil
case lastRune == '.':
// 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 in the given data
inner := map[string]interface{}{}
if _, ok := data[string(key)]; ok {
inner = data[string(key)].(map[string]interface{})
}
// recurse on sub-tree with remaining data
err := t.key(inner, nestedNameLevel)
if err == nil && len(inner) == 0 {
return errors.Errorf("key map %q has no value", string(key))
}
if len(inner) != 0 {
set(data, string(key), inner)
}
return err
case lastRune == '[':
// We are in a list index context, so we need to set an index.
i, err := t.keyIndex()
if err != nil {
return errors.Wrap(err, "error parsing index")
}
kk := string(key)
// find or create target list
list := []interface{}{}
if _, ok := data[kk]; ok {
list = data[kk].([]interface{})
}
// now we need to get the value after the ]
list, err = t.listItem(list, i, nestedNameLevel)
set(data, kk, list)
return err
}
}
}
func (t *literalParser) keyIndex() (int, error) {
// First, get the key.
stop := runeSet([]rune{']'})
v, _, err := runesUntilLiteral(t.sc, stop)
if err != nil {
return 0, err
}
// v should be the index
return strconv.Atoi(string(v))
}
func (t *literalParser) listItem(list []interface{}, i, nestedNameLevel int) ([]interface{}, error) {
if i < 0 {
return list, fmt.Errorf("negative %d index not allowed", i)
}
stop := runeSet([]rune{'[', '.', '='})
switch key, lastRune, err := runesUntilLiteral(t.sc, stop); {
case len(key) > 0:
return list, errors.Errorf("unexpected data at end of array index: %q", key)
case err != nil:
return list, err
case lastRune == '=':
value, err := t.val()
if err != nil && err != io.EOF {
return list, err
}
return setIndex(list, i, string(value))
case lastRune == '.':
// we have a nested object. Send to t.key
inner := map[string]interface{}{}
if len(list) > i {
var ok bool
inner, ok = list[i].(map[string]interface{})
if !ok {
// We have indices out of order. Initialize empty value.
list[i] = map[string]interface{}{}
inner = list[i].(map[string]interface{})
}
}
// recurse
err := t.key(inner, nestedNameLevel)
if err != nil {
return list, err
}
return setIndex(list, i, inner)
case lastRune == '[':
// now we have a nested list. Read the index and handle.
nextI, err := t.keyIndex()
if err != nil {
return list, errors.Wrap(err, "error parsing index")
}
var crtList []interface{}
if len(list) > i {
// If nested list already exists, take the value of list to next cycle.
existed := list[i]
if existed != nil {
crtList = list[i].([]interface{})
}
}
// Now we need to get the value after the ].
list2, err := t.listItem(crtList, nextI, nestedNameLevel)
if err != nil {
return list, err
}
return setIndex(list, i, list2)
default:
return nil, errors.Errorf("parse error: unexpected token %v", lastRune)
}
}
func (t *literalParser) val() ([]rune, error) {
stop := runeSet([]rune{})
v, _, err := runesUntilLiteral(t.sc, stop)
return v, err
}

@ -0,0 +1,480 @@
/*
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 strvals
import (
"fmt"
"testing"
"sigs.k8s.io/yaml"
)
func TestParseLiteral(t *testing.T) {
cases := []struct {
str string
expect map[string]interface{}
err bool
}{
{
str: "name",
err: true,
},
{
str: "name=",
expect: map[string]interface{}{"name": ""},
},
{
str: "name=value",
expect: map[string]interface{}{"name": "value"},
err: false,
},
{
str: "long_int_string=1234567890",
expect: map[string]interface{}{"long_int_string": "1234567890"},
err: false,
},
{
str: "boolean=true",
expect: map[string]interface{}{"boolean": "true"},
err: false,
},
{
str: "is_null=null",
expect: map[string]interface{}{"is_null": "null"},
err: false,
},
{
str: "zero=0",
expect: map[string]interface{}{"zero": "0"},
err: false,
},
{
str: "name1=null,name2=value2",
expect: map[string]interface{}{"name1": "null,name2=value2"},
err: false,
},
{
str: "name1=value,,,tail",
expect: map[string]interface{}{"name1": "value,,,tail"},
err: false,
},
{
str: "leading_zeros=00009",
expect: map[string]interface{}{"leading_zeros": "00009"},
err: false,
},
{
str: "name=one two three",
expect: map[string]interface{}{"name": "one two three"},
err: false,
},
{
str: "outer.inner=value",
expect: map[string]interface{}{"outer": map[string]interface{}{"inner": "value"}},
err: false,
},
{
str: "outer.middle.inner=value",
expect: map[string]interface{}{"outer": map[string]interface{}{"middle": map[string]interface{}{"inner": "value"}}},
err: false,
},
{
str: "name1.name2",
err: true,
},
{
str: "name1.name2=",
expect: map[string]interface{}{"name1": map[string]interface{}{"name2": ""}},
err: false,
},
{
str: "name1.=name2",
err: true,
},
{
str: "name1.,name2",
err: true,
},
{
str: "name1={value1,value2}",
expect: map[string]interface{}{"name1": "{value1,value2}"},
},
// List support
{
str: "list[0]=foo",
expect: map[string]interface{}{"list": []string{"foo"}},
err: false,
},
{
str: "list[0].foo=bar",
expect: map[string]interface{}{
"list": []interface{}{
map[string]interface{}{"foo": "bar"},
},
},
err: false,
},
{
str: "list[-30].hello=world",
err: true,
},
{
str: "list[3]=bar",
expect: map[string]interface{}{"list": []interface{}{nil, nil, nil, "bar"}},
err: false,
},
{
str: "illegal[0]name.foo=bar",
err: true,
},
{
str: "noval[0]",
expect: map[string]interface{}{"noval": []interface{}{}},
err: false,
},
{
str: "noval[0]=",
expect: map[string]interface{}{"noval": []interface{}{""}},
err: false,
},
{
str: "nested[0][0]=1",
expect: map[string]interface{}{"nested": []interface{}{[]interface{}{"1"}}},
err: false,
},
{
str: "nested[1][1]=1",
expect: map[string]interface{}{"nested": []interface{}{nil, []interface{}{nil, "1"}}},
err: false,
},
{
str: "name1.name2[0].foo=bar",
expect: map[string]interface{}{
"name1": map[string]interface{}{
"name2": []map[string]interface{}{{"foo": "bar"}},
},
},
},
{
str: "name1.name2[1].foo=bar",
expect: map[string]interface{}{
"name1": map[string]interface{}{
"name2": []map[string]interface{}{nil, {"foo": "bar"}},
},
},
},
{
str: "name1.name2[1].foo=bar",
expect: map[string]interface{}{
"name1": map[string]interface{}{
"name2": []map[string]interface{}{nil, {"foo": "bar"}},
},
},
},
{
str: "]={}].",
expect: map[string]interface{}{"]": "{}]."},
err: false,
},
// issue test cases: , = $ ( ) { } . \ \\
{
str: "name=val,val",
expect: map[string]interface{}{"name": "val,val"},
err: false,
},
{
str: "name=val.val",
expect: map[string]interface{}{"name": "val.val"},
err: false,
},
{
str: "name=val=val",
expect: map[string]interface{}{"name": "val=val"},
err: false,
},
{
str: "name=val$val",
expect: map[string]interface{}{"name": "val$val"},
err: false,
},
{
str: "name=(value",
expect: map[string]interface{}{"name": "(value"},
err: false,
},
{
str: "name=value)",
expect: map[string]interface{}{"name": "value)"},
err: false,
},
{
str: "name=(value)",
expect: map[string]interface{}{"name": "(value)"},
err: false,
},
{
str: "name={value",
expect: map[string]interface{}{"name": "{value"},
err: false,
},
{
str: "name=value}",
expect: map[string]interface{}{"name": "value}"},
err: false,
},
{
str: "name={value}",
expect: map[string]interface{}{"name": "{value}"},
err: false,
},
{
str: "name={value1,value2}",
expect: map[string]interface{}{"name": "{value1,value2}"},
err: false,
},
{
str: `name=val\val`,
expect: map[string]interface{}{"name": `val\val`},
err: false,
},
{
str: `name=val\\val`,
expect: map[string]interface{}{"name": `val\\val`},
err: false,
},
{
str: `name=val\\\val`,
expect: map[string]interface{}{"name": `val\\\val`},
err: false,
},
{
str: `name={val,.?*v\0a!l)some`,
expect: map[string]interface{}{"name": `{val,.?*v\0a!l)some`},
err: false,
},
{
str: `name=em%GT)tqUDqz,i-\h+Mbqs-!:.m\\rE=mkbM#rR}@{-k@`,
expect: map[string]interface{}{"name": `em%GT)tqUDqz,i-\h+Mbqs-!:.m\\rE=mkbM#rR}@{-k@`},
},
}
for _, tt := range cases {
got, err := ParseLiteral(tt.str)
if err != nil {
if !tt.err {
t.Fatalf("%s: %s", tt.str, err)
}
continue
}
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)
}
}
}
func TestParseLiteralInto(t *testing.T) {
tests := []struct {
input string
input2 string
got map[string]interface{}
expect map[string]interface{}
err bool
}{
{
input: "outer.inner1=value1,outer.inner3=value3,outer.inner4=4",
got: map[string]interface{}{
"outer": map[string]interface{}{
"inner1": "overwrite",
"inner2": "value2",
},
},
expect: map[string]interface{}{
"outer": map[string]interface{}{
"inner1": "value1,outer.inner3=value3,outer.inner4=4",
"inner2": "value2",
}},
err: false,
},
{
input: "listOuter[0][0].type=listValue",
input2: "listOuter[0][0].status=alive",
got: map[string]interface{}{},
expect: map[string]interface{}{
"listOuter": [][]interface{}{{map[string]string{
"type": "listValue",
"status": "alive",
}}},
},
err: false,
},
{
input: "listOuter[0][0].type=listValue",
input2: "listOuter[1][0].status=alive",
got: map[string]interface{}{},
expect: map[string]interface{}{
"listOuter": [][]interface{}{
{
map[string]string{"type": "listValue"},
},
{
map[string]string{"status": "alive"},
},
},
},
err: false,
},
{
input: "listOuter[0][1][0].type=listValue",
input2: "listOuter[0][0][1].status=alive",
got: map[string]interface{}{
"listOuter": []interface{}{
[]interface{}{
[]interface{}{
map[string]string{"exited": "old"},
},
},
},
},
expect: map[string]interface{}{
"listOuter": [][][]interface{}{
{
{
map[string]string{"exited": "old"},
map[string]string{"status": "alive"},
},
{
map[string]string{"type": "listValue"},
},
},
},
},
err: false,
},
}
for _, tt := range tests {
if err := ParseLiteralInto(tt.input, tt.got); err != nil {
t.Fatal(err)
}
if tt.err {
t.Errorf("%s: Expected error. Got nil", tt.input)
}
if tt.input2 != "" {
if err := ParseLiteralInto(tt.input2, tt.got); err != nil {
t.Fatal(err)
}
if tt.err {
t.Errorf("%s: Expected error. Got nil", tt.input2)
}
}
y1, err := yaml.Marshal(tt.expect)
if err != nil {
t.Fatal(err)
}
y2, err := yaml.Marshal(tt.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.input, y1, y2)
}
}
}
func TestParseLiteralNestedLevels(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 := ParseLiteral(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)
}
}
}
Loading…
Cancel
Save