diff --git a/cmd/helm/package.go b/cmd/helm/package.go index 185442b20..b2989fb4f 100644 --- a/cmd/helm/package.go +++ b/cmd/helm/package.go @@ -132,6 +132,7 @@ func newPackageCmd(out io.Writer) *cobra.Command { f.BoolVar(&client.InsecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart download") f.BoolVar(&client.PlainHTTP, "plain-http", false, "use insecure HTTP connections for the chart download") f.StringVar(&client.CaFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") + addValueOptionsFlags(f, valueOpts) return cmd } diff --git a/cmd/helm/package_test.go b/cmd/helm/package_test.go index 107928765..bcf59da89 100644 --- a/cmd/helm/package_test.go +++ b/cmd/helm/package_test.go @@ -25,6 +25,7 @@ import ( "helm.sh/helm/v4/pkg/chart" "helm.sh/helm/v4/pkg/chart/loader" + "helm.sh/helm/v4/pkg/chartutil" ) func TestPackage(t *testing.T) { @@ -191,6 +192,173 @@ func TestSetAppVersion(t *testing.T) { } } +func TestAddValueOptions(t *testing.T) { + tests := []struct { + name string + args []string + expect string + chart string + err bool + }{ + { + name: "package without set value", + args: []string{}, + expect: `# The pod name +Name: my-alpine +List: [] +List1: +- a +`, + chart: "issue-3141", + err: false, + }, + { + name: "package set values", + args: []string{ + "--set Name=other", + }, + chart: "issue-3141", + expect: `# The pod name +Name: other +List: [] +List1: +- a +`, + err: false, + }, + { + name: "package append values", + args: []string{ + "--set Key=other", + }, + chart: "issue-3141", + expect: `# The pod name +Name: my-alpine +List: [] +List1: +- a +Key: other +`, + err: false, + }, + { + name: "package multiple values", + args: []string{ + "--set a=b,c=d", + }, + chart: "issue-3141", + expect: `# The pod name +Name: my-alpine +List: [] +List1: +- a +a: b +c: d +`, + err: false, + }, + { + name: "package more complex expressions", + args: []string{ + "--set outer.inner=value", + }, + chart: "issue-3141", + expect: `# The pod name +Name: my-alpine +List: [] +List1: +- a +outer: + inner: value +`, + err: false, + }, + { + name: "package lists values", + args: []string{ + "--set List={a,b,c}", + }, + chart: "issue-3141", + expect: `# The pod name +Name: my-alpine +List: +- a +- b +- c +List1: +- a +`, + err: false, + }, + { + name: "package replace values", + args: []string{ + "--set List1={a,b,c}", + }, + chart: "issue-3141", + expect: `# The pod name +Name: my-alpine +List: [] +List1: +- a +- b +- c +`, + err: false, + }, + { + name: "package no values file", + args: []string{ + "--set List={a,b,c}", + }, + chart: "issue-3141-without-values", + expect: `List: +- a +- b +- c +`, + err: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := t.TempDir() + t.Cleanup(func() { + os.RemoveAll(dir) + }) + + chartToPackage := "testdata/testcharts/" + tt.chart + cmd := fmt.Sprintf("package %s --destination=%s %s", chartToPackage, dir, strings.Join(tt.args, " ")) + _, _, err := executeActionCommand(cmd) + if err != nil { + if tt.err { + return + } + t.Fatal(err) + } + if tt.err { + t.Fatalf("Expected error in test %q", tt.name) + } + chartPath := filepath.Join(dir, tt.chart+"-0.1.0.tgz") + if fi, err := os.Stat(chartPath); err != nil { + t.Errorf("expected file %q, got err %q", chartPath, err) + } else if fi.Size() == 0 { + t.Errorf("file %q has zero bytes.", chartPath) + } + ch, err := loader.Load(chartPath) + if err != nil { + t.Fatalf("unexpected error loading packaged chart: %v", err) + } + for _, f := range ch.Raw { + if f.Name == chartutil.ValuesfileName && string(f.Data) != tt.expect { + t.Errorf("expected values %q, found %q", tt.expect, string(f.Data)) + } + } + }) + } +} + func TestPackageFileCompletion(t *testing.T) { checkFileCompletion(t, "package", true) checkFileCompletion(t, "package mypath", true) // Multiple paths can be given diff --git a/cmd/helm/testdata/testcharts/issue-3141-without-values/.helmignore b/cmd/helm/testdata/testcharts/issue-3141-without-values/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-3141-without-values/.helmignore @@ -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/ diff --git a/cmd/helm/testdata/testcharts/issue-3141-without-values/Chart.yaml b/cmd/helm/testdata/testcharts/issue-3141-without-values/Chart.yaml new file mode 100644 index 000000000..6c54f9c38 --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-3141-without-values/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: issue-3141-without-values +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" diff --git a/cmd/helm/testdata/testcharts/issue-3141-without-values/templates/NOTES.txt b/cmd/helm/testdata/testcharts/issue-3141-without-values/templates/NOTES.txt new file mode 100644 index 000000000..d88bd46ba --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-3141-without-values/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "issue-3141.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "issue-3141.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "issue-3141.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "issue-3141.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/cmd/helm/testdata/testcharts/issue-3141-without-values/templates/_helpers.tpl b/cmd/helm/testdata/testcharts/issue-3141-without-values/templates/_helpers.tpl new file mode 100644 index 000000000..a3d45b3f4 --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-3141-without-values/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "issue-3141.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "issue-3141.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "issue-3141.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "issue-3141.labels" -}} +helm.sh/chart: {{ include "issue-3141.chart" . }} +{{ include "issue-3141.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "issue-3141.selectorLabels" -}} +app.kubernetes.io/name: {{ include "issue-3141.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "issue-3141.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "issue-3141.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/cmd/helm/testdata/testcharts/issue-3141-without-values/templates/deployment.yaml b/cmd/helm/testdata/testcharts/issue-3141-without-values/templates/deployment.yaml new file mode 100644 index 000000000..5886ea3c7 --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-3141-without-values/templates/deployment.yaml @@ -0,0 +1,61 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "issue-3141.fullname" . }} + labels: + {{- include "issue-3141.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "issue-3141.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "issue-3141.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "issue-3141.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 80 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/cmd/helm/testdata/testcharts/issue-3141-without-values/templates/hpa.yaml b/cmd/helm/testdata/testcharts/issue-3141-without-values/templates/hpa.yaml new file mode 100644 index 000000000..932584198 --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-3141-without-values/templates/hpa.yaml @@ -0,0 +1,28 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "issue-3141.fullname" . }} + labels: + {{- include "issue-3141.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "issue-3141.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/cmd/helm/testdata/testcharts/issue-3141-without-values/templates/ingress.yaml b/cmd/helm/testdata/testcharts/issue-3141-without-values/templates/ingress.yaml new file mode 100644 index 000000000..42adb7723 --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-3141-without-values/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "issue-3141.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "issue-3141.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/cmd/helm/testdata/testcharts/issue-3141-without-values/templates/service.yaml b/cmd/helm/testdata/testcharts/issue-3141-without-values/templates/service.yaml new file mode 100644 index 000000000..298831005 --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-3141-without-values/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "issue-3141.fullname" . }} + labels: + {{- include "issue-3141.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "issue-3141.selectorLabels" . | nindent 4 }} diff --git a/cmd/helm/testdata/testcharts/issue-3141-without-values/templates/serviceaccount.yaml b/cmd/helm/testdata/testcharts/issue-3141-without-values/templates/serviceaccount.yaml new file mode 100644 index 000000000..2554e23b1 --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-3141-without-values/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "issue-3141.serviceAccountName" . }} + labels: + {{- include "issue-3141.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/cmd/helm/testdata/testcharts/issue-3141-without-values/templates/tests/test-connection.yaml b/cmd/helm/testdata/testcharts/issue-3141-without-values/templates/tests/test-connection.yaml new file mode 100644 index 000000000..edb80e0ee --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-3141-without-values/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "issue-3141.fullname" . }}-test-connection" + labels: + {{- include "issue-3141.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "issue-3141.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/cmd/helm/testdata/testcharts/issue-3141/.helmignore b/cmd/helm/testdata/testcharts/issue-3141/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-3141/.helmignore @@ -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/ diff --git a/cmd/helm/testdata/testcharts/issue-3141/Chart.yaml b/cmd/helm/testdata/testcharts/issue-3141/Chart.yaml new file mode 100644 index 000000000..2a2e1b023 --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-3141/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: issue-3141 +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" diff --git a/cmd/helm/testdata/testcharts/issue-3141/templates/NOTES.txt b/cmd/helm/testdata/testcharts/issue-3141/templates/NOTES.txt new file mode 100644 index 000000000..d88bd46ba --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-3141/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "issue-3141.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "issue-3141.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "issue-3141.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "issue-3141.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/cmd/helm/testdata/testcharts/issue-3141/templates/_helpers.tpl b/cmd/helm/testdata/testcharts/issue-3141/templates/_helpers.tpl new file mode 100644 index 000000000..a3d45b3f4 --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-3141/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "issue-3141.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "issue-3141.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "issue-3141.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "issue-3141.labels" -}} +helm.sh/chart: {{ include "issue-3141.chart" . }} +{{ include "issue-3141.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "issue-3141.selectorLabels" -}} +app.kubernetes.io/name: {{ include "issue-3141.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "issue-3141.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "issue-3141.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/cmd/helm/testdata/testcharts/issue-3141/templates/deployment.yaml b/cmd/helm/testdata/testcharts/issue-3141/templates/deployment.yaml new file mode 100644 index 000000000..5886ea3c7 --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-3141/templates/deployment.yaml @@ -0,0 +1,61 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "issue-3141.fullname" . }} + labels: + {{- include "issue-3141.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "issue-3141.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "issue-3141.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "issue-3141.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 80 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/cmd/helm/testdata/testcharts/issue-3141/templates/hpa.yaml b/cmd/helm/testdata/testcharts/issue-3141/templates/hpa.yaml new file mode 100644 index 000000000..932584198 --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-3141/templates/hpa.yaml @@ -0,0 +1,28 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "issue-3141.fullname" . }} + labels: + {{- include "issue-3141.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "issue-3141.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/cmd/helm/testdata/testcharts/issue-3141/templates/ingress.yaml b/cmd/helm/testdata/testcharts/issue-3141/templates/ingress.yaml new file mode 100644 index 000000000..42adb7723 --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-3141/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "issue-3141.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "issue-3141.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/cmd/helm/testdata/testcharts/issue-3141/templates/service.yaml b/cmd/helm/testdata/testcharts/issue-3141/templates/service.yaml new file mode 100644 index 000000000..298831005 --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-3141/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "issue-3141.fullname" . }} + labels: + {{- include "issue-3141.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "issue-3141.selectorLabels" . | nindent 4 }} diff --git a/cmd/helm/testdata/testcharts/issue-3141/templates/serviceaccount.yaml b/cmd/helm/testdata/testcharts/issue-3141/templates/serviceaccount.yaml new file mode 100644 index 000000000..2554e23b1 --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-3141/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "issue-3141.serviceAccountName" . }} + labels: + {{- include "issue-3141.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/cmd/helm/testdata/testcharts/issue-3141/templates/tests/test-connection.yaml b/cmd/helm/testdata/testcharts/issue-3141/templates/tests/test-connection.yaml new file mode 100644 index 000000000..edb80e0ee --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-3141/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "issue-3141.fullname" . }}-test-connection" + labels: + {{- include "issue-3141.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "issue-3141.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/cmd/helm/testdata/testcharts/issue-3141/values.yaml b/cmd/helm/testdata/testcharts/issue-3141/values.yaml new file mode 100644 index 000000000..bbd537b25 --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-3141/values.yaml @@ -0,0 +1,6 @@ +# The pod name +Name: my-alpine +List: [] + +List1: + - a diff --git a/go.mod b/go.mod index 68b07c1a9..ba15a21ec 100644 --- a/go.mod +++ b/go.mod @@ -46,6 +46,7 @@ require ( k8s.io/klog/v2 v2.130.1 k8s.io/kubectl v0.32.0 oras.land/oras-go v1.2.6 + sigs.k8s.io/kustomize/kyaml v0.18.1 sigs.k8s.io/yaml v1.4.0 ) @@ -184,6 +185,5 @@ require ( k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/kustomize/api v0.18.0 // indirect - sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect ) diff --git a/pkg/action/package.go b/pkg/action/package.go index 2e792a65c..e6efd55be 100644 --- a/pkg/action/package.go +++ b/pkg/action/package.go @@ -26,6 +26,10 @@ import ( "github.com/pkg/errors" "golang.org/x/term" + kyaml "sigs.k8s.io/kustomize/kyaml/yaml" + "sigs.k8s.io/kustomize/kyaml/yaml/merge2" + + "helm.sh/helm/v4/pkg/chart" "helm.sh/helm/v4/pkg/chart/loader" "helm.sh/helm/v4/pkg/chartutil" "helm.sh/helm/v4/pkg/provenance" @@ -61,7 +65,7 @@ func NewPackage() *Package { } // Run executes 'helm package' against the given chart and returns the path to the packaged chart. -func (p *Package) Run(path string, _ map[string]interface{}) (string, error) { +func (p *Package) Run(path string, vals map[string]interface{}) (string, error) { ch, err := loader.LoadDir(path) if err != nil { return "", err @@ -98,6 +102,51 @@ func (p *Package) Run(path string, _ map[string]interface{}) (string, error) { dest = p.Destination } + // If vals is not empty and the values.yaml file does not exist, then we need to generate a values.yaml file. + needToGenerateValuesFile := len(vals) != 0 + + src := &kyaml.Node{} + if err := src.Encode(vals); err != nil { + return "", err + } + + for _, f := range ch.Raw { + // Always run to ensure that the values.yaml file is formatted. + if f.Name == chartutil.ValuesfileName { + dest, err := kyaml.Parse(string(f.Data)) + if err != nil { + return "", err + } + + // In the case of saving yaml comments, merges fields from src into dest. + rnode, err := merge2.Merge(kyaml.NewRNode(src), dest, kyaml.MergeOptions{}) + if err != nil { + return "", err + } + + data, err := rnode.String() + if err != nil { + return "", err + } + f.Data = []byte(data) + + // After the file is formatted and merged, it is not necessary to generate a new values.yaml file. + needToGenerateValuesFile = false + } + } + + if needToGenerateValuesFile { + data, err := kyaml.Marshal(src) + if err != nil { + return "", err + } + + ch.Raw = append(ch.Raw, &chart.File{ + Name: chartutil.ValuesfileName, + Data: data, + }) + } + name, err := chartutil.Save(ch, dest) if err != nil { return "", errors.Wrap(err, "failed to save")