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: "/"
schedule:
interval: "daily"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"

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

@ -35,11 +35,11 @@ jobs:
steps:
- 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.
- name: Initialize CodeQL
uses: github/codeql-action/init@959cbb7472c4d4ad70cdfe6f4976053fe48ab394 # pinv2.1.37
uses: github/codeql-action/init@1813ca74c3faaa3a2da2070b9b8a0b3e7373a0d8 # pinv2.21.0
with:
languages: ${{ matrix.language }}
# 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).
# If this step fails, then you should remove it and run the build manually (see below)
- 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.
# 📚 https://git.io/JvXDl
@ -64,4 +64,4 @@ jobs:
# make release
- 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
steps:
- name: Checkout source code
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # pin@v3.2.0
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # pin@v3.5.3
- name: Setup Go
uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # pin@3.5.0
uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # pin@4.0.1
with:
go-version: '1.20'
@ -49,10 +49,10 @@ jobs:
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout source code
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # pin@v3.2.0
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # pin@v3.5.3
- name: Setup Go
uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # pin@3.5.0
uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # pin@4.0.1
with:
go-version: '1.20'

@ -1,19 +1,19 @@
maintainers:
- adamreese
- bacongobbler
- hickeyma
- joejulian
- jdolitsky
- marckhouzam
- mattfarina
- sabre1041
- scottrigby
- SlickNik
- technosophos
triage:
- joejulian
- yxxhero
- zonggen
- gjenkins8
emeritus:
- adamreese
- bacongobbler
- fibonacci1729
- jascott1
- michelleN
@ -22,6 +22,7 @@ emeritus:
- prydonius
- rimusz
- seh
- SlickNik
- thomastaylor312
- vaikas-google
- 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.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.LiteralValues, "set-literal", []string{}, "set a literal STRING value on the command line")
}
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)
// 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)
if err != nil {
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) {
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.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")
@ -252,6 +264,7 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
RepositoryConfig: settings.RepositoryConfig,
RepositoryCache: settings.RepositoryCache,
Debug: settings.Debug,
RegistryClient: client.GetRegistryClient(),
}
if err := man.Update(); err != nil {
return nil, err
@ -268,6 +281,11 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
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
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
@ -308,3 +326,19 @@ func compInstall(args []string, toComplete string, client *action.Install) ([]st
}
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",
cmd: "lint --quiet testdata/testcharts/chart-with-only-crds",
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)

@ -212,7 +212,7 @@ func (o *repoAddOptions) run(out io.Writer) error {
f.Update(&c)
if err := f.WriteFile(o.repoFile, 0644); err != nil {
if err := f.WriteFile(o.repoFile, 0600); err != nil {
return err
}
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) {
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
}

@ -147,11 +147,10 @@ func (i *Index) SearchLiteral(term string, threshold int) []*Result {
term = strings.ToLower(term)
buf := []*Result{}
for k, v := range i.lines {
lk := strings.ToLower(k)
lv := strings.ToLower(v)
res := strings.Index(lv, term)
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]})
}
}

@ -105,11 +105,11 @@ func loadTestIndex(t *testing.T, all bool) *Index {
i := NewIndex()
i.AddRepo("testing", &repo.IndexFile{Entries: indexfileEntries}, all)
i.AddRepo("ztesting", &repo.IndexFile{Entries: map[string]repo.ChartVersions{
"pinta": {
"Pinta": {
{
URLs: []string{"http://example.com/charts/pinta-2.0.0.tgz"},
Metadata: &chart.Metadata{
Name: "pinta",
Name: "Pinta",
Version: "2.0.0",
Description: "Two ship, version two",
},
@ -170,14 +170,14 @@ func TestSearchByName(t *testing.T) {
query: "pinta",
expect: []*Result{
{Name: "testing/pinta"},
{Name: "ztesting/pinta"},
{Name: "ztesting/Pinta"},
},
},
{
name: "repo-specific search for one result",
query: "ztesting/pinta",
expect: []*Result{
{Name: "ztesting/pinta"},
{Name: "ztesting/Pinta"},
},
},
{
@ -199,7 +199,15 @@ func TestSearchByName(t *testing.T) {
query: "two",
expect: []*Result{
{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",
expect: []*Result{
{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)
// 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.ReleaseName = "release-name"
client.Replace = true // Skip the name check

@ -25,6 +25,8 @@ import (
var chartPath = "testdata/testcharts/subchart"
func TestTemplateCmd(t *testing.T) {
deletevalchart := "testdata/testcharts/issue-9027"
tests := []cmdTestCase{
{
name: "check name",
@ -131,6 +133,34 @@ func TestTemplateCmd(t *testing.T) {
cmd: fmt.Sprintf(`template '%s' --skip-tests`, chartPath),
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)
}

@ -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
kind: CustomResourceDefinition
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
- child: exports.SCBexported2
parent: exports.SCBexported2
# - child: exports.configmap
# parent: configmap
- configmap
- SCBexported1
tags:

@ -21,6 +21,10 @@ exports:
SCBexported2:
SCBexported2A: "blaster"
configmap:
configmap:
value: "bar"
global:
kolla:
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:
all:
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)
},
RunE: func(cmd *cobra.Command, args []string) error {
validationErr := validateCascadeFlag(client)
if validationErr != nil {
return validationErr
}
for i := 0; i < len(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.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.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.StringVar(&client.Description, "description", "", "add a custom description")
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)
// 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
// 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 {
@ -112,6 +118,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
instClient.ChartPathOptions = client.ChartPathOptions
instClient.Force = client.Force
instClient.DryRun = client.DryRun
instClient.DryRunOption = client.DryRunOption
instClient.DisableHooks = client.DisableHooks
instClient.SkipCRDs = client.SkipCRDs
instClient.Timeout = client.Timeout
@ -146,6 +153,10 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
if err != nil {
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)
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.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.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.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")

102
go.mod

@ -5,12 +5,12 @@ go 1.19
require (
github.com/BurntSushi/toml v1.2.1
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/squirrel v1.5.3
github.com/Masterminds/squirrel v1.5.4
github.com/Masterminds/vcs v1.13.3
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/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2
github.com/evanphx/json-patch v5.6.0+incompatible
@ -18,36 +18,38 @@ require (
github.com/gobwas/glob v0.2.3
github.com/gofrs/flock v0.8.1
github.com/gosuri/uitable v0.0.4
github.com/hashicorp/go-multierror v1.1.1
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/mitchellh/copystructure v1.2.0
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/pkg/errors v0.9.1
github.com/rubenv/sql-migrate v1.3.1
github.com/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.6.1
github.com/rubenv/sql-migrate v1.5.1
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.7.0
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
golang.org/x/crypto v0.5.0
golang.org/x/term v0.4.0
golang.org/x/text v0.6.0
k8s.io/api v0.26.0
k8s.io/apiextensions-apiserver v0.26.0
k8s.io/apimachinery v0.26.0
k8s.io/apiserver v0.26.0
k8s.io/cli-runtime v0.26.0
k8s.io/client-go v0.26.0
k8s.io/klog/v2 v2.80.1
k8s.io/kubectl v0.26.0
oras.land/oras-go v1.2.2
golang.org/x/crypto v0.11.0
golang.org/x/term v0.10.0
golang.org/x/text v0.11.0
k8s.io/api v0.27.3
k8s.io/apiextensions-apiserver v0.27.3
k8s.io/apimachinery v0.27.3
k8s.io/apiserver v0.27.3
k8s.io/cli-runtime v0.27.3
k8s.io/client-go v0.27.3
k8s.io/klog/v2 v2.100.1
k8s.io/kubectl v0.27.3
oras.land/oras-go v1.2.3
sigs.k8s.io/yaml v1.3.0
)
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/MakeNowJust/heredoc v1.0.0 // 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/osext v0.0.0-20130617224835-0dd3f918b21b // 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/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/cli v20.10.21+incompatible // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/docker v20.10.21+incompatible // indirect
github.com/docker/cli v23.0.1+incompatible // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/docker/docker v23.0.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.7.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-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/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/fatih/color v1.13.0 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/fvbommel/sortorder v1.0.1 // indirect
github.com/go-errors/errors v1.0.1 // indirect
github.com/go-gorp/gorp/v3 v3.0.5 // indirect
github.com/go-errors/errors v1.4.2 // indirect
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/swag v0.19.14 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // 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/golang/protobuf v1.5.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/gomodule/redigo v1.8.2 // indirect
github.com/google/btree v1.0.1 // 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/mux v1.8.0 // 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/huandu/xstrings v1.4.0 // 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/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/ps v0.0.0-20150810152359-62de8c46ede0 // 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-isatty v0.0.17 // 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/shopspring/decimal v1.3.1 // 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/xlab/treeprint v1.1.0 // 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/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
golang.org/x/net v0.5.0 // indirect
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/oauth2 v0.4.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
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 // indirect
google.golang.org/grpc v1.49.0 // indirect
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect
google.golang.org/grpc v1.53.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/component-base v0.26.0 // indirect
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect
k8s.io/utils v0.0.0-20221107191617-1a15be271d1d // indirect
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
sigs.k8s.io/kustomize/api v0.12.1 // indirect
sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect
k8s.io/component-base v0.27.3 // indirect
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect
k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/kustomize/api v0.13.2 // indirect
sigs.k8s.io/kustomize/kyaml v0.14.1 // 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)
// Create request
req, err := http.NewRequest("GET", p.String(), nil)
req, err := http.NewRequest(http.MethodGet, p.String(), nil)
if err != nil {
return nil, err
}

@ -29,7 +29,7 @@ var (
//
// Increment major number for new feature additions and behavioral changes.
// Increment minor number for bug fixes and performance enhancements.
version = "v3.11"
version = "v3.12"
// metadata is extra build time data
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
//
// 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{}
b := bytes.NewBuffer(nil)
@ -121,12 +121,10 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Valu
var files map[string]string
var err2 error
// A `helm template` or `helm install --dry-run` should not talk to the remote cluster.
// It will break in interesting and exotic ways because other data (e.g. discovery)
// is mocked. It is not up to the template author to decide when the user wants to
// connect to the cluster. So when the user says to dry run, respect the user's
// wishes and do not connect to the cluster.
if !dryRun && cfg.RESTClientGetter != nil {
// A `helm template` should not talk to the remote cluster. However, commands with the flag
//`--dry-run` with the value of `false`, `none`, or `server` should try to interact with the cluster.
// It may break in interesting and exotic ways because other data (e.g. discovery) is mocked.
if interactWithRemote && cfg.RESTClientGetter != nil {
restConfig, err := cfg.RESTClientGetter.ToRESTConfig()
if err != nil {
return hs, b, "", err
@ -189,13 +187,13 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Valu
if includeCrds {
for _, crd := range ch.CRDObjects() {
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 {
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 {
return hs, b, "", err
}
fileWritten[crd.Name] = true
fileWritten[crd.Filename] = true
}
}
}

@ -73,6 +73,7 @@ type Install struct {
Force bool
CreateNamespace bool
DryRun bool
DryRunOption string
DisableHooks bool
Replace bool
Wait bool
@ -142,6 +143,11 @@ func (i *Install) SetRegistryClient(registryClient *registry.Client) {
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 {
// We do these one file at a time in the order they were read.
totalItems := []*resource.Info{}
@ -223,15 +229,20 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
return nil, err
}
if err := chartutil.ProcessDependencies(chrt, vals); err != nil {
if err := chartutil.ProcessDependenciesWithMerge(chrt, vals); err != nil {
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
// contacts the upstream server and builds the capabilities object.
if crds := chrt.CRDObjects(); !i.ClientOnly && !i.SkipCRDs && len(crds) > 0 {
// 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.")
} else if err := i.installCRDs(crds); err != nil {
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
isUpgrade := i.IsUpgrade && i.DryRun
isUpgrade := i.IsUpgrade && i.isDryRun()
options := chartutil.ReleaseOptions{
Name: i.ReleaseName,
Namespace: i.Namespace,
@ -281,7 +292,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
rel := i.createRelease(chrt, vals)
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
if manifestDoc != nil {
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 {
toBeAdopted, err = existingResourceConflict(resources, rel.Name, rel.Namespace)
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
if i.DryRun {
if i.isDryRun() {
rel.Info.Description = "Dry run complete"
return rel, nil
}
@ -369,13 +380,25 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
return rel, err
}
rChan := make(chan resultMessage)
ctxChan := make(chan resultMessage)
doneChan := make(chan struct{})
defer close(doneChan)
go i.performInstall(rChan, rel, toBeAdopted, resources)
go i.handleContext(ctx, rChan, doneChan, rel)
result := <-rChan
//start preformInstall go routine
return result.r, result.e
go i.handleContext(ctx, ctxChan, doneChan, rel)
select {
case result := <-rChan:
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) {
@ -491,7 +514,8 @@ func (i *Install) availableName() error {
if err := chartutil.ValidateReleaseName(start); err != nil {
return errors.Wrapf(err, "release name %q", start)
}
if i.DryRun {
// On dry run, bail here
if i.isDryRun() {
return nil
}

@ -254,7 +254,7 @@ func TestInstallRelease_DryRun(t *testing.T) {
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) {
is := assert.New(t)
instAction := installAction(t)

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

@ -21,6 +21,7 @@ import (
"time"
"github.com/pkg/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/kube"
@ -35,12 +36,13 @@ import (
type Uninstall struct {
cfg *Configuration
DisableHooks bool
DryRun bool
KeepHistory bool
Wait bool
Timeout time.Duration
Description string
DisableHooks bool
DryRun bool
KeepHistory bool
Wait bool
DeletionPropagation string
Timeout time.Duration
Description string
}
// 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")}
}
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)
}
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.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 bool
// DryRun controls whether the operation is prepared, but not executed.
// If `true`, the upgrade is prepared but not performed.
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.
//
// 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 {
return nil, errors.Errorf("release name is invalid: %s", name)
}
u.cfg.Log("preparing upgrade for %s", name)
currentRelease, upgradedRelease, err := u.prepareUpgrade(name, chart, vals)
if err != nil {
@ -165,7 +167,8 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart.
if upgradedRelease.Skipped {
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)
if err := u.cfg.Releases.Update(upgradedRelease); err != nil {
return res, err
@ -175,6 +178,14 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart.
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.
func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, *release.Release, error) {
if chart == nil {
@ -219,7 +230,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin
return nil, nil, err
}
if err := chartutil.ProcessDependencies(chart, vals); err != nil {
if err := chartutil.ProcessDependenciesWithMerge(chart, vals); err != nil {
return nil, nil, err
}
@ -243,7 +254,13 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin
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 {
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)
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 {
@ -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)
if len(u.Description) > 0 {
upgradedRelease.Info.Description = u.Description

@ -62,8 +62,8 @@ func TestDefaultCapabilities(t *testing.T) {
func TestDefaultCapabilitiesHelmVersion(t *testing.T) {
hv := DefaultCapabilities.HelmVersion
if hv.Version != "v3.11" {
t.Errorf("Expected default HelmVersion to be v3.11, got %q", hv.Version)
if hv.Version != "v3.12" {
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
// the values destined for its dependencies.
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)
if err != nil {
return vals, err
@ -53,21 +83,26 @@ func CoalesceValues(chrt *chart.Chart, vals map[string]interface{}) (Values, err
if valsCopy == nil {
valsCopy = make(map[string]interface{})
}
return coalesce(log.Printf, chrt, valsCopy, "")
return valsCopy, nil
}
type printFn func(format string, v ...interface{})
// coalesce coalesces the dest values and the chart values, giving priority to the dest values.
//
// This is a helper function for CoalesceValues.
func coalesce(printf printFn, ch *chart.Chart, dest map[string]interface{}, prefix string) (map[string]interface{}, error) {
coalesceValues(printf, ch, dest, prefix)
return coalesceDeps(printf, ch, dest, prefix)
// This is a helper function for CoalesceValues and MergeValues.
//
// Note, the merge argument specifies whether this is being used by MergeValues
// 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.
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() {
if c, ok := dest[subchart.Name()]; !ok {
// 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 {
dvmap := dv.(map[string]interface{})
subPrefix := concatPrefix(prefix, chrt.Metadata.Name)
// 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.
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 {
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.
//
// 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{}
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
// top-down.
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
}
}
@ -156,12 +192,38 @@ func copyMap(src map[string]interface{}) map[string]interface{} {
// coalesceValues builds up a values map for a particular 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)
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 == nil {
// When the YAML value is null, we remove the value's key.
if value == nil && !merge {
// 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
// remove incompatible keys from any previous chart, file, or set values.
delete(v, key)
@ -177,7 +239,7 @@ func coalesceValues(printf printFn, c *chart.Chart, v map[string]interface{}, pr
} else {
// Because v has higher precedence than nv, dest values override src
// values.
coalesceTablesFullKey(printf, dest, src, concatPrefix(subPrefix, key))
coalesceTablesFullKey(printf, dest, src, concatPrefix(subPrefix, key), merge)
}
}
} else {
@ -191,13 +253,17 @@ func coalesceValues(printf printFn, c *chart.Chart, v map[string]interface{}, pr
//
// dest is considered authoritative.
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.
//
// 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
if src == nil {
return dst
@ -209,13 +275,13 @@ func coalesceTablesFullKey(printf printFn, dst, src map[string]interface{}, pref
// values.
for key, val := range src {
fullkey := concatPrefix(prefix, key)
if dv, ok := dst[key]; ok && dv == nil {
if dv, ok := dst[key]; ok && !merge && dv == nil {
delete(dst, key)
} else if !ok {
dst[key] = val
} else if istable(val) {
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 {
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)
}
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) {
dst := map[string]interface{}{
"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) {
c := withDeps(&chart.Chart{
@ -391,7 +682,7 @@ func TestCoalesceValuesWarnings(t *testing.T) {
warnings = append(warnings, fmt.Sprintf(format, v...))
}
_, err := coalesce(printf, c, vals, "")
_, err := coalesce(printf, c, vals, "", false)
if err != nil {
t.Fatal(err)
}

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

@ -19,15 +19,29 @@ import (
"log"
"strings"
"github.com/mitchellh/copystructure"
"helm.sh/helm/v3/pkg/chart"
)
// 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 {
if err := processDependencyEnabled(c, v, ""); err != nil {
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
@ -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.
func processImportValues(c *chart.Chart) error {
func processImportValues(c *chart.Chart, merge bool) error {
if c.Metadata.Dependencies == nil {
return nil
}
// 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 {
return err
}
@ -248,7 +268,11 @@ func processImportValues(c *chart.Chart) error {
continue
}
// 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:
child := "exports." + iv
outiv = append(outiv, map[string]string{
@ -260,26 +284,71 @@ func processImportValues(c *chart.Chart) error {
log.Printf("Warning: ImportValues missing table: %v", err)
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
}
// set the new values
c.Values = CoalesceTables(cvals, b)
// Imported values from a child to a parent chart have a higher priority than
// 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
}
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.
func processDependencyImportValues(c *chart.Chart) error {
func processDependencyImportValues(c *chart.Chart, merge bool) error {
for _, d := range c.Dependencies() {
// recurse
if err := processDependencyImportValues(d); err != nil {
if err := processDependencyImportValues(d, merge); err != nil {
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.SC1extra5"] = "tiller"
e["overridden-chart1.SC1bool"] = "false"
e["overridden-chart1.SC1float"] = "3.141592"
e["overridden-chart1.SC1int"] = "99"
e["overridden-chart1.SC1string"] = "pollywog"
// 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-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-chartA.SCAbool"] = "true"
@ -193,14 +196,17 @@ func TestProcessDependencyImportValues(t *testing.T) {
e["overridden-chartA.SCAstring"] = "jabberwocky"
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.SCAfloat"] = "41.3"
e["overridden-chartA-B.SCAint"] = "808"
e["overridden-chartA-B.SCAstring"] = "jabberwocky"
e["overridden-chartA-B.SCBbool"] = "false"
e["overridden-chartA-B.SCBfloat"] = "1.99"
e["overridden-chartA-B.SCBint"] = "77"
e["overridden-chartA-B.SCBstring"] = "jango"
e["overridden-chartA-B.SCAfloat"] = "3.33"
e["overridden-chartA-B.SCAint"] = "555"
e["overridden-chartA-B.SCAstring"] = "wormwood"
e["overridden-chartA-B.SCBbool"] = "true"
e["overridden-chartA-B.SCBfloat"] = "0.25"
e["overridden-chartA-B.SCBint"] = "98"
e["overridden-chartA-B.SCBstring"] = "murkwood"
e["overridden-chartA-B.SPextra6"] = "111"
e["overridden-chartA-B.SCAextra1"] = "23"
e["overridden-chartA-B.SCBextra1"] = "13"
@ -212,7 +218,7 @@ func TestProcessDependencyImportValues(t *testing.T) {
e["SCBexported2A"] = "blaster"
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)
}
cc := Values(c.Values)
@ -225,18 +231,44 @@ func TestProcessDependencyImportValues(t *testing.T) {
switch pv := pv.(type) {
case float64:
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:
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:
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) {
@ -244,10 +276,25 @@ func TestProcessDependencyImportValuesMultiLevelPrecedence(t *testing.T) {
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["app2.service.port"] = "8080"
if err := processDependencyImportValues(c); err != nil {
e["app2.service.port"] = "9090"
e["app3.service.port"] = "9090"
e["app4.service.port"] = "1234"
if err := processDependencyImportValues(c, true); err != nil {
t.Fatalf("processing import values dependencies %v", err)
}
cc := Values(c.Values)
@ -274,7 +321,7 @@ func TestProcessDependencyImportValuesForEnabledCharts(t *testing.T) {
c := loadChart(t, "testdata/import-values-from-enabled-subchart/parent-chart")
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)
}

@ -38,7 +38,7 @@ func TestSave(t *testing.T) {
tmp := ensure.TempDir(t)
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) {
c := &chart.Chart{
Metadata: &chart.Metadata{
@ -210,7 +210,7 @@ func TestSaveDir(t *testing.T) {
{Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")},
},
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())
}
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")
}
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")
}
}

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

@ -11,3 +11,9 @@ dependencies:
- name: app2
version: 0.1.0
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:
enabled: true
app3:
enabled: true
app4:
enabled: true

@ -31,11 +31,12 @@ import (
// Options captures the different ways to specify values
type Options struct {
ValueFiles []string // -f/--values
StringValues []string // --set-string
Values []string // --set
FileValues []string // --set-file
JSONValues []string // --set-json
ValueFiles []string // -f/--values
StringValues []string // --set-string
Values []string // --set
FileValues []string // --set-file
JSONValues []string // --set-json
LiteralValues []string // --set-literal
}
// 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
}

@ -248,7 +248,7 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
destPath := filepath.Join(m.ChartPath, "charts")
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.IsDir() {
return errors.Errorf("%q is not a directory", destPath)

@ -22,6 +22,7 @@ import (
"strings"
"sync"
"testing"
"text/template"
"helm.sh/helm/v3/pkg/chart"
"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:
//
// {{ .Files.Glob("secrets/*").AsSecrets() }}
// {{ .Files.Glob("secrets/*").AsSecrets() | indent 4 }}
func (f files) AsSecrets() string {
if f == nil {
return ""
@ -157,6 +157,9 @@ func (f files) Lines(path string) []string {
if f == nil || f[path] == nil {
return []string{}
}
return strings.Split(string(f[path]), "\n")
s := string(f[path])
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"},
{"story/name.txt", "The Secret Sharer"},
{"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 {
@ -96,3 +97,15 @@ func TestLines(t *testing.T) {
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"
apierrors "k8s.io/apimachinery/pkg/api/errors"
multierror "github.com/hashicorp/go-multierror"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"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
}
dynamicClient, err := c.Factory.DynamicClient()
if err != nil {
return nil, err
}
verifier := resource.NewQueryParamVerifier(dynamicClient, c.Factory.OpenAPIGetter(), resource.QueryParamFieldValidation)
schema, err := c.Factory.Validator(validationDirective, verifier)
schema, err := c.Factory.Validator(validationDirective)
if err != nil {
return nil, err
}
@ -363,13 +358,7 @@ func (c *Client) BuildTable(reader io.Reader, validate bool) (ResourceList, erro
validationDirective = metav1.FieldValidationStrict
}
dynamicClient, err := c.Factory.DynamicClient()
if err != nil {
return nil, err
}
verifier := resource.NewQueryParamVerifier(dynamicClient, c.Factory.OpenAPIGetter(), resource.QueryParamFieldValidation)
schema, err := c.Factory.Validator(validationDirective, verifier)
schema, err := c.Factory.Validator(validationDirective)
if err != nil {
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)
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)
continue
}
@ -465,17 +454,29 @@ func (c *Client) Update(original, target ResourceList, force bool) (*Result, err
return res, nil
}
// Delete deletes Kubernetes resources specified in the resources list. 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.
// Delete deletes Kubernetes resources specified in the resources list with
// background cascade deletion. 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) 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
res := &Result{}
mtx := sync.Mutex{}
err := perform(resources, func(info *resource.Info) error {
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 {
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 {
var result error
if len(infos) == 0 {
return ErrNoObjectsVisited
}
@ -542,10 +545,11 @@ func perform(infos ResourceList, fn func(*resource.Info) error) error {
for range infos {
err := <-errs
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.
@ -593,8 +597,7 @@ func createResource(info *resource.Info) error {
return info.Refresh(obj, true)
}
func deleteResource(info *resource.Info) error {
policy := metav1.DeletePropagationBackground
func deleteResource(info *resource.Info, policy metav1.DeletionPropagation) error {
opts := &metav1.DeleteOptions{PropagationPolicy: &policy}
_, err := resource.NewHelper(info.Client, info.Mapping).WithFieldManager(getManagedFieldsManager()).DeleteWithOptions(info.Namespace, info.Name, opts)
return err

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

@ -22,6 +22,7 @@ import (
"time"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/resource"
@ -37,10 +38,12 @@ type FailingKubeClient struct {
GetError error
WaitError error
DeleteError error
DeleteWithPropagationError error
WatchUntilReadyError error
UpdateError error
BuildError error
BuildTableError error
BuildDummy bool
BuildUnstructuredError error
WaitAndGetCompletedPodPhaseError error
WaitDuration time.Duration
@ -116,6 +119,9 @@ func (f *FailingKubeClient) Build(r io.Reader, _ bool) (kube.ResourceList, error
if f.BuildError != nil {
return []*resource.Info{}, f.BuildError
}
if f.BuildDummy {
return createDummyResourceList(), nil
}
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)
}
// 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"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/resource"
@ -115,6 +116,17 @@ func (p *PrintingKubeClient) WaitAndGetCompletedPodPhase(_ string, _ time.Durati
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 {
var builder strings.Builder
for _, info := range resources {

@ -21,6 +21,7 @@ import (
"time"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
@ -79,6 +80,14 @@ type InterfaceExt interface {
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.
//
// 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 _ InterfaceExt = (*Client)(nil)
var _ InterfaceDeletionPropagation = (*Client)(nil)
var _ InterfaceResources = (*Client)(nil)

@ -18,6 +18,7 @@ package kube // import "helm.sh/helm/v3/pkg/kube"
import (
"context"
"fmt"
appsv1 "k8s.io/api/apps/v1"
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,
// deployments, persistent volume claims, services, daemon sets, custom
// resource definitions, stateful sets, replication controllers, and replica
// sets. All other resource kinds are always considered ready.
// resource definitions, stateful sets, replication controllers, jobs (optional),
// 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
// 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:
if c.checkJobs {
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
}
ready, err := c.jobReady(job)
return ready, err
}
case *appsv1.Deployment, *appsv1beta1.Deployment, *appsv1beta2.Deployment, *extensionsv1beta1.Deployment:
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
}
func (c *ReadyChecker) jobReady(job *batchv1.Job) bool {
func (c *ReadyChecker) jobReady(job *batchv1.Job) (bool, error) {
if job.Status.Failed > *job.Spec.BackoffLimit {
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 {
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 {

@ -4,7 +4,6 @@ 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
@ -270,44 +269,52 @@ func Test_ReadyChecker_jobReady(t *testing.T) {
job *batchv1.Job
}
tests := []struct {
name string
args args
want bool
name string
args args
want bool
wantErr bool
}{
{
name: "job is completed",
args: args{job: newJob("foo", 1, intToInt32(1), 1, 0)},
want: true,
name: "job is completed",
args: args{job: newJob("foo", 1, intToInt32(1), 1, 0)},
want: true,
wantErr: false,
},
{
name: "job is incomplete",
args: args{job: newJob("foo", 1, intToInt32(1), 0, 0)},
want: false,
name: "job is incomplete",
args: args{job: newJob("foo", 1, intToInt32(1), 0, 0)},
want: false,
wantErr: false,
},
{
name: "job is failed",
args: args{job: newJob("foo", 1, intToInt32(1), 0, 1)},
want: false,
name: "job is failed but within BackoffLimit",
args: args{job: newJob("foo", 1, intToInt32(1), 0, 1)},
want: false,
wantErr: false,
},
{
name: "job is completed with retry",
args: args{job: newJob("foo", 1, intToInt32(1), 1, 1)},
want: true,
name: "job is completed with retry",
args: args{job: newJob("foo", 1, intToInt32(1), 1, 1)},
want: true,
wantErr: false,
},
{
name: "job is failed with retry",
args: args{job: newJob("foo", 1, intToInt32(1), 0, 2)},
want: false,
name: "job is failed and beyond BackoffLimit",
args: args{job: newJob("foo", 1, intToInt32(1), 0, 2)},
want: false,
wantErr: true,
},
{
name: "job is completed single run",
args: args{job: newJob("foo", 0, intToInt32(1), 1, 0)},
want: true,
name: "job is completed single run",
args: args{job: newJob("foo", 0, intToInt32(1), 1, 0)},
want: true,
wantErr: false,
},
{
name: "job is failed single run",
args: args{job: newJob("foo", 0, intToInt32(1), 0, 1)},
want: false,
name: "job is failed single run",
args: args{job: newJob("foo", 0, intToInt32(1), 0, 1)},
want: false,
wantErr: true,
},
{
name: "job with null completions",
@ -318,7 +325,12 @@ func Test_ReadyChecker_jobReady(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
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)
}
})

@ -50,7 +50,7 @@ func (w *waiter) waitForResources(created ResourceList) error {
ctx, cancel := context.WithTimeout(context.Background(), w.timeout)
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 {
ready, err := w.c.IsReady(ctx, v)
if !ready || err != nil {
@ -58,7 +58,7 @@ func (w *waiter) waitForResources(created ResourceList) error {
}
}
return true, nil
}, ctx.Done())
})
}
// 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)
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 {
err := v.Get()
if err == nil || !apierrors.IsNotFound(err) {
@ -76,7 +76,7 @@ func (w *waiter) waitForDeletedResources(deleted ResourceList) error {
}
}
return true, nil
}, ctx.Done())
})
}
// SelectorsForObject returns the pod label selector for a given object

@ -19,6 +19,7 @@ package lint
import (
"strings"
"testing"
"time"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/lint/support"
@ -34,6 +35,7 @@ const badValuesFileDir = "rules/testdata/badvaluesfile"
const badYamlFileDir = "rules/testdata/albatross"
const goodChartDir = "rules/testdata/goodone"
const subChartValuesDir = "rules/testdata/withsubchart"
const malformedTemplate = "rules/testdata/malformed-template"
func TestBadChart(t *testing.T) {
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
// 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
}
@ -141,10 +141,11 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace
break
}
// If YAML linting fails, we sill progress. So we don't capture the returned state
// on this linter run.
linter.RunLinterRule(support.ErrorSev, fpath, validateYamlContent(err))
// If YAML linting fails here, it will always fail in the next block as well, so we should return here.
// fix https://github.com/helm/helm/issues/11391
if !linter.RunLinterRule(support.ErrorSev, fpath, validateYamlContent(err)) {
return
}
if yamlStruct != nil {
// NOTE: set to warnings to allow users to support out-of-date kubernetes
// 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",
Command: "echo -n os-arch",
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: "arm64", Command: "echo -n linux-arm64"},
{OperatingSystem: "linux", Architecture: "ppc64le", Command: "echo -n linux-ppc64le"},
@ -98,8 +98,8 @@ func TestPlatformPrepareCommand(t *testing.T) {
var osStrCmp string
os := runtime.GOOS
arch := runtime.GOARCH
if os == "linux" && arch == "i386" {
osStrCmp = "linux-i386"
if os == "linux" && arch == "386" {
osStrCmp = "linux-386"
} else if os == "linux" && arch == "amd64" {
osStrCmp = "linux-amd64"
} else if os == "linux" && arch == "arm64" {
@ -125,7 +125,7 @@ func TestPartialPlatformPrepareCommand(t *testing.T) {
Name: "test",
Command: "echo -n os-arch",
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"},
},
},
@ -134,7 +134,7 @@ func TestPartialPlatformPrepareCommand(t *testing.T) {
os := runtime.GOOS
arch := runtime.GOARCH
if os == "linux" {
osStrCmp = "linux-i386"
osStrCmp = "linux-386"
} else if os == "windows" && arch == "amd64" {
osStrCmp = "win-64"
} else {
@ -166,7 +166,7 @@ func TestNoMatchPrepareCommand(t *testing.T) {
Metadata: &Metadata{
Name: "test",
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 {
provData []byte
strictMode bool
test bool
}
)
@ -551,7 +552,9 @@ func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResu
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 {
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
func (c *Client) Tags(ref string) ([]string, error) {
parsedReference, err := registry.ParseReference(ref)

@ -23,8 +23,12 @@ import (
"io"
"net/http"
"strings"
"time"
helmtime "helm.sh/helm/v3/pkg/time"
"github.com/Masterminds/semver/v3"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
orascontext "oras.land/oras-go/pkg/context"
@ -35,6 +39,11 @@ import (
"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
func IsOCI(url string) bool {
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
}
// 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) {
// Bad bytes
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")
// Load a test chart
@ -227,20 +227,20 @@ func testPush(suite *TestSuite) {
// non-strict ref (chart name)
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)")
// 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")
// non-strict ref (chart version)
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)")
// 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")
// basic push, good ref
@ -249,7 +249,7 @@ func testPush(suite *TestSuite) {
meta, err = extractChartMeta(chartData)
suite.Nil(err, "no error extracting chart meta")
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")
_, err = suite.RegistryClient.Pull(ref)
@ -267,7 +267,7 @@ func testPush(suite *TestSuite) {
// push with prov
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")
_, err = suite.RegistryClient.Pull(ref)
@ -279,12 +279,12 @@ func testPush(suite *TestSuite) {
suite.Equal(ref, result.Ref)
suite.Equal(meta.Name, result.Chart.Meta.Name)
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(973), result.Chart.Size)
suite.Equal(int64(695), result.Prov.Size)
suite.Equal(
"sha256:af4c20a1df1431495e673c14ecfa3a2ba24839a7784349d6787cd67957392e83",
"sha256:b57e8ffd938c43253f30afedb3c209136288e6b3af3b33473e95ea3b805888e6",
result.Manifest.Digest)
suite.Equal(
"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580",
@ -352,12 +352,12 @@ func testPull(suite *TestSuite) {
suite.Equal(ref, result.Ref)
suite.Equal(meta.Name, result.Chart.Meta.Name)
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(973), result.Chart.Size)
suite.Equal(int64(695), result.Prov.Size)
suite.Equal(
"sha256:af4c20a1df1431495e673c14ecfa3a2ba24839a7784349d6787cd67957392e83",
"sha256:b57e8ffd938c43253f30afedb3c209136288e6b3af3b33473e95ea3b805888e6",
result.Manifest.Digest)
suite.Equal(
"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580",
@ -368,7 +368,7 @@ func testPull(suite *TestSuite) {
suite.Equal(
"sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256",
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))
suite.Equal("{\"name\":\"signtest\",\"version\":\"0.1.0\",\"description\":\"A Helm chart for Kubernetes\",\"apiVersion\":\"v1\"}",
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: "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.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 {
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" {
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) {

@ -202,7 +202,7 @@ func TestWriteFile(t *testing.T) {
t.Errorf("failed to create test-file (%v)", err)
}
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)
}

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