Merge branch 'main' into rebase-recursive-dependencies

Signed-off-by: Alik Khilazhev <7482065+alikhil@users.noreply.github.com>
pull/30855/head
Alik Khilazhev 6 months ago committed by GitHub
commit e0305dc233
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

4
.github/env vendored

@ -1,2 +1,2 @@
GOLANG_VERSION=1.24
GOLANGCI_LINT_VERSION=v2.1.0
GOLANG_VERSION=1.25
GOLANGCI_LINT_VERSION=v2.5.0

@ -47,7 +47,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@4dd16135b69a43b6c8efb853346f8437d92d3c93 # pinv3.26.6
uses: github/codeql-action/init@e296a935590eb16afc0c0108289f68c87e2a89a5 # pinv4.30.7
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@ -58,7 +58,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@4dd16135b69a43b6c8efb853346f8437d92d3c93 # pinv3.26.6
uses: github/codeql-action/autobuild@e296a935590eb16afc0c0108289f68c87e2a89a5 # pinv4.30.7
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@ -72,4 +72,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@4dd16135b69a43b6c8efb853346f8437d92d3c93 # pinv3.26.6
uses: github/codeql-action/analyze@e296a935590eb16afc0c0108289f68c87e2a89a5 # pinv4.30.7

@ -64,6 +64,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard (optional).
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@v3
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: results.sarif

@ -0,0 +1,48 @@
# AGENTS.md
## Overview
Helm is a package manager for Kubernetes written in Go, supporting v3 (stable) and v4 (unstable) APIs.
## Build & Test
```bash
make build # Build binary
make test # Run all tests (style + unit)
make test-unit # Unit tests only
make test-coverage # With coverage
make test-style # Linting
golangci-lint run # Direct linting
go test -run TestName # Specific test
```
## Code Structure
- `/cmd/helm/` - CLI entry point (Cobra-based)
- `/pkg/` - Public API
- `action/` - Core operations (install, upgrade, rollback)
- `chart/v2/` - Stable chart format
- `engine/` - Template rendering (Go templates + Sprig)
- `registry/` - OCI support
- `storage/` - Release backends (Secrets/ConfigMaps/SQL)
- `/internal/` - Private implementation
- `chart/v3/` - Next-gen chart format
## Development Guidelines
### Code Standards
- Use table-driven tests with testify
- Golden files in `testdata/` for complex output
- Mock Kubernetes clients for action tests
- All commits must include DCO sign-off: `git commit -s`
### Branching
- `main` - Helm v4 development
- `dev-v3` - Helm v3 stable (backport from main)
### Dependencies
- `k8s.io/client-go` - Kubernetes interaction
- `github.com/spf13/cobra` - CLI framework
- `github.com/Masterminds/sprig` - Template functions
### Key Patterns
- **Actions**: Operations in `/pkg/action/` use shared Configuration
- **Dual Chart Support**: v2 (stable) in `/pkg/`, v3 (dev) in `/internal/`
- **Storage Abstraction**: Pluggable release storage backends

@ -1,8 +1,8 @@
BINDIR := $(CURDIR)/bin
INSTALL_PATH ?= /usr/local/bin
DIST_DIRS := find * -type d -exec
TARGETS := darwin/amd64 darwin/arm64 linux/amd64 linux/386 linux/arm linux/arm64 linux/ppc64le linux/s390x linux/riscv64 windows/amd64 windows/arm64
TARGET_OBJS ?= darwin-amd64.tar.gz darwin-amd64.tar.gz.sha256 darwin-amd64.tar.gz.sha256sum darwin-arm64.tar.gz darwin-arm64.tar.gz.sha256 darwin-arm64.tar.gz.sha256sum linux-amd64.tar.gz linux-amd64.tar.gz.sha256 linux-amd64.tar.gz.sha256sum linux-386.tar.gz linux-386.tar.gz.sha256 linux-386.tar.gz.sha256sum linux-arm.tar.gz linux-arm.tar.gz.sha256 linux-arm.tar.gz.sha256sum linux-arm64.tar.gz linux-arm64.tar.gz.sha256 linux-arm64.tar.gz.sha256sum linux-ppc64le.tar.gz linux-ppc64le.tar.gz.sha256 linux-ppc64le.tar.gz.sha256sum linux-s390x.tar.gz linux-s390x.tar.gz.sha256 linux-s390x.tar.gz.sha256sum linux-riscv64.tar.gz linux-riscv64.tar.gz.sha256 linux-riscv64.tar.gz.sha256sum windows-amd64.zip windows-amd64.zip.sha256 windows-amd64.zip.sha256sum windows-arm64.zip windows-arm64.zip.sha256 windows-arm64.zip.sha256sum
TARGETS := darwin/amd64 darwin/arm64 linux/amd64 linux/386 linux/arm linux/arm64 linux/loong64 linux/ppc64le linux/s390x linux/riscv64 windows/amd64 windows/arm64
TARGET_OBJS ?= darwin-amd64.tar.gz darwin-amd64.tar.gz.sha256 darwin-amd64.tar.gz.sha256sum darwin-arm64.tar.gz darwin-arm64.tar.gz.sha256 darwin-arm64.tar.gz.sha256sum linux-amd64.tar.gz linux-amd64.tar.gz.sha256 linux-amd64.tar.gz.sha256sum linux-386.tar.gz linux-386.tar.gz.sha256 linux-386.tar.gz.sha256sum linux-arm.tar.gz linux-arm.tar.gz.sha256 linux-arm.tar.gz.sha256sum linux-arm64.tar.gz linux-arm64.tar.gz.sha256 linux-arm64.tar.gz.sha256sum linux-loong64.tar.gz linux-loong64.tar.gz.sha256 linux-loong64.tar.gz.sha256sum linux-ppc64le.tar.gz linux-ppc64le.tar.gz.sha256 linux-ppc64le.tar.gz.sha256sum linux-s390x.tar.gz linux-s390x.tar.gz.sha256 linux-s390x.tar.gz.sha256sum linux-riscv64.tar.gz linux-riscv64.tar.gz.sha256 linux-riscv64.tar.gz.sha256sum windows-amd64.zip windows-amd64.zip.sha256 windows-amd64.zip.sha256sum windows-arm64.zip windows-arm64.zip.sha256 windows-arm64.zip.sha256sum
BINNAME ?= helm
GOBIN = $(shell go env GOBIN)
@ -69,6 +69,8 @@ LDFLAGS += -X helm.sh/helm/v4/pkg/internal/v3/lint/rules.k8sVersionMajor=$(K8S_M
LDFLAGS += -X helm.sh/helm/v4/pkg/internal/v3/lint/rules.k8sVersionMinor=$(K8S_MODULES_MINOR_VER)
LDFLAGS += -X helm.sh/helm/v4/pkg/chart/common/util.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER)
LDFLAGS += -X helm.sh/helm/v4/pkg/chart/common/util.k8sVersionMinor=$(K8S_MODULES_MINOR_VER)
LDFLAGS += -X helm.sh/helm/v4/internal/version.kubeClientVersionMajor=$(K8S_MODULES_MAJOR_VER)
LDFLAGS += -X helm.sh/helm/v4/internal/version.kubeClientVersionMinor=$(K8S_MODULES_MINOR_VER)
.PHONY: all
all: build

@ -12,7 +12,7 @@ require (
github.com/Masterminds/vcs v1.13.3
github.com/ProtonMail/go-crypto v1.3.0
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
github.com/cyphar/filepath-securejoin v0.5.0
github.com/cyphar/filepath-securejoin v0.6.0
github.com/distribution/distribution/v3 v3.0.0
github.com/evanphx/json-patch/v5 v5.9.11
github.com/extism/go-sdk v1.7.1
@ -49,7 +49,7 @@ require (
k8s.io/klog/v2 v2.130.1
k8s.io/kubectl v0.34.1
oras.land/oras-go/v2 v2.6.0
sigs.k8s.io/controller-runtime v0.22.3
sigs.k8s.io/controller-runtime v0.22.4
sigs.k8s.io/kustomize/kyaml v0.20.1
sigs.k8s.io/yaml v1.6.0
)

@ -59,8 +59,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyphar/filepath-securejoin v0.5.0 h1:hIAhkRBMQ8nIeuVwcAoymp7MY4oherZdAxD+m0u9zaw=
github.com/cyphar/filepath-securejoin v0.5.0/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is=
github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@ -536,8 +536,8 @@ k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc=
oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o=
sigs.k8s.io/controller-runtime v0.22.3 h1:I7mfqz/a/WdmDCEnXmSPm8/b/yRTy6JsKKENTijTq8Y=
sigs.k8s.io/controller-runtime v0.22.3/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A=
sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I=

@ -19,6 +19,7 @@ import (
"path/filepath"
"regexp"
"strings"
"time"
"helm.sh/helm/v4/pkg/chart/common"
)
@ -47,9 +48,13 @@ type Chart struct {
Values map[string]interface{} `json:"values"`
// Schema is an optional JSON schema for imposing structure on Values
Schema []byte `json:"schema"`
// SchemaModTime the schema was last modified
SchemaModTime time.Time `json:"schemamodtime,omitempty"`
// Files are miscellaneous files in a chart archive,
// e.g. README, LICENSE, etc.
Files []*common.File `json:"files"`
// ModTime the chart metadata was last modified
ModTime time.Time `json:"modtime,omitzero"`
parent *Chart
dependencies []*Chart

@ -18,6 +18,7 @@ package v3
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/assert"
@ -25,27 +26,33 @@ import (
)
func TestCRDs(t *testing.T) {
modTime := time.Now()
chrt := Chart{
Files: []*common.File{
{
Name: "crds/foo.yaml",
Data: []byte("hello"),
Name: "crds/foo.yaml",
ModTime: modTime,
Data: []byte("hello"),
},
{
Name: "bar.yaml",
Data: []byte("hello"),
Name: "bar.yaml",
ModTime: modTime,
Data: []byte("hello"),
},
{
Name: "crds/foo/bar/baz.yaml",
Data: []byte("hello"),
Name: "crds/foo/bar/baz.yaml",
ModTime: modTime,
Data: []byte("hello"),
},
{
Name: "crdsfoo/bar/baz.yaml",
Data: []byte("hello"),
Name: "crdsfoo/bar/baz.yaml",
ModTime: modTime,
Data: []byte("hello"),
},
{
Name: "crds/README.md",
Data: []byte("# hello"),
Name: "crds/README.md",
ModTime: modTime,
Data: []byte("# hello"),
},
},
}
@ -61,8 +68,9 @@ func TestSaveChartNoRawData(t *testing.T) {
chrt := Chart{
Raw: []*common.File{
{
Name: "fhqwhgads.yaml",
Data: []byte("Everybody to the Limit"),
Name: "fhqwhgads.yaml",
ModTime: time.Now(),
Data: []byte("Everybody to the Limit"),
},
},
}
@ -163,27 +171,33 @@ func TestChartFullPath(t *testing.T) {
}
func TestCRDObjects(t *testing.T) {
modTime := time.Now()
chrt := Chart{
Files: []*common.File{
{
Name: "crds/foo.yaml",
Data: []byte("hello"),
Name: "crds/foo.yaml",
ModTime: modTime,
Data: []byte("hello"),
},
{
Name: "bar.yaml",
Data: []byte("hello"),
Name: "bar.yaml",
ModTime: modTime,
Data: []byte("hello"),
},
{
Name: "crds/foo/bar/baz.yaml",
Data: []byte("hello"),
Name: "crds/foo/bar/baz.yaml",
ModTime: modTime,
Data: []byte("hello"),
},
{
Name: "crdsfoo/bar/baz.yaml",
Data: []byte("hello"),
Name: "crdsfoo/bar/baz.yaml",
ModTime: modTime,
Data: []byte("hello"),
},
{
Name: "crds/README.md",
Data: []byte("# hello"),
Name: "crds/README.md",
ModTime: modTime,
Data: []byte("# hello"),
},
},
}
@ -193,16 +207,18 @@ func TestCRDObjects(t *testing.T) {
Name: "crds/foo.yaml",
Filename: "crds/foo.yaml",
File: &common.File{
Name: "crds/foo.yaml",
Data: []byte("hello"),
Name: "crds/foo.yaml",
ModTime: modTime,
Data: []byte("hello"),
},
},
{
Name: "crds/foo/bar/baz.yaml",
Filename: "crds/foo/bar/baz.yaml",
File: &common.File{
Name: "crds/foo/bar/baz.yaml",
Data: []byte("hello"),
Name: "crds/foo/bar/baz.yaml",
ModTime: modTime,
Data: []byte("hello"),
},
},
}

@ -126,7 +126,7 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string
linter.RunLinterRule(support.ErrorSev, fpath, validateAllowedExtension(fileName))
// We only apply the following lint rules to yaml files
if filepath.Ext(fileName) != ".yaml" || filepath.Ext(fileName) == ".yml" {
if !isYamlFileExtension(fileName) {
continue
}
@ -335,6 +335,11 @@ func validateListAnnotations(yamlStruct *k8sYamlStruct, manifest string) error {
return nil
}
func isYamlFileExtension(fileName string) bool {
ext := strings.ToLower(filepath.Ext(fileName))
return ext == ".yaml" || ext == ".yml"
}
// k8sYamlStruct stubs a Kubernetes YAML file.
type k8sYamlStruct struct {
APIVersion string `json:"apiVersion"`

@ -22,6 +22,7 @@ import (
"path/filepath"
"strings"
"testing"
"time"
chart "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/internal/chart/v3/lint/support"
@ -183,6 +184,7 @@ func TestValidateMetadataName(t *testing.T) {
}
func TestDeprecatedAPIFails(t *testing.T) {
modTime := time.Now()
mychart := chart.Chart{
Metadata: &chart.Metadata{
APIVersion: "v2",
@ -192,12 +194,14 @@ func TestDeprecatedAPIFails(t *testing.T) {
},
Templates: []*common.File{
{
Name: "templates/baddeployment.yaml",
Data: []byte("apiVersion: apps/v1beta1\nkind: Deployment\nmetadata:\n name: baddep\nspec: {selector: {matchLabels: {foo: bar}}}"),
Name: "templates/baddeployment.yaml",
ModTime: modTime,
Data: []byte("apiVersion: apps/v1beta1\nkind: Deployment\nmetadata:\n name: baddep\nspec: {selector: {matchLabels: {foo: bar}}}"),
},
{
Name: "templates/goodsecret.yaml",
Data: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: goodsecret"),
Name: "templates/goodsecret.yaml",
ModTime: modTime,
Data: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: goodsecret"),
},
},
}
@ -252,8 +256,9 @@ func TestStrictTemplateParsingMapError(t *testing.T) {
},
Templates: []*common.File{
{
Name: "templates/configmap.yaml",
Data: []byte(manifest),
Name: "templates/configmap.yaml",
ModTime: time.Now(),
Data: []byte(manifest),
},
},
}
@ -381,8 +386,9 @@ func TestEmptyWithCommentsManifests(t *testing.T) {
},
Templates: []*common.File{
{
Name: "templates/empty-with-comments.yaml",
Data: []byte("#@formatter:off\n"),
Name: "templates/empty-with-comments.yaml",
ModTime: time.Now(),
Data: []byte("#@formatter:off\n"),
},
},
}
@ -439,3 +445,23 @@ items:
t.Fatalf("List objects keep annotations should pass. got: %s", err)
}
}
func TestIsYamlFileExtension(t *testing.T) {
tests := []struct {
filename string
expected bool
}{
{"test.yaml", true},
{"test.yml", true},
{"test.txt", false},
{"test", false},
}
for _, test := range tests {
result := isYamlFileExtension(test.filename)
if result != test.expected {
t.Errorf("isYamlFileExtension(%s) = %v; want %v", test.filename, result, test.expected)
}
}
}

@ -111,7 +111,7 @@ func LoadDir(dir string) (*chart.Chart, error) {
data = bytes.TrimPrefix(data, utf8bom)
files = append(files, &archive.BufferedFile{Name: n, Data: data})
files = append(files, &archive.BufferedFile{Name: n, ModTime: fi.ModTime(), Data: data})
return nil
}
if err = sympath.Walk(topdir, walk); err != nil {

@ -25,6 +25,7 @@ import (
"maps"
"os"
"path/filepath"
"slices"
"strings"
utilyaml "k8s.io/apimachinery/pkg/util/yaml"
@ -71,11 +72,12 @@ func Load(name string) (*chart.Chart, error) {
func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
c := new(chart.Chart)
subcharts := make(map[string][]*archive.BufferedFile)
var subChartsKeys []string
// do not rely on assumed ordering of files in the chart and crash
// if Chart.yaml was not coming early enough to initialize metadata
for _, f := range files {
c.Raw = append(c.Raw, &common.File{Name: f.Name, Data: f.Data})
c.Raw = append(c.Raw, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data})
if f.Name == "Chart.yaml" {
if c.Metadata == nil {
c.Metadata = new(chart.Metadata)
@ -89,6 +91,7 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
if c.Metadata.APIVersion == "" {
c.Metadata.APIVersion = chart.APIVersionV3
}
c.ModTime = f.ModTime
}
}
for _, f := range files {
@ -109,20 +112,24 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
c.Values = values
case f.Name == "values.schema.json":
c.Schema = f.Data
c.SchemaModTime = f.ModTime
case strings.HasPrefix(f.Name, "templates/"):
c.Templates = append(c.Templates, &common.File{Name: f.Name, Data: f.Data})
c.Templates = append(c.Templates, &common.File{Name: f.Name, Data: f.Data, ModTime: f.ModTime})
case strings.HasPrefix(f.Name, "charts/"):
if filepath.Ext(f.Name) == ".prov" {
c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data})
c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data, ModTime: f.ModTime})
continue
}
fname := strings.TrimPrefix(f.Name, "charts/")
cname := strings.SplitN(fname, "/", 2)[0]
subcharts[cname] = append(subcharts[cname], &archive.BufferedFile{Name: fname, Data: f.Data})
if slices.Index(subChartsKeys, cname) == -1 {
subChartsKeys = append(subChartsKeys, cname)
}
subcharts[cname] = append(subcharts[cname], &archive.BufferedFile{Name: fname, ModTime: f.ModTime, Data: f.Data})
default:
c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data})
c.Files = append(c.Files, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data})
}
}

@ -184,9 +184,11 @@ func TestLoadFile(t *testing.T) {
}
func TestLoadFiles(t *testing.T) {
modTime := time.Now()
goodFiles := []*archive.BufferedFile{
{
Name: "Chart.yaml",
Name: "Chart.yaml",
ModTime: modTime,
Data: []byte(`apiVersion: v3
name: frobnitz
description: This is a frobnitz.
@ -207,20 +209,24 @@ icon: https://example.com/64x64.png
`),
},
{
Name: "values.yaml",
Data: []byte("var: some values"),
Name: "values.yaml",
ModTime: modTime,
Data: []byte("var: some values"),
},
{
Name: "values.schema.json",
Data: []byte("type: Values"),
Name: "values.schema.json",
ModTime: modTime,
Data: []byte("type: Values"),
},
{
Name: "templates/deployment.yaml",
Data: []byte("some deployment"),
Name: "templates/deployment.yaml",
ModTime: modTime,
Data: []byte("some deployment"),
},
{
Name: "templates/service.yaml",
Data: []byte("some service"),
Name: "templates/service.yaml",
ModTime: modTime,
Data: []byte("some service"),
},
}
@ -260,26 +266,32 @@ icon: https://example.com/64x64.png
// Test the order of file loading. The Chart.yaml file needs to come first for
// later comparison checks. See https://github.com/helm/helm/pull/8948
func TestLoadFilesOrder(t *testing.T) {
modTime := time.Now()
goodFiles := []*archive.BufferedFile{
{
Name: "requirements.yaml",
Data: []byte("dependencies:"),
Name: "requirements.yaml",
ModTime: modTime,
Data: []byte("dependencies:"),
},
{
Name: "values.yaml",
Data: []byte("var: some values"),
Name: "values.yaml",
ModTime: modTime,
Data: []byte("var: some values"),
},
{
Name: "templates/deployment.yaml",
Data: []byte("some deployment"),
Name: "templates/deployment.yaml",
ModTime: modTime,
Data: []byte("some deployment"),
},
{
Name: "templates/service.yaml",
Data: []byte("some service"),
Name: "templates/service.yaml",
ModTime: modTime,
Data: []byte("some service"),
},
{
Name: "Chart.yaml",
Name: "Chart.yaml",
ModTime: modTime,
Data: []byte(`apiVersion: v3
name: frobnitz
description: This is a frobnitz.

@ -218,9 +218,10 @@ httpRoute:
# value: v2
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
# For publicly distributed charts, we recommend leaving 'resources' commented out.
# This makes resource allocation a conscious choice for the user and increases the chances
# charts run on a wide range of environments from low-resource clusters like Minikube to those
# with strict resource policies. 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
@ -660,7 +661,7 @@ func CreateFrom(chartfile *chart.Metadata, dest, src string) error {
for _, template := range schart.Templates {
newData := transform(string(template.Data), schart.Name())
updatedTemplates = append(updatedTemplates, &common.File{Name: template.Name, Data: newData})
updatedTemplates = append(updatedTemplates, &common.File{Name: template.Name, ModTime: template.ModTime, Data: newData})
}
schart.Templates = updatedTemplates

@ -166,7 +166,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
if err != nil {
return err
}
if err := writeToTar(out, filepath.Join(base, ChartfileName), cdata); err != nil {
if err := writeToTar(out, filepath.Join(base, ChartfileName), cdata, c.ModTime); err != nil {
return err
}
@ -176,7 +176,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
if err != nil {
return err
}
if err := writeToTar(out, filepath.Join(base, "Chart.lock"), ldata); err != nil {
if err := writeToTar(out, filepath.Join(base, "Chart.lock"), ldata, c.Lock.Generated); err != nil {
return err
}
}
@ -184,7 +184,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
// Save values.yaml
for _, f := range c.Raw {
if f.Name == ValuesfileName {
if err := writeToTar(out, filepath.Join(base, ValuesfileName), f.Data); err != nil {
if err := writeToTar(out, filepath.Join(base, ValuesfileName), f.Data, f.ModTime); err != nil {
return err
}
}
@ -195,7 +195,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
if !json.Valid(c.Schema) {
return errors.New("invalid JSON in " + SchemafileName)
}
if err := writeToTar(out, filepath.Join(base, SchemafileName), c.Schema); err != nil {
if err := writeToTar(out, filepath.Join(base, SchemafileName), c.Schema, c.SchemaModTime); err != nil {
return err
}
}
@ -203,7 +203,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
// Save templates
for _, f := range c.Templates {
n := filepath.Join(base, f.Name)
if err := writeToTar(out, n, f.Data); err != nil {
if err := writeToTar(out, n, f.Data, f.ModTime); err != nil {
return err
}
}
@ -211,7 +211,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
// Save files
for _, f := range c.Files {
n := filepath.Join(base, f.Name)
if err := writeToTar(out, n, f.Data); err != nil {
if err := writeToTar(out, n, f.Data, f.ModTime); err != nil {
return err
}
}
@ -226,13 +226,16 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
}
// writeToTar writes a single file to a tar archive.
func writeToTar(out *tar.Writer, name string, body []byte) error {
func writeToTar(out *tar.Writer, name string, body []byte, modTime time.Time) error {
// TODO: Do we need to create dummy parent directory names if none exist?
h := &tar.Header{
Name: filepath.ToSlash(name),
Mode: 0644,
Size: int64(len(body)),
ModTime: time.Now(),
ModTime: modTime,
}
if h.ModTime.IsZero() {
h.ModTime = time.Now()
}
if err := out.WriteHeader(h); err != nil {
return err

@ -20,6 +20,8 @@ import (
"archive/tar"
"bytes"
"compress/gzip"
"crypto/sha256"
"fmt"
"io"
"os"
"path"
@ -49,7 +51,7 @@ func TestSave(t *testing.T) {
Digest: "testdigest",
},
Files: []*common.File{
{Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")},
{Name: "scheherazade/shahryar.txt", ModTime: time.Now(), Data: []byte("1,001 Nights")},
},
Schema: []byte("{\n \"title\": \"Values\"\n}"),
}
@ -115,7 +117,7 @@ func TestSave(t *testing.T) {
Digest: "testdigest",
},
Files: []*common.File{
{Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")},
{Name: "scheherazade/shahryar.txt", ModTime: time.Now(), Data: []byte("1,001 Nights")},
},
}
_, err := Save(c, tmp)
@ -141,7 +143,6 @@ func TestSavePreservesTimestamps(t *testing.T) {
// check will fail because `initialCreateTime` will be identical to the
// written timestamp for the files.
initialCreateTime := time.Now().Add(-1 * time.Second)
tmp := t.TempDir()
c := &chart.Chart{
@ -150,14 +151,16 @@ func TestSavePreservesTimestamps(t *testing.T) {
Name: "ahab",
Version: "1.2.3",
},
ModTime: initialCreateTime,
Values: map[string]interface{}{
"imageName": "testimage",
"imageId": 42,
},
Files: []*common.File{
{Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")},
{Name: "scheherazade/shahryar.txt", ModTime: initialCreateTime, Data: []byte("1,001 Nights")},
},
Schema: []byte("{\n \"title\": \"Values\"\n}"),
Schema: []byte("{\n \"title\": \"Values\"\n}"),
SchemaModTime: initialCreateTime,
}
where, err := Save(c, tmp)
@ -170,8 +173,9 @@ func TestSavePreservesTimestamps(t *testing.T) {
t.Fatalf("Failed to parse tar: %v", err)
}
roundedTime := initialCreateTime.Round(time.Second)
for _, header := range allHeaders {
if header.ModTime.Before(initialCreateTime) {
if !header.ModTime.Equal(roundedTime) {
t.Fatalf("File timestamp not preserved: %v", header.ModTime)
}
}
@ -213,6 +217,7 @@ func retrieveAllHeadersFromTar(path string) ([]*tar.Header, error) {
func TestSaveDir(t *testing.T) {
tmp := t.TempDir()
modTime := time.Now()
c := &chart.Chart{
Metadata: &chart.Metadata{
@ -221,10 +226,10 @@ func TestSaveDir(t *testing.T) {
Version: "1.2.3",
},
Files: []*common.File{
{Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")},
{Name: "scheherazade/shahryar.txt", ModTime: modTime, Data: []byte("1,001 Nights")},
},
Templates: []*common.File{
{Name: path.Join(TemplatesDir, "nested", "dir", "thing.yaml"), Data: []byte("abc: {{ .Values.abc }}")},
{Name: path.Join(TemplatesDir, "nested", "dir", "thing.yaml"), ModTime: modTime, Data: []byte("abc: {{ .Values.abc }}")},
},
}
@ -260,3 +265,92 @@ func TestSaveDir(t *testing.T) {
t.Fatalf("Did not get expected error for chart named %q", c.Name())
}
}
func TestRepeatableSave(t *testing.T) {
tmp := t.TempDir()
defer os.RemoveAll(tmp)
modTime := time.Date(2021, 9, 1, 20, 34, 58, 651387237, time.UTC)
tests := []struct {
name string
chart *chart.Chart
want string
}{
{
name: "Package 1 file",
chart: &chart.Chart{
Metadata: &chart.Metadata{
APIVersion: chart.APIVersionV3,
Name: "ahab",
Version: "1.2.3",
},
ModTime: modTime,
Lock: &chart.Lock{
Digest: "testdigest",
Generated: modTime,
},
Files: []*common.File{
{Name: "scheherazade/shahryar.txt", ModTime: modTime, Data: []byte("1,001 Nights")},
},
Schema: []byte("{\n \"title\": \"Values\"\n}"),
SchemaModTime: modTime,
},
want: "5bfea18cc3c8cbc265744bc32bffa9489a4dbe87d6b51b90f4255e4839d35e03",
},
{
name: "Package 2 files",
chart: &chart.Chart{
Metadata: &chart.Metadata{
APIVersion: chart.APIVersionV3,
Name: "ahab",
Version: "1.2.3",
},
ModTime: modTime,
Lock: &chart.Lock{
Digest: "testdigest",
Generated: modTime,
},
Files: []*common.File{
{Name: "scheherazade/shahryar.txt", ModTime: modTime, Data: []byte("1,001 Nights")},
{Name: "scheherazade/dunyazad.txt", ModTime: modTime, Data: []byte("1,001 Nights again")},
},
Schema: []byte("{\n \"title\": \"Values\"\n}"),
SchemaModTime: modTime,
},
want: "a240365c21e0a2f4a57873132a9b686566a612d08bcb3f20c9446bfff005ccce",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// create package
dest := path.Join(tmp, "newdir")
where, err := Save(test.chart, dest)
if err != nil {
t.Fatalf("Failed to save: %s", err)
}
// get shasum for package
result, err := sha256Sum(where)
if err != nil {
t.Fatalf("Failed to check shasum: %s", err)
}
// assert that the package SHA is what we wanted.
if result != test.want {
t.Errorf("FormatName() result = %v, want %v", result, test.want)
}
})
}
}
func sha256Sum(filePath string) (string, error) {
f, err := os.Open(filePath)
if err != nil {
return "", err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
}

@ -20,6 +20,7 @@ import (
"context"
"log/slog"
"os"
"sync/atomic"
)
// DebugEnabledFunc is a function type that determines if debug logging is enabled
@ -85,3 +86,37 @@ func NewLogger(debugEnabled DebugEnabledFunc) *slog.Logger {
return slog.New(dynamicHandler)
}
// LoggerSetterGetter is an interface that can set and get a logger
type LoggerSetterGetter interface {
// SetLogger sets a new slog.Handler
SetLogger(newHandler slog.Handler)
// Logger returns the slog.Logger created from the slog.Handler
Logger() *slog.Logger
}
type LogHolder struct {
// logger is an atomic.Pointer[slog.Logger] to store the slog.Logger
// We use atomic.Pointer for thread safety
logger atomic.Pointer[slog.Logger]
}
// Logger returns the logger for the LogHolder. If nil, returns slog.Default().
func (l *LogHolder) Logger() *slog.Logger {
if lg := l.logger.Load(); lg != nil {
return lg
}
return slog.New(slog.DiscardHandler) // Should never be reached
}
// SetLogger sets the logger for the LogHolder. If nil, sets the default logger.
func (l *LogHolder) SetLogger(newHandler slog.Handler) {
if newHandler == nil {
l.logger.Store(slog.New(slog.DiscardHandler)) // Assume nil as discarding logs
return
}
l.logger.Store(slog.New(newHandler))
}
// Ensure LogHolder implements LoggerSetterGetter
var _ LoggerSetterGetter = &LogHolder{}

@ -0,0 +1,115 @@
/*
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 logging
import (
"bytes"
"log/slog"
"testing"
"github.com/stretchr/testify/assert"
)
func TestLogHolder_Logger(t *testing.T) {
t.Run("should return new logger with a then set handler", func(t *testing.T) {
holder := &LogHolder{}
buf := &bytes.Buffer{}
handler := slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug})
holder.SetLogger(handler)
logger := holder.Logger()
assert.NotNil(t, logger)
// Test that the logger works
logger.Info("test message")
assert.Contains(t, buf.String(), "test message")
})
t.Run("should return discard - defaultlogger when no handler is set", func(t *testing.T) {
holder := &LogHolder{}
logger := holder.Logger()
assert.Equal(t, slog.Handler(slog.DiscardHandler), logger.Handler())
})
}
func TestLogHolder_SetLogger(t *testing.T) {
t.Run("sets logger with valid handler", func(t *testing.T) {
holder := &LogHolder{}
buf := &bytes.Buffer{}
handler := slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug})
holder.SetLogger(handler)
logger := holder.Logger()
assert.NotNil(t, logger)
// Compare the handler directly
assert.Equal(t, handler, logger.Handler())
})
t.Run("sets discard logger with nil handler", func(t *testing.T) {
holder := &LogHolder{}
holder.SetLogger(nil)
logger := holder.Logger()
assert.NotNil(t, logger)
assert.Equal(t, slog.Handler(slog.DiscardHandler), logger.Handler())
})
t.Run("can replace existing logger", func(t *testing.T) {
holder := &LogHolder{}
// Set first logger
buf1 := &bytes.Buffer{}
handler1 := slog.NewTextHandler(buf1, &slog.HandlerOptions{Level: slog.LevelDebug})
holder.SetLogger(handler1)
logger1 := holder.Logger()
assert.Equal(t, handler1, logger1.Handler())
// Replace with second logger
buf2 := &bytes.Buffer{}
handler2 := slog.NewTextHandler(buf2, &slog.HandlerOptions{Level: slog.LevelDebug})
holder.SetLogger(handler2)
logger2 := holder.Logger()
assert.Equal(t, handler2, logger2.Handler())
})
}
func TestLogHolder_InterfaceCompliance(t *testing.T) {
t.Run("implements LoggerSetterGetter interface", func(_ *testing.T) {
var _ LoggerSetterGetter = &LogHolder{}
})
t.Run("interface methods work correctly", func(t *testing.T) {
var holder LoggerSetterGetter = &LogHolder{}
buf := &bytes.Buffer{}
handler := slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug})
holder.SetLogger(handler)
logger := holder.Logger()
assert.NotNil(t, logger)
assert.Equal(t, handler, logger.Handler())
})
}

@ -85,10 +85,10 @@ func NewExtractor(source string) (Extractor, error) {
//
// - The character `:` is considered illegal because it is a separator on UNIX and a
// drive designator on Windows.
// - The path component `..` is considered suspicions, and therefore illegal
// - The path component `..` is considered suspicious, and therefore illegal
// - The character \ (backslash) is treated as a path separator and is converted to /.
// - Beginning a path with a path separator is illegal
// - Rudimentary symlink protects are offered by SecureJoin.
// - Rudimentary symlink protections are offered by SecureJoin.
func cleanJoin(root, dest string) (string, error) {
// On Windows, this is a drive separator. On UNIX-like, this is the path list separator.

@ -139,18 +139,24 @@ func Update(i Installer) error {
}
// NewForSource determines the correct Installer for the given source.
func NewForSource(source, version string) (Installer, error) {
// Check if source is an OCI registry reference
func NewForSource(source, version string) (installer Installer, err error) {
if strings.HasPrefix(source, fmt.Sprintf("%s://", registry.OCIScheme)) {
return NewOCIInstaller(source)
}
// Check if source is a local directory
if isLocalReference(source) {
return NewLocalInstaller(source)
// Source is an OCI registry reference
installer, err = NewOCIInstaller(source)
} else if isLocalReference(source) {
// Source is a local directory
installer, err = NewLocalInstaller(source)
} else if isRemoteHTTPArchive(source) {
return NewHTTPInstaller(source)
installer, err = NewHTTPInstaller(source)
} else {
installer, err = NewVCSInstaller(source, version)
}
if err != nil {
return installer, fmt.Errorf("cannot get information about plugin source %q (if it's a local directory, does it exist?), last error was: %w", source, err)
}
return NewVCSInstaller(source, version)
return
}
// FindSource determines the correct Installer for the given source.

@ -29,8 +29,8 @@ import (
"helm.sh/helm/v4/pkg/helmpath"
)
// ErrPluginNotAFolder indicates that the plugin path is not a folder.
var ErrPluginNotAFolder = errors.New("expected plugin to be a folder")
// ErrPluginNotADirectory indicates that the plugin path is not a directory.
var ErrPluginNotADirectory = errors.New("expected plugin to be a directory (containing a file 'plugin.yaml')")
// LocalInstaller installs plugins from the filesystem.
type LocalInstaller struct {
@ -91,7 +91,7 @@ func (i *LocalInstaller) installFromDirectory() error {
return err
}
if !stat.IsDir() {
return ErrPluginNotAFolder
return ErrPluginNotADirectory
}
if !isPlugin(i.Source) {

@ -64,7 +64,7 @@ func TestLocalInstallerNotAFolder(t *testing.T) {
if err == nil {
t.Fatal("expected error")
}
if err != ErrPluginNotAFolder {
if err != ErrPluginNotADirectory {
t.Fatalf("expected error to equal: %q", err)
}
}

@ -73,27 +73,27 @@ type pluginTypeMeta struct {
var pluginTypes = []pluginTypeMeta{
{
pluginType: "test/v1",
inputType: reflect.TypeOf(schema.InputMessageTestV1{}),
outputType: reflect.TypeOf(schema.OutputMessageTestV1{}),
configType: reflect.TypeOf(schema.ConfigTestV1{}),
inputType: reflect.TypeFor[schema.InputMessageTestV1](),
outputType: reflect.TypeFor[schema.OutputMessageTestV1](),
configType: reflect.TypeFor[schema.ConfigTestV1](),
},
{
pluginType: "cli/v1",
inputType: reflect.TypeOf(schema.InputMessageCLIV1{}),
outputType: reflect.TypeOf(schema.OutputMessageCLIV1{}),
configType: reflect.TypeOf(schema.ConfigCLIV1{}),
inputType: reflect.TypeFor[schema.InputMessageCLIV1](),
outputType: reflect.TypeFor[schema.OutputMessageCLIV1](),
configType: reflect.TypeFor[schema.ConfigCLIV1](),
},
{
pluginType: "getter/v1",
inputType: reflect.TypeOf(schema.InputMessageGetterV1{}),
outputType: reflect.TypeOf(schema.OutputMessageGetterV1{}),
configType: reflect.TypeOf(schema.ConfigGetterV1{}),
inputType: reflect.TypeFor[schema.InputMessageGetterV1](),
outputType: reflect.TypeFor[schema.OutputMessageGetterV1](),
configType: reflect.TypeFor[schema.ConfigGetterV1](),
},
{
pluginType: "postrenderer/v1",
inputType: reflect.TypeOf(schema.InputMessagePostRendererV1{}),
outputType: reflect.TypeOf(schema.OutputMessagePostRendererV1{}),
configType: reflect.TypeOf(schema.ConfigPostRendererV1{}),
inputType: reflect.TypeFor[schema.InputMessagePostRendererV1](),
outputType: reflect.TypeFor[schema.OutputMessagePostRendererV1](),
configType: reflect.TypeFor[schema.ConfigPostRendererV1](),
},
}

@ -71,7 +71,7 @@ func TestSubprocessPluginRuntime(t *testing.T) {
output, err := p.Invoke(t.Context(), &Input{
Message: schema.InputMessageCLIV1{
ExtraArgs: []string{"arg1", "arg2"},
//Env: []string{"FOO=bar"},
// Env: []string{"FOO=bar"},
},
})

@ -70,7 +70,7 @@ func symwalk(path string, info os.FileInfo, walkFn filepath.WalkFunc) error {
if err != nil {
return fmt.Errorf("error evaluating symlink %s: %w", path, err)
}
//This log message is to highlight a symlink that is being used within a chart, symlinks can be used for nefarious reasons.
// This log message is to highlight a symlink that is being used within a chart, symlinks can be used for nefarious reasons.
slog.Info("found symbolic link in path. Contents of linked file included and used", "path", path, "resolved", resolved)
if info, err = os.Lstat(resolved); err != nil {
return err

@ -26,16 +26,16 @@ import (
)
type TLSConfigOptions struct {
insecureSkipTLSverify bool
insecureSkipTLSVerify bool
certPEMBlock, keyPEMBlock []byte
caPEMBlock []byte
}
type TLSConfigOption func(options *TLSConfigOptions) error
func WithInsecureSkipVerify(insecureSkipTLSverify bool) TLSConfigOption {
func WithInsecureSkipVerify(insecureSkipTLSVerify bool) TLSConfigOption {
return func(options *TLSConfigOptions) error {
options.insecureSkipTLSverify = insecureSkipTLSverify
options.insecureSkipTLSVerify = insecureSkipTLSVerify
return nil
}
@ -97,7 +97,7 @@ func NewTLSConfig(options ...TLSConfigOption) (*tls.Config, error) {
}
config := tls.Config{
InsecureSkipVerify: to.insecureSkipTLSverify,
InsecureSkipVerify: to.insecureSkipTLSVerify,
}
if len(to.certPEMBlock) > 0 && len(to.keyPEMBlock) > 0 {

@ -42,11 +42,11 @@ func TestNewTLSConfig(t *testing.T) {
certFile := testfile(t, testCertFile)
keyFile := testfile(t, testKeyFile)
caCertFile := testfile(t, testCaCertFile)
insecureSkipTLSverify := false
insecureSkipTLSVerify := false
{
cfg, err := NewTLSConfig(
WithInsecureSkipVerify(insecureSkipTLSverify),
WithInsecureSkipVerify(insecureSkipTLSVerify),
WithCertKeyPairFiles(certFile, keyFile),
WithCAFile(caCertFile),
)
@ -66,7 +66,7 @@ func TestNewTLSConfig(t *testing.T) {
}
{
cfg, err := NewTLSConfig(
WithInsecureSkipVerify(insecureSkipTLSverify),
WithInsecureSkipVerify(insecureSkipTLSVerify),
WithCAFile(caCertFile),
)
if err != nil {
@ -86,7 +86,7 @@ func TestNewTLSConfig(t *testing.T) {
{
cfg, err := NewTLSConfig(
WithInsecureSkipVerify(insecureSkipTLSverify),
WithInsecureSkipVerify(insecureSkipTLSVerify),
WithCertKeyPairFiles(certFile, keyFile),
)
if err != nil {

@ -18,6 +18,7 @@ package version // import "helm.sh/helm/v4/internal/version"
import (
"flag"
"fmt"
"runtime"
"strings"
)
@ -37,6 +38,11 @@ var (
gitCommit = ""
// gitTreeState is the state of the git tree
gitTreeState = ""
// The Kubernetes version can be set by LDFLAGS. In order to do that the value
// must be a string.
kubeClientVersionMajor = ""
kubeClientVersionMinor = ""
)
// BuildInfo describes the compile time information.
@ -49,6 +55,8 @@ type BuildInfo struct {
GitTreeState string `json:"git_tree_state,omitempty"`
// GoVersion is the version of the Go compiler used.
GoVersion string `json:"go_version,omitempty"`
// KubeClientVersion is the version of client-go Helm was build with
KubeClientVersion string `json:"kube_client_version"`
}
// GetVersion returns the semver string of the version
@ -67,10 +75,11 @@ func GetUserAgent() string {
// Get returns build info
func Get() BuildInfo {
v := BuildInfo{
Version: GetVersion(),
GitCommit: gitCommit,
GitTreeState: gitTreeState,
GoVersion: runtime.Version(),
Version: GetVersion(),
GitCommit: gitCommit,
GitTreeState: gitTreeState,
GoVersion: runtime.Version(),
KubeClientVersion: fmt.Sprintf("v%s.%s", kubeClientVersionMajor, kubeClientVersionMinor),
}
// HACK(bacongobbler): strip out GoVersion during a test run for consistent test output

@ -40,6 +40,7 @@ import (
"sigs.k8s.io/kustomize/kyaml/kio"
kyaml "sigs.k8s.io/kustomize/kyaml/yaml"
"helm.sh/helm/v4/internal/logging"
"helm.sh/helm/v4/pkg/chart/common"
chart "helm.sh/helm/v4/pkg/chart/v2"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
@ -109,7 +110,32 @@ type Configuration struct {
// HookOutputFunc called with container name and returns and expects writer that will receive the log output.
HookOutputFunc func(namespace, pod, container string) io.Writer
// Mutex is an exclusive lock for concurrent access to the action
mutex sync.Mutex
// Embed a LogHolder to provide logger functionality
logging.LogHolder
}
type ConfigurationOption func(c *Configuration)
// Override the default logging handler
// If unspecified, the default logger will be used
func ConfigurationSetLogger(h slog.Handler) ConfigurationOption {
return func(c *Configuration) {
c.SetLogger(h)
}
}
func NewConfiguration(options ...ConfigurationOption) *Configuration {
c := &Configuration{}
c.SetLogger(slog.Default().Handler())
for _, o := range options {
o(c)
}
return c
}
const (
@ -376,8 +402,8 @@ func (cfg *Configuration) getCapabilities() (*common.Capabilities, error) {
apiVersions, err := GetVersionSet(dc)
if err != nil {
if discovery.IsGroupDiscoveryFailedError(err) {
slog.Warn("the kubernetes server has an orphaned API service", slog.Any("error", err))
slog.Warn("to fix this, kubectl delete apiservice <service-name>")
cfg.Logger().Warn("the kubernetes server has an orphaned API service", slog.Any("error", err))
cfg.Logger().Warn("to fix this, kubectl delete apiservice <service-name>")
} else {
return nil, fmt.Errorf("could not get apiVersions from Kubernetes: %w", err)
}
@ -476,13 +502,14 @@ func GetVersionSet(client discovery.ServerResourcesInterface) (common.VersionSet
// recordRelease with an update operation in case reuse has been set.
func (cfg *Configuration) recordRelease(r *release.Release) {
if err := cfg.Releases.Update(r); err != nil {
slog.Warn("failed to update release", "name", r.Name, "revision", r.Version, slog.Any("error", err))
cfg.Logger().Warn("failed to update release", "name", r.Name, "revision", r.Version, slog.Any("error", err))
}
}
// Init initializes the action configuration
func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namespace, helmDriver string) error {
kc := kube.New(getter)
kc.SetLogger(cfg.Logger().Handler())
lazyClient := &lazyClient{
namespace: namespace,
@ -493,9 +520,11 @@ func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namesp
switch helmDriver {
case "secret", "secrets", "":
d := driver.NewSecrets(newSecretClient(lazyClient))
d.SetLogger(cfg.Logger().Handler())
store = storage.Init(d)
case "configmap", "configmaps":
d := driver.NewConfigMaps(newConfigMapClient(lazyClient))
d.SetLogger(cfg.Logger().Handler())
store = storage.Init(d)
case "memory":
var d *driver.Memory
@ -510,6 +539,7 @@ func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namesp
if d == nil {
d = driver.NewMemory()
}
d.SetLogger(cfg.Logger().Handler())
d.SetNamespace(namespace)
store = storage.Init(d)
case "sql":
@ -520,6 +550,7 @@ func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namesp
if err != nil {
return fmt.Errorf("unable to instantiate SQL driver: %w", err)
}
d.SetLogger(cfg.Logger().Handler())
store = storage.Init(d)
default:
return fmt.Errorf("unknown driver %q", helmDriver)

@ -123,9 +123,10 @@ type chartOptions struct {
type chartOption func(*chartOptions)
func buildChart(opts ...chartOption) *chart.Chart {
modTime := time.Now()
defaultTemplates := []*common.File{
{Name: "templates/hello", Data: []byte("hello: world")},
{Name: "templates/hooks", Data: []byte(manifestWithHook)},
{Name: "templates/hello", ModTime: modTime, Data: []byte("hello: world")},
{Name: "templates/hooks", ModTime: modTime, Data: []byte(manifestWithHook)},
}
return buildChartWithTemplates(defaultTemplates, opts...)
}
@ -181,8 +182,9 @@ func withValues(values map[string]interface{}) chartOption {
func withNotes(notes string) chartOption {
return func(opts *chartOptions) {
opts.Templates = append(opts.Templates, &common.File{
Name: "templates/NOTES.txt",
Data: []byte(notes),
Name: "templates/NOTES.txt",
ModTime: time.Now(),
Data: []byte(notes),
})
}
}
@ -201,12 +203,13 @@ func withMetadataDependency(dependency chart.Dependency) chartOption {
func withSampleTemplates() chartOption {
return func(opts *chartOptions) {
modTime := time.Now()
sampleTemplates := []*common.File{
// This adds basic templates and partials.
{Name: "templates/goodbye", Data: []byte("goodbye: world")},
{Name: "templates/empty", Data: []byte("")},
{Name: "templates/with-partials", Data: []byte(`hello: {{ template "_planet" . }}`)},
{Name: "templates/partials/_planet", Data: []byte(`{{define "_planet"}}Earth{{end}}`)},
{Name: "templates/goodbye", ModTime: modTime, Data: []byte("goodbye: world")},
{Name: "templates/empty", ModTime: modTime, Data: []byte("")},
{Name: "templates/with-partials", ModTime: modTime, Data: []byte(`hello: {{ template "_planet" . }}`)},
{Name: "templates/partials/_planet", ModTime: modTime, Data: []byte(`{{define "_planet"}}Earth{{end}}`)},
}
opts.Templates = append(opts.Templates, sampleTemplates...)
}
@ -214,20 +217,21 @@ func withSampleTemplates() chartOption {
func withSampleSecret() chartOption {
return func(opts *chartOptions) {
sampleSecret := &common.File{Name: "templates/secret.yaml", Data: []byte("apiVersion: v1\nkind: Secret\n")}
sampleSecret := &common.File{Name: "templates/secret.yaml", ModTime: time.Now(), Data: []byte("apiVersion: v1\nkind: Secret\n")}
opts.Templates = append(opts.Templates, sampleSecret)
}
}
func withSampleIncludingIncorrectTemplates() chartOption {
return func(opts *chartOptions) {
modTime := time.Now()
sampleTemplates := []*common.File{
// This adds basic templates and partials.
{Name: "templates/goodbye", Data: []byte("goodbye: world")},
{Name: "templates/empty", Data: []byte("")},
{Name: "templates/incorrect", Data: []byte("{{ .Values.bad.doh }}")},
{Name: "templates/with-partials", Data: []byte(`hello: {{ template "_planet" . }}`)},
{Name: "templates/partials/_planet", Data: []byte(`{{define "_planet"}}Earth{{end}}`)},
{Name: "templates/goodbye", ModTime: modTime, Data: []byte("goodbye: world")},
{Name: "templates/empty", ModTime: modTime, Data: []byte("")},
{Name: "templates/incorrect", ModTime: modTime, Data: []byte("{{ .Values.bad.doh }}")},
{Name: "templates/with-partials", ModTime: modTime, Data: []byte(`hello: {{ template "_planet" . }}`)},
{Name: "templates/partials/_planet", ModTime: modTime, Data: []byte(`{{define "_planet"}}Earth{{end}}`)},
}
opts.Templates = append(opts.Templates, sampleTemplates...)
}
@ -236,7 +240,7 @@ func withSampleIncludingIncorrectTemplates() chartOption {
func withMultipleManifestTemplate() chartOption {
return func(opts *chartOptions) {
sampleTemplates := []*common.File{
{Name: "templates/rbac", Data: []byte(rbacManifests)},
{Name: "templates/rbac", ModTime: time.Now(), Data: []byte(rbacManifests)},
}
opts.Templates = append(opts.Templates, sampleTemplates...)
}
@ -344,7 +348,7 @@ func TestConfiguration_Init(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &Configuration{}
cfg := NewConfiguration()
actualErr := cfg.Init(nil, "default", tt.helmDriver)
if tt.expectErr {
@ -853,7 +857,7 @@ func TestRenderResources_PostRenderer_MergeError(t *testing.T) {
Version: "0.1.0",
},
Templates: []*common.File{
{Name: "templates/invalid", Data: []byte("invalid: yaml: content:")},
{Name: "templates/invalid", ModTime: time.Now(), Data: []byte("invalid: yaml: content:")},
},
}
values := map[string]interface{}{}

@ -17,8 +17,6 @@ limitations under the License.
package action
import (
"log/slog"
"fmt"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
@ -55,6 +53,6 @@ func (h *History) Run(name string) ([]release.Releaser, error) {
return nil, fmt.Errorf("release name is invalid: %s", name)
}
slog.Debug("getting history for release", "release", name)
h.cfg.Logger().Debug("getting history for release", "release", name)
return h.cfg.Releases.History(name)
}

@ -180,9 +180,10 @@ func runInstallForHooksWithSuccess(t *testing.T, manifest, expectedNamespace str
outBuffer := &bytes.Buffer{}
instAction.cfg.KubeClient = &kubefake.PrintingKubeClient{Out: io.Discard, LogOutput: outBuffer}
modTime := time.Now()
templates := []*common.File{
{Name: "templates/hello", Data: []byte("hello: world")},
{Name: "templates/hooks", Data: []byte(manifest)},
{Name: "templates/hello", ModTime: modTime, Data: []byte("hello: world")},
{Name: "templates/hooks", ModTime: modTime, Data: []byte(manifest)},
}
vals := map[string]interface{}{}
@ -209,9 +210,10 @@ func runInstallForHooksWithFailure(t *testing.T, manifest, expectedNamespace str
outBuffer := &bytes.Buffer{}
failingClient.PrintingKubeClient = kubefake.PrintingKubeClient{Out: io.Discard, LogOutput: outBuffer}
modTime := time.Now()
templates := []*common.File{
{Name: "templates/hello", Data: []byte("hello: world")},
{Name: "templates/hooks", Data: []byte(manifest)},
{Name: "templates/hello", ModTime: modTime, Data: []byte("hello: world")},
{Name: "templates/hooks", ModTime: modTime, Data: []byte(manifest)},
}
vals := map[string]interface{}{}
@ -408,3 +410,35 @@ data:
})
}
}
func TestConfiguration_hookSetDeletePolicy(t *testing.T) {
tests := map[string]struct {
policies []release.HookDeletePolicy
expected []release.HookDeletePolicy
}{
"no polices specified result in the default policy": {
policies: nil,
expected: []release.HookDeletePolicy{
release.HookBeforeHookCreation,
},
},
"unknown policy is untouched": {
policies: []release.HookDeletePolicy{
release.HookDeletePolicy("never"),
},
expected: []release.HookDeletePolicy{
release.HookDeletePolicy("never"),
},
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
cfg := &Configuration{}
h := &release.Hook{
DeletePolicies: tt.policies,
}
cfg.hookSetDeletePolicy(h)
assert.Equal(t, tt.expected, h.DeletePolicies)
})
}
}

@ -140,7 +140,7 @@ type ChartPathOptions struct {
CaFile string // --ca-file
CertFile string // --cert-file
KeyFile string // --key-file
InsecureSkipTLSverify bool // --insecure-skip-verify
InsecureSkipTLSVerify bool // --insecure-skip-verify
PlainHTTP bool // --plain-http
Keyring string // --keyring
Password string // --password
@ -194,7 +194,7 @@ func (i *Install) installCRDs(crds []chart.CRD) error {
// If the error is CRD already exists, continue.
if apierrors.IsAlreadyExists(err) {
crdName := res[0].Name
slog.Debug("CRD is already present. Skipping", "crd", crdName)
i.cfg.Logger().Debug("CRD is already present. Skipping", "crd", crdName)
continue
}
return fmt.Errorf("failed to install CRD %s: %w", obj.Name, err)
@ -222,7 +222,7 @@ func (i *Install) installCRDs(crds []chart.CRD) error {
return err
}
slog.Debug("clearing discovery cache")
i.cfg.Logger().Debug("clearing discovery cache")
discoveryClient.Invalidate()
_, _ = discoveryClient.ServerGroups()
@ -235,7 +235,7 @@ func (i *Install) installCRDs(crds []chart.CRD) error {
return err
}
if resettable, ok := restMapper.(meta.ResettableRESTMapper); ok {
slog.Debug("clearing REST mapper cache")
i.cfg.Logger().Debug("clearing REST mapper cache")
resettable.Reset()
}
}
@ -268,24 +268,24 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st
if interactWithServer(i.DryRunStrategy) {
if err := i.cfg.KubeClient.IsReachable(); err != nil {
slog.Error(fmt.Sprintf("cluster reachability check failed: %v", err))
i.cfg.Logger().Error(fmt.Sprintf("cluster reachability check failed: %v", err))
return nil, fmt.Errorf("cluster reachability check failed: %w", err)
}
}
// HideSecret must be used with dry run. Otherwise, return an error.
if !isDryRun(i.DryRunStrategy) && i.HideSecret {
slog.Error("hiding Kubernetes secrets requires a dry-run mode")
i.cfg.Logger().Error("hiding Kubernetes secrets requires a dry-run mode")
return nil, errors.New("hiding Kubernetes secrets requires a dry-run mode")
}
if err := i.availableName(); err != nil {
slog.Error("release name check failed", slog.Any("error", err))
i.cfg.Logger().Error("release name check failed", slog.Any("error", err))
return nil, fmt.Errorf("release name check failed: %w", err)
}
if err := chartutil.ProcessDependencies(chrt, vals); err != nil {
slog.Error("chart dependencies processing failed", slog.Any("error", err))
i.cfg.Logger().Error("chart dependencies processing failed", slog.Any("error", err))
return nil, fmt.Errorf("chart dependencies processing failed: %w", err)
}
@ -294,7 +294,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st
if crds := chrt.CRDObjects(); interactWithServer(i.DryRunStrategy) && !i.SkipCRDs && len(crds) > 0 {
// On dry run, bail here
if isDryRun(i.DryRunStrategy) {
slog.Warn("This chart or one of its subcharts contains CRDs. Rendering may fail or contain inaccuracies.")
i.cfg.Logger().Warn("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
}
@ -314,7 +314,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st
mem.SetNamespace(i.Namespace)
i.cfg.Releases = storage.Init(mem)
} else if interactWithServer(i.DryRunStrategy) && len(i.APIVersions) > 0 {
slog.Debug("API Version list given outside of client only mode, this list will be ignored")
i.cfg.Logger().Debug("API Version list given outside of client only mode, this list will be ignored")
}
// Make sure if RollbackOnFailure is set, that wait is set as well. This makes it so
@ -540,7 +540,7 @@ func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.Resource
// One possible strategy would be to do a timed retry to see if we can get
// this stored in the future.
if err := i.recordRelease(rel); err != nil {
slog.Error("failed to record the release", slog.Any("error", err))
i.cfg.Logger().Error("failed to record the release", slog.Any("error", err))
}
return rel, nil
@ -549,11 +549,12 @@ func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.Resource
func (i *Install) failRelease(rel *release.Release, err error) (*release.Release, error) {
rel.SetStatus(rcommon.StatusFailed, fmt.Sprintf("Release %q failed: %s", i.ReleaseName, err.Error()))
if i.RollbackOnFailure {
slog.Debug("install failed and rollback-on-failure is set, uninstalling release", "release", i.ReleaseName)
i.cfg.Logger().Debug("install failed and rollback-on-failure is set, uninstalling release", "release", i.ReleaseName)
uninstall := NewUninstall(i.cfg)
uninstall.DisableHooks = i.DisableHooks
uninstall.KeepHistory = false
uninstall.Timeout = i.Timeout
uninstall.WaitStrategy = i.WaitStrategy
if _, uninstallErr := uninstall.Run(i.ReleaseName); uninstallErr != nil {
return rel, fmt.Errorf("an error occurred while uninstalling the release. original install error: %w: %w", err, uninstallErr)
}
@ -886,7 +887,7 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) (
Options: []getter.Option{
getter.WithPassCredentialsAll(c.PassCredentialsAll),
getter.WithTLSClientConfig(c.CertFile, c.KeyFile, c.CaFile),
getter.WithInsecureSkipVerifyTLS(c.InsecureSkipTLSverify),
getter.WithInsecureSkipVerifyTLS(c.InsecureSkipTLSVerify),
getter.WithPlainHTTP(c.PlainHTTP),
getter.WithBasicAuth(c.Username, c.Password),
},
@ -911,7 +912,7 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) (
repo.WithChartVersion(version),
repo.WithClientTLS(c.CertFile, c.KeyFile, c.CaFile),
repo.WithUsernamePassword(c.Username, c.Password),
repo.WithInsecureSkipTLSverify(c.InsecureSkipTLSverify),
repo.WithInsecureSkipTLSVerify(c.InsecureSkipTLSVerify),
repo.WithPassCredentialsAll(c.PassCredentialsAll),
)
if err != nil {

@ -465,8 +465,9 @@ func TestInstallRelease_DryRun_Lookup(t *testing.T) {
mockChart := buildChart(withSampleTemplates())
mockChart.Templates = append(mockChart.Templates, &common.File{
Name: "templates/lookup",
Data: []byte(`goodbye: {{ lookup "v1" "Namespace" "" "___" }}`),
Name: "templates/lookup",
ModTime: time.Now(),
Data: []byte(`goodbye: {{ lookup "v1" "Namespace" "" "___" }}`),
})
resi, err := instAction.Run(mockChart, vals)

@ -58,7 +58,7 @@ type Package struct {
CertFile string
KeyFile string
CaFile string
InsecureSkipTLSverify bool
InsecureSkipTLSVerify bool
}
const (

@ -82,7 +82,7 @@ func (p *Pull) Run(chartRef string) (string, error) {
getter.WithBasicAuth(p.Username, p.Password),
getter.WithPassCredentialsAll(p.PassCredentialsAll),
getter.WithTLSClientConfig(p.CertFile, p.KeyFile, p.CaFile),
getter.WithInsecureSkipVerifyTLS(p.InsecureSkipTLSverify),
getter.WithInsecureSkipVerifyTLS(p.InsecureSkipTLSVerify),
getter.WithPlainHTTP(p.PlainHTTP),
},
RegistryClient: p.cfg.RegistryClient,
@ -124,7 +124,7 @@ func (p *Pull) Run(chartRef string) (string, error) {
repo.WithChartVersion(p.Version),
repo.WithClientTLS(p.CertFile, p.KeyFile, p.CaFile),
repo.WithUsernamePassword(p.Username, p.Password),
repo.WithInsecureSkipTLSverify(p.InsecureSkipTLSverify),
repo.WithInsecureSkipTLSVerify(p.InsecureSkipTLSVerify),
repo.WithPassCredentialsAll(p.PassCredentialsAll),
)
if err != nil {

@ -35,7 +35,7 @@ type Push struct {
certFile string
keyFile string
caFile string
insecureSkipTLSverify bool
insecureSkipTLSVerify bool
plainHTTP bool
out io.Writer
}
@ -62,7 +62,7 @@ func WithTLSClientConfig(certFile, keyFile, caFile string) PushOpt {
// WithInsecureSkipTLSVerify determines if a TLS Certificate will be checked
func WithInsecureSkipTLSVerify(insecureSkipTLSVerify bool) PushOpt {
return func(p *Push) {
p.insecureSkipTLSverify = insecureSkipTLSVerify
p.insecureSkipTLSVerify = insecureSkipTLSVerify
}
}
@ -98,7 +98,7 @@ func (p *Push) Run(chartRef string, remote string) (string, error) {
Pushers: pusher.All(p.Settings),
Options: []pusher.Option{
pusher.WithTLSClientConfig(p.certFile, p.keyFile, p.caFile),
pusher.WithInsecureSkipTLSVerify(p.insecureSkipTLSverify),
pusher.WithInsecureSkipTLSVerify(p.insecureSkipTLSVerify),
pusher.WithPlainHTTP(p.plainHTTP),
},
}

@ -19,7 +19,6 @@ package action
import (
"bytes"
"fmt"
"log/slog"
"strings"
"time"
@ -76,26 +75,26 @@ func (r *Rollback) Run(name string) error {
r.cfg.Releases.MaxHistory = r.MaxHistory
slog.Debug("preparing rollback", "name", name)
r.cfg.Logger().Debug("preparing rollback", "name", name)
currentRelease, targetRelease, serverSideApply, err := r.prepareRollback(name)
if err != nil {
return err
}
if !isDryRun(r.DryRunStrategy) {
slog.Debug("creating rolled back release", "name", name)
r.cfg.Logger().Debug("creating rolled back release", "name", name)
if err := r.cfg.Releases.Create(targetRelease); err != nil {
return err
}
}
slog.Debug("performing rollback", "name", name)
r.cfg.Logger().Debug("performing rollback", "name", name)
if _, err := r.performRollback(currentRelease, targetRelease, serverSideApply); err != nil {
return err
}
if !isDryRun(r.DryRunStrategy) {
slog.Debug("updating status for rolled back release", "name", name)
r.cfg.Logger().Debug("updating status for rolled back release", "name", name)
if err := r.cfg.Releases.Update(targetRelease); err != nil {
return err
}
@ -151,7 +150,7 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele
return nil, nil, false, fmt.Errorf("release has no %d version", previousVersion)
}
slog.Debug("rolling back", "name", name, "currentVersion", currentRelease.Version, "targetVersion", previousVersion)
r.cfg.Logger().Debug("rolling back", "name", name, "currentVersion", currentRelease.Version, "targetVersion", previousVersion)
previousReleasei, err := r.cfg.Releases.Get(name, previousVersion)
if err != nil {
@ -194,7 +193,7 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele
func (r *Rollback) performRollback(currentRelease, targetRelease *release.Release, serverSideApply bool) (*release.Release, error) {
if isDryRun(r.DryRunStrategy) {
slog.Debug("dry run", "name", targetRelease.Name)
r.cfg.Logger().Debug("dry run", "name", targetRelease.Name)
return targetRelease, nil
}
@ -214,7 +213,7 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas
return targetRelease, err
}
} else {
slog.Debug("rollback hooks disabled", "name", targetRelease.Name)
r.cfg.Logger().Debug("rollback hooks disabled", "name", targetRelease.Name)
}
// It is safe to use "forceOwnership" here because these are resources currently rendered by the chart.
@ -232,28 +231,28 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas
if err != nil {
msg := fmt.Sprintf("Rollback %q failed: %s", targetRelease.Name, err)
slog.Warn(msg)
r.cfg.Logger().Warn(msg)
currentRelease.Info.Status = common.StatusSuperseded
targetRelease.Info.Status = common.StatusFailed
targetRelease.Info.Description = msg
r.cfg.recordRelease(currentRelease)
r.cfg.recordRelease(targetRelease)
if r.CleanupOnFail {
slog.Debug("cleanup on fail set, cleaning up resources", "count", len(results.Created))
r.cfg.Logger().Debug("cleanup on fail set, cleaning up resources", "count", len(results.Created))
_, errs := r.cfg.KubeClient.Delete(results.Created, metav1.DeletePropagationBackground)
if errs != nil {
return targetRelease, fmt.Errorf(
"an error occurred while cleaning up resources. original rollback error: %w",
fmt.Errorf("unable to cleanup resources: %w", joinErrors(errs, ", ")))
}
slog.Debug("resource cleanup complete")
r.cfg.Logger().Debug("resource cleanup complete")
}
return targetRelease, err
}
waiter, err := r.cfg.KubeClient.GetWaiter(r.WaitStrategy)
if err != nil {
return nil, fmt.Errorf("unable to set metadata visitor from target release: %w", err)
return nil, fmt.Errorf("unable to get waiter: %w", err)
}
if r.WaitForJobs {
if err := waiter.WaitWithJobs(target, r.Timeout); err != nil {
@ -288,7 +287,7 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas
if err != nil {
return nil, err
}
slog.Debug("superseding previous deployment", "version", rel.Version)
r.cfg.Logger().Debug("superseding previous deployment", "version", rel.Version)
rel.Info.Status = common.StatusSuperseded
r.cfg.recordRelease(rel)
}

@ -18,6 +18,7 @@ package action
import (
"testing"
"time"
"helm.sh/helm/v4/pkg/chart/common"
chart "helm.sh/helm/v4/pkg/chart/v2"
@ -26,17 +27,18 @@ import (
func TestShow(t *testing.T) {
config := actionConfigFixture(t)
client := NewShow(ShowAll, config)
modTime := time.Now()
client.chart = &chart.Chart{
Metadata: &chart.Metadata{Name: "alpine"},
Files: []*common.File{
{Name: "README.md", Data: []byte("README\n")},
{Name: "crds/ignoreme.txt", Data: []byte("error")},
{Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")},
{Name: "crds/bar.json", Data: []byte("---\nbar\n")},
{Name: "crds/baz.yaml", Data: []byte("baz\n")},
{Name: "README.md", ModTime: modTime, Data: []byte("README\n")},
{Name: "crds/ignoreme.txt", ModTime: modTime, Data: []byte("error")},
{Name: "crds/foo.yaml", ModTime: modTime, Data: []byte("---\nfoo\n")},
{Name: "crds/bar.json", ModTime: modTime, Data: []byte("---\nbar\n")},
{Name: "crds/baz.yaml", ModTime: modTime, Data: []byte("baz\n")},
},
Raw: []*common.File{
{Name: "values.yaml", Data: []byte("VALUES\n")},
{Name: "values.yaml", ModTime: modTime, Data: []byte("VALUES\n")},
},
Values: map[string]interface{}{},
}
@ -104,13 +106,14 @@ func TestShowValuesByJsonPathFormat(t *testing.T) {
func TestShowCRDs(t *testing.T) {
config := actionConfigFixture(t)
client := NewShow(ShowCRDs, config)
modTime := time.Now()
client.chart = &chart.Chart{
Metadata: &chart.Metadata{Name: "alpine"},
Files: []*common.File{
{Name: "crds/ignoreme.txt", Data: []byte("error")},
{Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")},
{Name: "crds/bar.json", Data: []byte("---\nbar\n")},
{Name: "crds/baz.yaml", Data: []byte("baz\n")},
{Name: "crds/ignoreme.txt", ModTime: modTime, Data: []byte("error")},
{Name: "crds/foo.yaml", ModTime: modTime, Data: []byte("---\nfoo\n")},
{Name: "crds/bar.json", ModTime: modTime, Data: []byte("---\nbar\n")},
{Name: "crds/baz.yaml", ModTime: modTime, Data: []byte("baz\n")},
},
}
@ -137,12 +140,13 @@ baz
func TestShowNoReadme(t *testing.T) {
config := actionConfigFixture(t)
client := NewShow(ShowAll, config)
modTime := time.Now()
client.chart = &chart.Chart{
Metadata: &chart.Metadata{Name: "alpine"},
Files: []*common.File{
{Name: "crds/ignoreme.txt", Data: []byte("error")},
{Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")},
{Name: "crds/bar.json", Data: []byte("---\nbar\n")},
{Name: "crds/ignoreme.txt", ModTime: modTime, Data: []byte("error")},
{Name: "crds/foo.yaml", ModTime: modTime, Data: []byte("---\nfoo\n")},
{Name: "crds/bar.json", ModTime: modTime, Data: []byte("---\nbar\n")},
},
}

@ -119,7 +119,7 @@ func (u *Uninstall) Run(name string) (*releasei.UninstallReleaseResponse, error)
return nil, fmt.Errorf("the release named %q is already deleted", name)
}
slog.Debug("uninstall: deleting release", "name", name)
u.cfg.Logger().Debug("uninstall: deleting release", "name", name)
rel.Info.Status = common.StatusUninstalling
rel.Info.Deleted = time.Now()
rel.Info.Description = "Deletion in progress (or silently failed)"
@ -131,18 +131,18 @@ func (u *Uninstall) Run(name string) (*releasei.UninstallReleaseResponse, error)
return res, err
}
} else {
slog.Debug("delete hooks disabled", "release", name)
u.cfg.Logger().Debug("delete hooks disabled", "release", name)
}
// From here on out, the release is currently considered to be in StatusUninstalling
// state.
if err := u.cfg.Releases.Update(rel); err != nil {
slog.Debug("uninstall: Failed to store updated release", slog.Any("error", err))
u.cfg.Logger().Debug("uninstall: Failed to store updated release", slog.Any("error", err))
}
deletedResources, kept, errs := u.deleteRelease(rel)
if errs != nil {
slog.Debug("uninstall: Failed to delete release", slog.Any("error", errs))
u.cfg.Logger().Debug("uninstall: Failed to delete release", slog.Any("error", errs))
return nil, fmt.Errorf("failed to delete release: %s", name)
}
@ -170,7 +170,7 @@ func (u *Uninstall) Run(name string) (*releasei.UninstallReleaseResponse, error)
}
if !u.KeepHistory {
slog.Debug("purge requested", "release", name)
u.cfg.Logger().Debug("purge requested", "release", name)
err := u.purgeReleases(rels...)
if err != nil {
errs = append(errs, fmt.Errorf("uninstall: Failed to purge the release: %w", err))
@ -185,7 +185,7 @@ func (u *Uninstall) Run(name string) (*releasei.UninstallReleaseResponse, error)
}
if err := u.cfg.Releases.Update(rel); err != nil {
slog.Debug("uninstall: Failed to store updated release", slog.Any("error", err))
u.cfg.Logger().Debug("uninstall: Failed to store updated release", slog.Any("error", err))
}
if len(errs) > 0 {

@ -187,7 +187,7 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, ch chart.Char
return nil, fmt.Errorf("release name is invalid: %s", name)
}
slog.Debug("preparing upgrade", "name", name)
u.cfg.Logger().Debug("preparing upgrade", "name", name)
currentRelease, upgradedRelease, serverSideApply, err := u.prepareUpgrade(name, chrt, vals)
if err != nil {
return nil, err
@ -195,7 +195,7 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, ch chart.Char
u.cfg.Releases.MaxHistory = u.MaxHistory
slog.Debug("performing update", "name", name)
u.cfg.Logger().Debug("performing update", "name", name)
res, err := u.performUpgrade(ctx, currentRelease, upgradedRelease, serverSideApply)
if err != nil {
return res, err
@ -203,7 +203,7 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, ch chart.Char
// Do not update for dry runs
if !isDryRun(u.DryRunStrategy) {
slog.Debug("updating status for upgraded release", "name", name)
u.cfg.Logger().Debug("updating status for upgraded release", "name", name)
if err := u.cfg.Releases.Update(upgradedRelease); err != nil {
return res, err
}
@ -310,7 +310,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[str
return nil, nil, false, err
}
slog.Debug("determined release apply method", slog.Bool("server_side_apply", serverSideApply), slog.String("previous_release_apply_method", lastRelease.ApplyMethod))
u.cfg.Logger().Debug("determined release apply method", slog.Bool("server_side_apply", serverSideApply), slog.String("previous_release_apply_method", lastRelease.ApplyMethod))
// Store an upgraded release.
upgradedRelease := &release.Release{
@ -393,7 +393,7 @@ func (u *Upgrade) performUpgrade(ctx context.Context, originalRelease, upgradedR
})
if isDryRun(u.DryRunStrategy) {
slog.Debug("dry run for release", "name", upgradedRelease.Name)
u.cfg.Logger().Debug("dry run for release", "name", upgradedRelease.Name)
if len(u.Description) > 0 {
upgradedRelease.Info.Description = u.Description
} else {
@ -402,7 +402,7 @@ func (u *Upgrade) performUpgrade(ctx context.Context, originalRelease, upgradedR
return upgradedRelease, nil
}
slog.Debug("creating upgraded release", "name", upgradedRelease.Name)
u.cfg.Logger().Debug("creating upgraded release", "name", upgradedRelease.Name)
if err := u.cfg.Releases.Create(upgradedRelease); err != nil {
return nil, err
}
@ -459,7 +459,7 @@ func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *rele
return
}
} else {
slog.Debug("upgrade hooks disabled", "name", upgradedRelease.Name)
u.cfg.Logger().Debug("upgrade hooks disabled", "name", upgradedRelease.Name)
}
upgradeClientSideFieldManager := isReleaseApplyMethodClientSideApply(originalRelease.ApplyMethod) && serverSideApply // Update client-side field manager if transitioning from client-side to server-side apply
@ -517,13 +517,13 @@ func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *rele
func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, err error) (*release.Release, error) {
msg := fmt.Sprintf("Upgrade %q failed: %s", rel.Name, err)
slog.Warn("upgrade failed", "name", rel.Name, slog.Any("error", err))
u.cfg.Logger().Warn("upgrade failed", "name", rel.Name, slog.Any("error", err))
rel.Info.Status = rcommon.StatusFailed
rel.Info.Description = msg
u.cfg.recordRelease(rel)
if u.CleanupOnFail && len(created) > 0 {
slog.Debug("cleanup on fail set", "cleaning_resources", len(created))
u.cfg.Logger().Debug("cleanup on fail set", "cleaning_resources", len(created))
_, errs := u.cfg.KubeClient.Delete(created, metav1.DeletePropagationBackground)
if errs != nil {
return rel, fmt.Errorf(
@ -535,11 +535,11 @@ func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, e
),
)
}
slog.Debug("resource cleanup complete")
u.cfg.Logger().Debug("resource cleanup complete")
}
if u.RollbackOnFailure {
slog.Debug("Upgrade failed and rollback-on-failure is set, rolling back to previous successful release")
u.cfg.Logger().Debug("Upgrade failed and rollback-on-failure is set, rolling back to previous successful release")
// As a protection, get the last successful release before rollback.
// If there are no successful releases, bail out
@ -567,9 +567,7 @@ func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, e
rollin := NewRollback(u.cfg)
rollin.Version = filteredHistory[0].Version
if u.WaitStrategy == kube.HookOnlyStrategy {
rollin.WaitStrategy = kube.StatusWatcherStrategy
}
rollin.WaitStrategy = u.WaitStrategy
rollin.WaitForJobs = u.WaitForJobs
rollin.DisableHooks = u.DisableHooks
rollin.ForceReplace = u.ForceReplace
@ -596,13 +594,13 @@ func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, e
func (u *Upgrade) reuseValues(chart *chartv2.Chart, current *release.Release, newVals map[string]interface{}) (map[string]interface{}, error) {
if u.ResetValues {
// If ResetValues is set, we completely ignore current.Config.
slog.Debug("resetting values to the chart's original version")
u.cfg.Logger().Debug("resetting values to the chart's original version")
return newVals, nil
}
// If the ReuseValues flag is set, we always copy the old values over the new config's values.
if u.ReuseValues {
slog.Debug("reusing the old release's values")
u.cfg.Logger().Debug("reusing the old release's values")
// We have to regenerate the old coalesced values:
oldVals, err := util.CoalesceValues(current.Chart, current.Config)
@ -619,7 +617,7 @@ func (u *Upgrade) reuseValues(chart *chartv2.Chart, current *release.Release, ne
// If the ResetThenReuseValues flag is set, we use the new chart's values, but we copy the old config's values over the new config's values.
if u.ResetThenReuseValues {
slog.Debug("merging values from old release to new values")
u.cfg.Logger().Debug("merging values from old release to new values")
newVals = util.CoalesceTables(newVals, current.Config)
@ -627,7 +625,7 @@ func (u *Upgrade) reuseValues(chart *chartv2.Chart, current *release.Release, ne
}
if len(newVals) == 0 && len(current.Config) > 0 {
slog.Debug("copying values from old release", "name", current.Name, "version", current.Version)
u.cfg.Logger().Debug("copying values from old release", "name", current.Name, "version", current.Version)
newVals = current.Config
}
return newVals, nil

@ -15,6 +15,8 @@ limitations under the License.
package common
import "time"
// File represents a file as a name/value pair.
//
// By convention, name is a relative path within the scope of the chart's
@ -24,4 +26,6 @@ type File struct {
Name string `json:"name"`
// Data is the template as byte data.
Data []byte `json:"data"`
// ModTime is the file's mod-time
ModTime time.Time `json:"modtime,omitzero"`
}

@ -24,6 +24,7 @@ import (
"log/slog"
"net/http"
"strings"
"sync"
"time"
"github.com/santhosh-tekuri/jsonschema/v6"
@ -93,7 +94,22 @@ func ValidateAgainstSchema(ch chart.Charter, values map[string]interface{}) erro
if err != nil {
return err
}
subchartValues := values[sub.Name()].(map[string]interface{})
raw, exists := values[sub.Name()]
if !exists || raw == nil {
// No values provided for this subchart; nothing to validate
continue
}
subchartValues, ok := raw.(map[string]any)
if !ok {
sb.WriteString(fmt.Sprintf(
"%s:\ninvalid type for values: expected object (map), got %T\n",
sub.Name(), raw,
))
continue
}
if err := ValidateAgainstSchema(subchart, subchartValues); err != nil {
sb.WriteString(err.Error())
}
@ -127,6 +143,7 @@ func ValidateAgainstSingleSchema(values common.Values, schemaJSON []byte) (reter
"file": jsonschema.FileLoader{},
"http": newHTTPURLLoader(),
"https": newHTTPURLLoader(),
"urn": urnLoader{},
}
compiler := jsonschema.NewCompiler()
@ -149,6 +166,35 @@ func ValidateAgainstSingleSchema(values common.Values, schemaJSON []byte) (reter
return nil
}
// URNResolverFunc allows SDK to plug a URN resolver. It must return a
// schema document compatible with the validator (e.g., result of
// jsonschema.UnmarshalJSON).
type URNResolverFunc func(urn string) (any, error)
// URNResolver is the default resolver used by the URN loader. By default it
// returns a clear error.
var URNResolver URNResolverFunc = func(urn string) (any, error) {
return nil, fmt.Errorf("URN not resolved: %s", urn)
}
// urnLoader implements resolution for the urn: scheme by delegating to
// URNResolver. If unresolved, it logs a warning and returns a permissive
// boolean-true schema to avoid hard failures (back-compat behavior).
type urnLoader struct{}
// warnedURNs ensures we log the unresolved-URN warning only once per URN.
var warnedURNs sync.Map
func (l urnLoader) Load(urlStr string) (any, error) {
if doc, err := URNResolver(urlStr); err == nil && doc != nil {
return doc, nil
}
if _, loaded := warnedURNs.LoadOrStore(urlStr, struct{}{}); !loaded {
slog.Warn("unresolved URN reference ignored; using permissive schema", "urn", urlStr)
}
return jsonschema.UnmarshalJSON(strings.NewReader("true"))
}
// Note, JSONSchemaValidationError is used to wrap the error from the underlying
// validation package so that Helm has a clean interface and the validation package
// could be replaced without changing the Helm SDK API.

@ -286,3 +286,106 @@ func TestHTTPURLLoader_Load(t *testing.T) {
}
})
}
// Test that an unresolved URN $ref is soft-ignored and validation succeeds.
// it mimics the behavior of Helm 3.18.4
func TestValidateAgainstSingleSchema_UnresolvedURN_Ignored(t *testing.T) {
schema := []byte(`{
"$schema": "https://json-schema.org/draft-07/schema#",
"$ref": "urn:example:helm:schemas:v1:helm-schema-validation-conditions:v1/helmSchemaValidation-true"
}`)
vals := map[string]interface{}{"any": "value"}
if err := ValidateAgainstSingleSchema(vals, schema); err != nil {
t.Fatalf("expected no error when URN unresolved is ignored, got: %v", err)
}
}
// Non-regression tests for https://github.com/helm/helm/issues/31202
// Ensure ValidateAgainstSchema does not panic when:
// - subchart key is missing
// - subchart value is nil
// - subchart value has an invalid type
func TestValidateAgainstSchema_MissingSubchartValues_NoPanic(t *testing.T) {
subchartJSON := []byte(subchartSchema)
subchart := &chart.Chart{
Metadata: &chart.Metadata{Name: "subchart"},
Schema: subchartJSON,
}
chrt := &chart.Chart{
Metadata: &chart.Metadata{Name: "chrt"},
}
chrt.AddDependency(subchart)
// No "subchart" key present in values
vals := map[string]any{
"name": "John",
}
defer func() {
if r := recover(); r != nil {
t.Fatalf("ValidateAgainstSchema panicked (missing subchart values): %v", r)
}
}()
if err := ValidateAgainstSchema(chrt, vals); err != nil {
t.Fatalf("expected no error when subchart values are missing, got: %v", err)
}
}
func TestValidateAgainstSchema_SubchartNil_NoPanic(t *testing.T) {
subchartJSON := []byte(subchartSchema)
subchart := &chart.Chart{
Metadata: &chart.Metadata{Name: "subchart"},
Schema: subchartJSON,
}
chrt := &chart.Chart{
Metadata: &chart.Metadata{Name: "chrt"},
}
chrt.AddDependency(subchart)
// "subchart" key present but nil
vals := map[string]any{
"name": "John",
"subchart": nil,
}
defer func() {
if r := recover(); r != nil {
t.Fatalf("ValidateAgainstSchema panicked (nil subchart values): %v", r)
}
}()
if err := ValidateAgainstSchema(chrt, vals); err != nil {
t.Fatalf("expected no error when subchart values are nil, got: %v", err)
}
}
func TestValidateAgainstSchema_InvalidSubchartValuesType_NoPanic(t *testing.T) {
subchartJSON := []byte(subchartSchema)
subchart := &chart.Chart{
Metadata: &chart.Metadata{Name: "subchart"},
Schema: subchartJSON,
}
chrt := &chart.Chart{
Metadata: &chart.Metadata{Name: "chrt"},
}
chrt.AddDependency(subchart)
// "subchart" is the wrong type (string instead of map)
vals := map[string]any{
"name": "John",
"subchart": "oops",
}
defer func() {
if r := recover(); r != nil {
t.Fatalf("ValidateAgainstSchema panicked (invalid subchart values type): %v", r)
}
}()
// We expect a non-nil error (invalid type), but crucially no panic.
if err := ValidateAgainstSchema(chrt, vals); err == nil {
t.Fatalf("expected an error when subchart values have invalid type, got nil")
}
}

@ -18,6 +18,7 @@ package util
import (
"testing"
"time"
"helm.sh/helm/v4/pkg/chart/common"
chart "helm.sh/helm/v4/pkg/chart/v2"
@ -46,7 +47,7 @@ func TestToRenderValues(t *testing.T) {
Templates: []*common.File{},
Values: chartValues,
Files: []*common.File{
{Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")},
{Name: "scheherazade/shahryar.txt", ModTime: time.Now(), Data: []byte("1,001 Nights")},
},
}
c.AddDependency(&chart.Chart{

@ -29,6 +29,7 @@ import (
"path"
"regexp"
"strings"
"time"
)
// MaxDecompressedChartSize is the maximum size of a chart archive that will be
@ -46,8 +47,9 @@ var utf8bom = []byte{0xEF, 0xBB, 0xBF}
// BufferedFile represents an archive file buffered for later processing.
type BufferedFile struct {
Name string
Data []byte
Name string
ModTime time.Time
Data []byte
}
// LoadArchiveFiles reads in files out of an archive into memory. This function
@ -148,7 +150,7 @@ func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) {
data := bytes.TrimPrefix(b.Bytes(), utf8bom)
files = append(files, &BufferedFile{Name: n, Data: data})
files = append(files, &BufferedFile{Name: n, ModTime: hd.ModTime, Data: data})
b.Reset()
}

@ -19,6 +19,7 @@ import (
"path/filepath"
"regexp"
"strings"
"time"
"helm.sh/helm/v4/pkg/chart/common"
)
@ -50,9 +51,13 @@ type Chart struct {
Values map[string]interface{} `json:"values"`
// Schema is an optional JSON schema for imposing structure on Values
Schema []byte `json:"schema"`
// SchemaModTime the schema was last modified
SchemaModTime time.Time `json:"schemamodtime,omitempty"`
// Files are miscellaneous files in a chart archive,
// e.g. README, LICENSE, etc.
Files []*common.File `json:"files"`
// ModTime the chart metadata was last modified
ModTime time.Time `json:"modtime,omitzero"`
parent *Chart
dependencies []*Chart

@ -18,6 +18,7 @@ package v2
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/assert"
@ -25,27 +26,33 @@ import (
)
func TestCRDs(t *testing.T) {
modTime := time.Now()
chrt := Chart{
Files: []*common.File{
{
Name: "crds/foo.yaml",
Data: []byte("hello"),
Name: "crds/foo.yaml",
ModTime: modTime,
Data: []byte("hello"),
},
{
Name: "bar.yaml",
Data: []byte("hello"),
Name: "bar.yaml",
ModTime: modTime,
Data: []byte("hello"),
},
{
Name: "crds/foo/bar/baz.yaml",
Data: []byte("hello"),
Name: "crds/foo/bar/baz.yaml",
ModTime: modTime,
Data: []byte("hello"),
},
{
Name: "crdsfoo/bar/baz.yaml",
Data: []byte("hello"),
Name: "crdsfoo/bar/baz.yaml",
ModTime: modTime,
Data: []byte("hello"),
},
{
Name: "crds/README.md",
Data: []byte("# hello"),
Name: "crds/README.md",
ModTime: modTime,
Data: []byte("# hello"),
},
},
}
@ -61,8 +68,9 @@ func TestSaveChartNoRawData(t *testing.T) {
chrt := Chart{
Raw: []*common.File{
{
Name: "fhqwhgads.yaml",
Data: []byte("Everybody to the Limit"),
Name: "fhqwhgads.yaml",
ModTime: time.Now(),
Data: []byte("Everybody to the Limit"),
},
},
}
@ -163,27 +171,33 @@ func TestChartFullPath(t *testing.T) {
}
func TestCRDObjects(t *testing.T) {
modTime := time.Now()
chrt := Chart{
Files: []*common.File{
{
Name: "crds/foo.yaml",
Data: []byte("hello"),
Name: "crds/foo.yaml",
ModTime: modTime,
Data: []byte("hello"),
},
{
Name: "bar.yaml",
Data: []byte("hello"),
Name: "bar.yaml",
ModTime: modTime,
Data: []byte("hello"),
},
{
Name: "crds/foo/bar/baz.yaml",
Data: []byte("hello"),
Name: "crds/foo/bar/baz.yaml",
ModTime: modTime,
Data: []byte("hello"),
},
{
Name: "crdsfoo/bar/baz.yaml",
Data: []byte("hello"),
Name: "crdsfoo/bar/baz.yaml",
ModTime: modTime,
Data: []byte("hello"),
},
{
Name: "crds/README.md",
Data: []byte("# hello"),
Name: "crds/README.md",
ModTime: modTime,
Data: []byte("# hello"),
},
},
}
@ -193,16 +207,18 @@ func TestCRDObjects(t *testing.T) {
Name: "crds/foo.yaml",
Filename: "crds/foo.yaml",
File: &common.File{
Name: "crds/foo.yaml",
Data: []byte("hello"),
Name: "crds/foo.yaml",
ModTime: modTime,
Data: []byte("hello"),
},
},
{
Name: "crds/foo/bar/baz.yaml",
Filename: "crds/foo/bar/baz.yaml",
File: &common.File{
Name: "crds/foo/bar/baz.yaml",
Data: []byte("hello"),
Name: "crds/foo/bar/baz.yaml",
ModTime: modTime,
Data: []byte("hello"),
},
},
}

@ -156,7 +156,7 @@ func (t *templateLinter) Lint() {
t.linter.RunLinterRule(support.ErrorSev, fileName, validateAllowedExtension(fileName))
// We only apply the following lint rules to yaml files
if filepath.Ext(fileName) != ".yaml" || filepath.Ext(fileName) == ".yml" {
if !isYamlFileExtension(fileName) {
continue
}
@ -366,6 +366,11 @@ func validateListAnnotations(yamlStruct *k8sYamlStruct, manifest string) error {
return nil
}
func isYamlFileExtension(fileName string) bool {
ext := strings.ToLower(filepath.Ext(fileName))
return ext == ".yaml" || ext == ".yml"
}
// k8sYamlStruct stubs a Kubernetes YAML file.
type k8sYamlStruct struct {
APIVersion string `json:"apiVersion"`

@ -22,6 +22,7 @@ import (
"path/filepath"
"strings"
"testing"
"time"
"helm.sh/helm/v4/pkg/chart/common"
chart "helm.sh/helm/v4/pkg/chart/v2"
@ -194,6 +195,7 @@ func TestValidateMetadataName(t *testing.T) {
}
func TestDeprecatedAPIFails(t *testing.T) {
modTime := time.Now()
mychart := chart.Chart{
Metadata: &chart.Metadata{
APIVersion: "v2",
@ -203,12 +205,14 @@ func TestDeprecatedAPIFails(t *testing.T) {
},
Templates: []*common.File{
{
Name: "templates/baddeployment.yaml",
Data: []byte("apiVersion: apps/v1beta1\nkind: Deployment\nmetadata:\n name: baddep\nspec: {selector: {matchLabels: {foo: bar}}}"),
Name: "templates/baddeployment.yaml",
ModTime: modTime,
Data: []byte("apiVersion: apps/v1beta1\nkind: Deployment\nmetadata:\n name: baddep\nspec: {selector: {matchLabels: {foo: bar}}}"),
},
{
Name: "templates/goodsecret.yaml",
Data: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: goodsecret"),
Name: "templates/goodsecret.yaml",
ModTime: modTime,
Data: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: goodsecret"),
},
},
}
@ -267,8 +271,9 @@ func TestStrictTemplateParsingMapError(t *testing.T) {
},
Templates: []*common.File{
{
Name: "templates/configmap.yaml",
Data: []byte(manifest),
Name: "templates/configmap.yaml",
ModTime: time.Now(),
Data: []byte(manifest),
},
},
}
@ -400,8 +405,9 @@ func TestEmptyWithCommentsManifests(t *testing.T) {
},
Templates: []*common.File{
{
Name: "templates/empty-with-comments.yaml",
Data: []byte("#@formatter:off\n"),
Name: "templates/empty-with-comments.yaml",
ModTime: time.Now(),
Data: []byte("#@formatter:off\n"),
},
},
}
@ -462,3 +468,23 @@ items:
t.Fatalf("List objects keep annotations should pass. got: %s", err)
}
}
func TestIsYamlFileExtension(t *testing.T) {
tests := []struct {
filename string
expected bool
}{
{"test.yaml", true},
{"test.yml", true},
{"test.txt", false},
{"test", false},
}
for _, test := range tests {
result := isYamlFileExtension(test.filename)
if result != test.expected {
t.Errorf("isYamlFileExtension(%s) = %v; want %v", test.filename, result, test.expected)
}
}
}

@ -111,7 +111,7 @@ func LoadDir(dir string) (*chart.Chart, error) {
data = bytes.TrimPrefix(data, utf8bom)
files = append(files, &archive.BufferedFile{Name: n, Data: data})
files = append(files, &archive.BufferedFile{Name: n, ModTime: fi.ModTime(), Data: data})
return nil
}
if err = sympath.Walk(topdir, walk); err != nil {

@ -76,7 +76,7 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
// do not rely on assumed ordering of files in the chart and crash
// if Chart.yaml was not coming early enough to initialize metadata
for _, f := range files {
c.Raw = append(c.Raw, &common.File{Name: f.Name, Data: f.Data})
c.Raw = append(c.Raw, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data})
if f.Name == "Chart.yaml" {
if c.Metadata == nil {
c.Metadata = new(chart.Metadata)
@ -90,6 +90,7 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
if c.Metadata.APIVersion == "" {
c.Metadata.APIVersion = chart.APIVersionV1
}
c.ModTime = f.ModTime
}
}
for _, f := range files {
@ -110,6 +111,7 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
c.Values = values
case f.Name == "values.schema.json":
c.Schema = f.Data
c.SchemaModTime = f.ModTime
// Deprecated: requirements.yaml is deprecated use Chart.yaml.
// We will handle it for you because we are nice people
@ -124,7 +126,7 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
return c, fmt.Errorf("cannot load requirements.yaml: %w", err)
}
if c.Metadata.APIVersion == chart.APIVersionV1 {
c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data})
c.Files = append(c.Files, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data})
}
// Deprecated: requirements.lock is deprecated use Chart.lock.
case f.Name == "requirements.lock":
@ -139,22 +141,22 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
log.Printf("Warning: Dependency locking is handled in Chart.lock since apiVersion \"v2\". We recommend migrating to Chart.lock.")
}
if c.Metadata.APIVersion == chart.APIVersionV1 {
c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data})
c.Files = append(c.Files, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data})
}
case strings.HasPrefix(f.Name, "templates/"):
c.Templates = append(c.Templates, &common.File{Name: f.Name, Data: f.Data})
c.Templates = append(c.Templates, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data})
case strings.HasPrefix(f.Name, "charts/"):
if filepath.Ext(f.Name) == ".prov" {
c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data})
c.Files = append(c.Files, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data})
continue
}
fname := strings.TrimPrefix(f.Name, "charts/")
cname := strings.SplitN(fname, "/", 2)[0]
subcharts[cname] = append(subcharts[cname], &archive.BufferedFile{Name: fname, Data: f.Data})
subcharts[cname] = append(subcharts[cname], &archive.BufferedFile{Name: fname, ModTime: f.ModTime, Data: f.Data})
default:
c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data})
c.Files = append(c.Files, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data})
}
}

@ -219,8 +219,9 @@ func TestLoadFiles_BadCases(t *testing.T) {
name: "These files contain only requirements.lock",
bufferedFiles: []*archive.BufferedFile{
{
Name: "requirements.lock",
Data: []byte(""),
Name: "requirements.lock",
ModTime: time.Now(),
Data: []byte(""),
},
},
expectError: "validation: chart.metadata.apiVersion is required"},
@ -236,9 +237,11 @@ func TestLoadFiles_BadCases(t *testing.T) {
}
func TestLoadFiles(t *testing.T) {
modTime := time.Now()
goodFiles := []*archive.BufferedFile{
{
Name: "Chart.yaml",
Name: "Chart.yaml",
ModTime: modTime,
Data: []byte(`apiVersion: v1
name: frobnitz
description: This is a frobnitz.
@ -259,20 +262,24 @@ icon: https://example.com/64x64.png
`),
},
{
Name: "values.yaml",
Data: []byte("var: some values"),
Name: "values.yaml",
ModTime: modTime,
Data: []byte("var: some values"),
},
{
Name: "values.schema.json",
Data: []byte("type: Values"),
Name: "values.schema.json",
ModTime: modTime,
Data: []byte("type: Values"),
},
{
Name: "templates/deployment.yaml",
Data: []byte("some deployment"),
Name: "templates/deployment.yaml",
ModTime: modTime,
Data: []byte("some deployment"),
},
{
Name: "templates/service.yaml",
Data: []byte("some service"),
Name: "templates/service.yaml",
ModTime: modTime,
Data: []byte("some service"),
},
}
@ -312,26 +319,32 @@ icon: https://example.com/64x64.png
// Test the order of file loading. The Chart.yaml file needs to come first for
// later comparison checks. See https://github.com/helm/helm/pull/8948
func TestLoadFilesOrder(t *testing.T) {
modTime := time.Now()
goodFiles := []*archive.BufferedFile{
{
Name: "requirements.yaml",
Data: []byte("dependencies:"),
Name: "requirements.yaml",
ModTime: modTime,
Data: []byte("dependencies:"),
},
{
Name: "values.yaml",
Data: []byte("var: some values"),
Name: "values.yaml",
ModTime: modTime,
Data: []byte("var: some values"),
},
{
Name: "templates/deployment.yaml",
Data: []byte("some deployment"),
Name: "templates/deployment.yaml",
ModTime: modTime,
Data: []byte("some deployment"),
},
{
Name: "templates/service.yaml",
Data: []byte("some service"),
Name: "templates/service.yaml",
ModTime: modTime,
Data: []byte("some service"),
},
{
Name: "Chart.yaml",
Name: "Chart.yaml",
ModTime: modTime,
Data: []byte(`apiVersion: v1
name: frobnitz
description: This is a frobnitz.

@ -660,7 +660,7 @@ func CreateFrom(chartfile *chart.Metadata, dest, src string) error {
for _, template := range schart.Templates {
newData := transform(string(template.Data), schart.Name())
updatedTemplates = append(updatedTemplates, &common.File{Name: template.Name, Data: newData})
updatedTemplates = append(updatedTemplates, &common.File{Name: template.Name, ModTime: template.ModTime, Data: newData})
}
schart.Templates = updatedTemplates

@ -175,7 +175,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
if err != nil {
return err
}
if err := writeToTar(out, filepath.Join(base, ChartfileName), cdata); err != nil {
if err := writeToTar(out, filepath.Join(base, ChartfileName), cdata, c.ModTime); err != nil {
return err
}
@ -187,7 +187,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
if err != nil {
return err
}
if err := writeToTar(out, filepath.Join(base, "Chart.lock"), ldata); err != nil {
if err := writeToTar(out, filepath.Join(base, "Chart.lock"), ldata, c.Lock.Generated); err != nil {
return err
}
}
@ -196,7 +196,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
// Save values.yaml
for _, f := range c.Raw {
if f.Name == ValuesfileName {
if err := writeToTar(out, filepath.Join(base, ValuesfileName), f.Data); err != nil {
if err := writeToTar(out, filepath.Join(base, ValuesfileName), f.Data, f.ModTime); err != nil {
return err
}
}
@ -207,7 +207,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
if !json.Valid(c.Schema) {
return errors.New("invalid JSON in " + SchemafileName)
}
if err := writeToTar(out, filepath.Join(base, SchemafileName), c.Schema); err != nil {
if err := writeToTar(out, filepath.Join(base, SchemafileName), c.Schema, c.SchemaModTime); err != nil {
return err
}
}
@ -215,7 +215,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
// Save templates
for _, f := range c.Templates {
n := filepath.Join(base, f.Name)
if err := writeToTar(out, n, f.Data); err != nil {
if err := writeToTar(out, n, f.Data, f.ModTime); err != nil {
return err
}
}
@ -223,7 +223,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
// Save files
for _, f := range c.Files {
n := filepath.Join(base, f.Name)
if err := writeToTar(out, n, f.Data); err != nil {
if err := writeToTar(out, n, f.Data, f.ModTime); err != nil {
return err
}
}
@ -238,13 +238,16 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
}
// writeToTar writes a single file to a tar archive.
func writeToTar(out *tar.Writer, name string, body []byte) error {
func writeToTar(out *tar.Writer, name string, body []byte, modTime time.Time) error {
// TODO: Do we need to create dummy parent directory names if none exist?
h := &tar.Header{
Name: filepath.ToSlash(name),
Mode: 0644,
Size: int64(len(body)),
ModTime: time.Now(),
ModTime: modTime,
}
if h.ModTime.IsZero() {
h.ModTime = time.Now()
}
if err := out.WriteHeader(h); err != nil {
return err

@ -20,6 +20,8 @@ import (
"archive/tar"
"bytes"
"compress/gzip"
"crypto/sha256"
"fmt"
"io"
"os"
"path"
@ -49,7 +51,7 @@ func TestSave(t *testing.T) {
Digest: "testdigest",
},
Files: []*common.File{
{Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")},
{Name: "scheherazade/shahryar.txt", ModTime: time.Now(), Data: []byte("1,001 Nights")},
},
Schema: []byte("{\n \"title\": \"Values\"\n}"),
}
@ -118,7 +120,7 @@ func TestSave(t *testing.T) {
Digest: "testdigest",
},
Files: []*common.File{
{Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")},
{Name: "scheherazade/shahryar.txt", ModTime: time.Now(), Data: []byte("1,001 Nights")},
},
}
_, err := Save(c, tmp)
@ -153,14 +155,16 @@ func TestSavePreservesTimestamps(t *testing.T) {
Name: "ahab",
Version: "1.2.3",
},
ModTime: initialCreateTime,
Values: map[string]interface{}{
"imageName": "testimage",
"imageId": 42,
},
Files: []*common.File{
{Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")},
{Name: "scheherazade/shahryar.txt", ModTime: initialCreateTime, Data: []byte("1,001 Nights")},
},
Schema: []byte("{\n \"title\": \"Values\"\n}"),
Schema: []byte("{\n \"title\": \"Values\"\n}"),
SchemaModTime: initialCreateTime,
}
where, err := Save(c, tmp)
@ -173,8 +177,9 @@ func TestSavePreservesTimestamps(t *testing.T) {
t.Fatalf("Failed to parse tar: %v", err)
}
roundedTime := initialCreateTime.Round(time.Second)
for _, header := range allHeaders {
if header.ModTime.Before(initialCreateTime) {
if !header.ModTime.Equal(roundedTime) {
t.Fatalf("File timestamp not preserved: %v", header.ModTime)
}
}
@ -217,6 +222,7 @@ func retrieveAllHeadersFromTar(path string) ([]*tar.Header, error) {
func TestSaveDir(t *testing.T) {
tmp := t.TempDir()
modTime := time.Now()
c := &chart.Chart{
Metadata: &chart.Metadata{
APIVersion: chart.APIVersionV1,
@ -224,10 +230,10 @@ func TestSaveDir(t *testing.T) {
Version: "1.2.3",
},
Files: []*common.File{
{Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")},
{Name: "scheherazade/shahryar.txt", ModTime: modTime, Data: []byte("1,001 Nights")},
},
Templates: []*common.File{
{Name: path.Join(TemplatesDir, "nested", "dir", "thing.yaml"), Data: []byte("abc: {{ .Values.abc }}")},
{Name: path.Join(TemplatesDir, "nested", "dir", "thing.yaml"), ModTime: modTime, Data: []byte("abc: {{ .Values.abc }}")},
},
}
@ -263,3 +269,92 @@ func TestSaveDir(t *testing.T) {
t.Fatalf("Did not get expected error for chart named %q", c.Name())
}
}
func TestRepeatableSave(t *testing.T) {
tmp := t.TempDir()
defer os.RemoveAll(tmp)
modTime := time.Date(2021, 9, 1, 20, 34, 58, 651387237, time.UTC)
tests := []struct {
name string
chart *chart.Chart
want string
}{
{
name: "Package 1 file",
chart: &chart.Chart{
Metadata: &chart.Metadata{
APIVersion: chart.APIVersionV2,
Name: "ahab",
Version: "1.2.3",
},
ModTime: modTime,
Lock: &chart.Lock{
Digest: "testdigest",
Generated: modTime,
},
Files: []*common.File{
{Name: "scheherazade/shahryar.txt", ModTime: modTime, Data: []byte("1,001 Nights")},
},
Schema: []byte("{\n \"title\": \"Values\"\n}"),
SchemaModTime: modTime,
},
want: "fea2662522317b65c2788ff9e5fc446a9264830038dac618d4449493d99b3257",
},
{
name: "Package 2 files",
chart: &chart.Chart{
Metadata: &chart.Metadata{
APIVersion: chart.APIVersionV2,
Name: "ahab",
Version: "1.2.3",
},
ModTime: modTime,
Lock: &chart.Lock{
Digest: "testdigest",
Generated: modTime,
},
Files: []*common.File{
{Name: "scheherazade/shahryar.txt", ModTime: modTime, Data: []byte("1,001 Nights")},
{Name: "scheherazade/dunyazad.txt", ModTime: modTime, Data: []byte("1,001 Nights again")},
},
Schema: []byte("{\n \"title\": \"Values\"\n}"),
SchemaModTime: modTime,
},
want: "7ae92b2f274bb51ea3f1969e4187d78cc52b5f6f663b44b8fb3b40bcb8ee46f3",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// create package
dest := path.Join(tmp, "newdir")
where, err := Save(test.chart, dest)
if err != nil {
t.Fatalf("Failed to save: %s", err)
}
// get shasum for package
result, err := sha256Sum(where)
if err != nil {
t.Fatalf("Failed to check shasum: %s", err)
}
// assert that the package SHA is what we wanted.
if result != test.want {
t.Errorf("FormatName() result = %v, want %v", result, test.want)
}
})
}
}
func sha256Sum(filePath string) (string, error) {
f, err := os.Open(filePath)
if err != nil {
return "", err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
}

@ -130,7 +130,7 @@ func addDependencySubcommandFlags(f *pflag.FlagSet, client *action.Dependency) {
f.StringVar(&client.Password, "password", "", "chart repository password where to locate the requested chart")
f.StringVar(&client.CertFile, "cert-file", "", "identify HTTPS client using this SSL certificate file")
f.StringVar(&client.KeyFile, "key-file", "", "identify HTTPS client using this SSL key file")
f.BoolVar(&client.InsecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart download")
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")
f.BoolVar(&client.BuildOrUpdateRecursive, "recursive", false, "build or update dependencies recursively")

@ -55,7 +55,7 @@ func newDependencyBuildCmd(out io.Writer) *cobra.Command {
chartpath = filepath.Clean(args[0])
}
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password)
client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password)
if err != nil {
return fmt.Errorf("missing registry client: %w", err)
}

@ -59,7 +59,7 @@ func newDependencyUpdateCmd(_ *action.Configuration, out io.Writer) *cobra.Comma
chartpath = filepath.Clean(args[0])
}
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password)
client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password)
if err != nil {
return fmt.Errorf("missing registry client: %w", err)
}

@ -110,7 +110,7 @@ func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) {
f.StringVar(&c.Password, "password", "", "chart repository password where to locate the requested chart")
f.StringVar(&c.CertFile, "cert-file", "", "identify HTTPS client using this SSL certificate file")
f.StringVar(&c.KeyFile, "key-file", "", "identify HTTPS client using this SSL key file")
f.BoolVar(&c.InsecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart download")
f.BoolVar(&c.InsecureSkipTLSVerify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart download")
f.BoolVar(&c.PlainHTTP, "plain-http", false, "use insecure HTTP connections for the chart download")
f.StringVar(&c.CaFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle")
f.BoolVar(&c.PassCredentialsAll, "pass-credentials", false, "pass credentials to all domains")

@ -144,7 +144,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
},
RunE: func(cmd *cobra.Command, args []string) error {
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password)
client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password)
if err != nil {
return fmt.Errorf("missing registry client: %w", err)
}

@ -245,3 +245,373 @@ func TestListOutputCompletion(t *testing.T) {
func TestListFileCompletion(t *testing.T) {
checkFileCompletion(t, "list", false)
}
func TestListOutputFormats(t *testing.T) {
defaultNamespace := "default"
timestamp := time.Unix(1452902400, 0).UTC()
chartInfo := &chart.Chart{
Metadata: &chart.Metadata{
Name: "test-chart",
Version: "1.0.0",
AppVersion: "0.0.1",
},
}
releaseFixture := []*release.Release{
{
Name: "test-release",
Version: 1,
Namespace: defaultNamespace,
Info: &release.Info{
LastDeployed: timestamp,
Status: common.StatusDeployed,
},
Chart: chartInfo,
},
}
tests := []cmdTestCase{{
name: "list releases in json format",
cmd: "list --output json",
golden: "output/list-json.txt",
rels: releaseFixture,
}, {
name: "list releases in yaml format",
cmd: "list --output yaml",
golden: "output/list-yaml.txt",
rels: releaseFixture,
}}
runTestCmd(t, tests)
}
func TestReleaseListWriter(t *testing.T) {
timestamp := time.Unix(1452902400, 0).UTC()
chartInfo := &chart.Chart{
Metadata: &chart.Metadata{
Name: "test-chart",
Version: "1.0.0",
AppVersion: "0.0.1",
},
}
releases := []*release.Release{
{
Name: "test-release",
Version: 1,
Namespace: "default",
Info: &release.Info{
LastDeployed: timestamp,
Status: common.StatusDeployed,
},
Chart: chartInfo,
},
}
tests := []struct {
name string
releases []*release.Release
timeFormat string
noHeaders bool
noColor bool
}{
{
name: "empty releases list",
releases: []*release.Release{},
timeFormat: "",
noHeaders: false,
noColor: false,
},
{
name: "custom time format",
releases: releases,
timeFormat: "2006-01-02",
noHeaders: false,
noColor: false,
},
{
name: "no headers",
releases: releases,
timeFormat: "",
noHeaders: true,
noColor: false,
},
{
name: "no color",
releases: releases,
timeFormat: "",
noHeaders: false,
noColor: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
writer := newReleaseListWriter(tt.releases, tt.timeFormat, tt.noHeaders, tt.noColor)
if writer == nil {
t.Error("Expected writer to be non-nil")
} else {
if len(writer.releases) != len(tt.releases) {
t.Errorf("Expected %d releases, got %d", len(tt.releases), len(writer.releases))
}
}
})
}
}
func TestReleaseListWriterMethods(t *testing.T) {
timestamp := time.Unix(1452902400, 0).UTC()
zeroTimestamp := time.Time{}
chartInfo := &chart.Chart{
Metadata: &chart.Metadata{
Name: "test-chart",
Version: "1.0.0",
AppVersion: "0.0.1",
},
}
releases := []*release.Release{
{
Name: "test-release",
Version: 1,
Namespace: "default",
Info: &release.Info{
LastDeployed: timestamp,
Status: common.StatusDeployed,
},
Chart: chartInfo,
},
{
Name: "zero-time-release",
Version: 1,
Namespace: "default",
Info: &release.Info{
LastDeployed: zeroTimestamp,
Status: common.StatusFailed,
},
Chart: chartInfo,
},
}
tests := []struct {
name string
status common.Status
}{
{"deployed", common.StatusDeployed},
{"failed", common.StatusFailed},
{"pending-install", common.StatusPendingInstall},
{"pending-upgrade", common.StatusPendingUpgrade},
{"pending-rollback", common.StatusPendingRollback},
{"uninstalling", common.StatusUninstalling},
{"uninstalled", common.StatusUninstalled},
{"superseded", common.StatusSuperseded},
{"unknown", common.StatusUnknown},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
testReleases := []*release.Release{
{
Name: "test-release",
Version: 1,
Namespace: "default",
Info: &release.Info{
LastDeployed: timestamp,
Status: tt.status,
},
Chart: chartInfo,
},
}
writer := newReleaseListWriter(testReleases, "", false, false)
var buf []byte
out := &bytesWriter{buf: &buf}
err := writer.WriteJSON(out)
if err != nil {
t.Errorf("WriteJSON failed: %v", err)
}
err = writer.WriteYAML(out)
if err != nil {
t.Errorf("WriteYAML failed: %v", err)
}
err = writer.WriteTable(out)
if err != nil {
t.Errorf("WriteTable failed: %v", err)
}
})
}
writer := newReleaseListWriter(releases, "", false, false)
var buf []byte
out := &bytesWriter{buf: &buf}
err := writer.WriteJSON(out)
if err != nil {
t.Errorf("WriteJSON failed: %v", err)
}
err = writer.WriteYAML(out)
if err != nil {
t.Errorf("WriteYAML failed: %v", err)
}
err = writer.WriteTable(out)
if err != nil {
t.Errorf("WriteTable failed: %v", err)
}
}
func TestFilterReleases(t *testing.T) {
releases := []*release.Release{
{Name: "release1"},
{Name: "release2"},
{Name: "release3"},
}
tests := []struct {
name string
releases []*release.Release
ignoredReleaseNames []string
expectedCount int
}{
{
name: "nil ignored list",
releases: releases,
ignoredReleaseNames: nil,
expectedCount: 3,
},
{
name: "empty ignored list",
releases: releases,
ignoredReleaseNames: []string{},
expectedCount: 3,
},
{
name: "filter one release",
releases: releases,
ignoredReleaseNames: []string{"release1"},
expectedCount: 2,
},
{
name: "filter multiple releases",
releases: releases,
ignoredReleaseNames: []string{"release1", "release3"},
expectedCount: 1,
},
{
name: "filter non-existent release",
releases: releases,
ignoredReleaseNames: []string{"non-existent"},
expectedCount: 3,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := filterReleases(tt.releases, tt.ignoredReleaseNames)
if len(result) != tt.expectedCount {
t.Errorf("Expected %d releases, got %d", tt.expectedCount, len(result))
}
})
}
}
type bytesWriter struct {
buf *[]byte
}
func (b *bytesWriter) Write(p []byte) (n int, err error) {
*b.buf = append(*b.buf, p...)
return len(p), nil
}
func TestListCustomTimeFormat(t *testing.T) {
defaultNamespace := "default"
timestamp := time.Unix(1452902400, 0).UTC()
chartInfo := &chart.Chart{
Metadata: &chart.Metadata{
Name: "test-chart",
Version: "1.0.0",
AppVersion: "0.0.1",
},
}
releaseFixture := []*release.Release{
{
Name: "test-release",
Version: 1,
Namespace: defaultNamespace,
Info: &release.Info{
LastDeployed: timestamp,
Status: common.StatusDeployed,
},
Chart: chartInfo,
},
}
tests := []cmdTestCase{{
name: "list releases with custom time format",
cmd: "list --time-format '2006-01-02 15:04:05'",
golden: "output/list-time-format.txt",
rels: releaseFixture,
}}
runTestCmd(t, tests)
}
func TestListStatusMapping(t *testing.T) {
defaultNamespace := "default"
timestamp := time.Unix(1452902400, 0).UTC()
chartInfo := &chart.Chart{
Metadata: &chart.Metadata{
Name: "test-chart",
Version: "1.0.0",
AppVersion: "0.0.1",
},
}
testCases := []struct {
name string
status common.Status
}{
{"deployed", common.StatusDeployed},
{"failed", common.StatusFailed},
{"pending-install", common.StatusPendingInstall},
{"pending-upgrade", common.StatusPendingUpgrade},
{"pending-rollback", common.StatusPendingRollback},
{"uninstalling", common.StatusUninstalling},
{"uninstalled", common.StatusUninstalled},
{"superseded", common.StatusSuperseded},
{"unknown", common.StatusUnknown},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
releaseFixture := []*release.Release{
{
Name: "test-release",
Version: 1,
Namespace: defaultNamespace,
Info: &release.Info{
LastDeployed: timestamp,
Status: tc.status,
},
Chart: chartInfo,
},
}
writer := newReleaseListWriter(releaseFixture, "", false, false)
if len(writer.releases) != 1 {
t.Errorf("Expected 1 release, got %d", len(writer.releases))
}
if writer.releases[0].Status != tc.status.String() {
t.Errorf("Expected status %s, got %s", tc.status.String(), writer.releases[0].Status)
}
})
}
}

@ -76,7 +76,7 @@ func newPackageCmd(out io.Writer) *cobra.Command {
}
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password)
client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password)
if err != nil {
return fmt.Errorf("missing registry client: %w", err)
}
@ -131,7 +131,7 @@ func newPackageCmd(out io.Writer) *cobra.Command {
f.StringVar(&client.Password, "password", "", "chart repository password where to locate the requested chart")
f.StringVar(&client.CertFile, "cert-file", "", "identify HTTPS client using this SSL certificate file")
f.StringVar(&client.KeyFile, "key-file", "", "identify HTTPS client using this SSL key file")
f.BoolVar(&client.InsecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart download")
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")

@ -40,7 +40,7 @@ type pluginInstallOptions struct {
certFile string
keyFile string
caFile string
insecureSkipTLSverify bool
insecureSkipTLSVerify bool
plainHTTP bool
password string
username string
@ -88,7 +88,7 @@ func newPluginInstallCmd(out io.Writer) *cobra.Command {
cmd.Flags().StringVar(&o.certFile, "cert-file", "", "identify registry client using this SSL certificate file")
cmd.Flags().StringVar(&o.keyFile, "key-file", "", "identify registry client using this SSL key file")
cmd.Flags().StringVar(&o.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle")
cmd.Flags().BoolVar(&o.insecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the plugin download")
cmd.Flags().BoolVar(&o.insecureSkipTLSVerify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the plugin download")
cmd.Flags().BoolVar(&o.plainHTTP, "plain-http", false, "use insecure HTTP connections for the plugin download")
cmd.Flags().StringVar(&o.username, "username", "", "registry username")
cmd.Flags().StringVar(&o.password, "password", "", "registry password")
@ -106,7 +106,7 @@ func (o *pluginInstallOptions) newInstallerForSource() (installer.Installer, err
// Build getter options for OCI
options := []getter.Option{
getter.WithTLSClientConfig(o.certFile, o.keyFile, o.caFile),
getter.WithInsecureSkipVerifyTLS(o.insecureSkipTLSverify),
getter.WithInsecureSkipVerifyTLS(o.insecureSkipTLSVerify),
getter.WithPlainHTTP(o.plainHTTP),
getter.WithBasicAuth(o.username, o.password),
}

@ -66,7 +66,7 @@ func newPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
}
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password)
client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password)
if err != nil {
return fmt.Errorf("missing registry client: %w", err)
}

@ -38,7 +38,7 @@ type registryPushOptions struct {
certFile string
keyFile string
caFile string
insecureSkipTLSverify bool
insecureSkipTLSVerify bool
plainHTTP bool
password string
username string
@ -71,7 +71,7 @@ func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
},
RunE: func(_ *cobra.Command, args []string) error {
registryClient, err := newRegistryClient(
o.certFile, o.keyFile, o.caFile, o.insecureSkipTLSverify, o.plainHTTP, o.username, o.password,
o.certFile, o.keyFile, o.caFile, o.insecureSkipTLSVerify, o.plainHTTP, o.username, o.password,
)
if err != nil {
@ -82,7 +82,7 @@ func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
remote := args[1]
client := action.NewPushWithOpts(action.WithPushConfig(cfg),
action.WithTLSClientConfig(o.certFile, o.keyFile, o.caFile),
action.WithInsecureSkipTLSVerify(o.insecureSkipTLSverify),
action.WithInsecureSkipTLSVerify(o.insecureSkipTLSVerify),
action.WithPlainHTTP(o.plainHTTP),
action.WithPushOptWriter(out))
client.Settings = settings
@ -99,7 +99,7 @@ func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f.StringVar(&o.certFile, "cert-file", "", "identify registry client using this SSL certificate file")
f.StringVar(&o.keyFile, "key-file", "", "identify registry client using this SSL key file")
f.StringVar(&o.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle")
f.BoolVar(&o.insecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart upload")
f.BoolVar(&o.insecureSkipTLSVerify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart upload")
f.BoolVar(&o.plainHTTP, "plain-http", false, "use insecure HTTP connections for the chart upload")
f.StringVar(&o.username, "username", "", "chart repository username where to locate the requested chart")
f.StringVar(&o.password, "password", "", "chart repository password where to locate the requested chart")

@ -57,7 +57,7 @@ type repoAddOptions struct {
certFile string
keyFile string
caFile string
insecureSkipTLSverify bool
insecureSkipTLSVerify bool
repoFile string
repoCache string
@ -94,7 +94,7 @@ func newRepoAddCmd(out io.Writer) *cobra.Command {
f.StringVar(&o.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file")
f.StringVar(&o.keyFile, "key-file", "", "identify HTTPS client using this SSL key file")
f.StringVar(&o.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle")
f.BoolVar(&o.insecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the repository")
f.BoolVar(&o.insecureSkipTLSVerify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the repository")
f.BoolVar(&o.allowDeprecatedRepos, "allow-deprecated-repos", false, "by default, this command will not allow adding official repos that have been permanently deleted. This disables that behavior")
f.BoolVar(&o.passCredentialsAll, "pass-credentials", false, "pass credentials to all domains")
f.DurationVar(&o.timeout, "timeout", getter.DefaultHTTPTimeout*time.Second, "time to wait for the index file download to complete")
@ -177,7 +177,7 @@ func (o *repoAddOptions) run(out io.Writer) error {
CertFile: o.certFile,
KeyFile: o.keyFile,
CAFile: o.caFile,
InsecureSkipTLSverify: o.insecureSkipTLSverify,
InsecureSkipTLSVerify: o.insecureSkipTLSVerify,
}
// Check if the repo name is legal

@ -103,7 +103,7 @@ By default, the default directories depend on the Operating System. The defaults
var settings = cli.New()
func NewRootCmd(out io.Writer, args []string, logSetup func(bool)) (*cobra.Command, error) {
actionConfig := new(action.Configuration)
actionConfig := action.NewConfiguration()
cmd, err := newRootCmdWithConfig(actionConfig, out, args, logSetup)
if err != nil {
return nil, err
@ -393,10 +393,10 @@ func checkForExpiredRepos(repofile string) {
}
func newRegistryClient(
certFile, keyFile, caFile string, insecureSkipTLSverify, plainHTTP bool, username, password string,
certFile, keyFile, caFile string, insecureSkipTLSVerify, plainHTTP bool, username, password string,
) (*registry.Client, error) {
if certFile != "" && keyFile != "" || caFile != "" || insecureSkipTLSverify {
registryClient, err := newRegistryClientWithTLS(certFile, keyFile, caFile, insecureSkipTLSverify, username, password)
if certFile != "" && keyFile != "" || caFile != "" || insecureSkipTLSVerify {
registryClient, err := newRegistryClientWithTLS(certFile, keyFile, caFile, insecureSkipTLSVerify, username, password)
if err != nil {
return nil, err
}
@ -430,10 +430,10 @@ func newDefaultRegistryClient(plainHTTP bool, username, password string) (*regis
}
func newRegistryClientWithTLS(
certFile, keyFile, caFile string, insecureSkipTLSverify bool, username, password string,
certFile, keyFile, caFile string, insecureSkipTLSVerify bool, username, password string,
) (*registry.Client, error) {
tlsConf, err := tlsutil.NewTLSConfig(
tlsutil.WithInsecureSkipVerify(insecureSkipTLSverify),
tlsutil.WithInsecureSkipVerify(insecureSkipTLSVerify),
tlsutil.WithCertKeyPairFiles(certFile, keyFile),
tlsutil.WithCAFile(caFile),
)

@ -287,7 +287,7 @@ func compListChartsOfRepo(repoName string, prefix string) []string {
if isNotExist(err) {
// If there is no cached charts file, fallback to the full index file.
// This is much slower but can happen after the caching feature is first
// installed but before the user does a 'helm repo update' to generate the
// installed but before the user does a 'helm repo update' to generate the
// first cached charts file.
path = filepath.Join(settings.RepositoryCache, helmpath.CacheIndexFile(repoName))
if indexFile, err := repo.LoadIndexFile(path); err == nil {

@ -227,7 +227,7 @@ func runShow(args []string, client *action.Show) (string, error) {
func addRegistryClient(client *action.Show) error {
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password)
client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password)
if err != nil {
return fmt.Errorf("missing registry client: %w", err)
}

@ -76,7 +76,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
}
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password)
client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password)
if err != nil {
return fmt.Errorf("missing registry client: %w", err)
}

@ -0,0 +1 @@
[{"name":"test-release","namespace":"default","revision":"1","updated":"2016-01-16 00:00:00 +0000 UTC","status":"deployed","chart":"test-chart-1.0.0","app_version":"0.0.1"}]

@ -0,0 +1,2 @@
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
test-release default 1 2016-01-16 00:00:00 deployed test-chart-1.0.0 0.0.1

@ -0,0 +1,7 @@
- app_version: 0.0.1
chart: test-chart-1.0.0
name: test-release
namespace: default
revision: "1"
status: deployed
updated: 2016-01-16 00:00:00 +0000 UTC

@ -1 +1 @@
version.BuildInfo{Version:"v4.0", GitCommit:"", GitTreeState:"", GoVersion:""}
version.BuildInfo{Version:"v4.0", GitCommit:"", GitTreeState:"", GoVersion:"", KubeClientVersion:"v."}

@ -105,7 +105,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client.Namespace = settings.Namespace()
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password)
client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password)
if err != nil {
return fmt.Errorf("missing registry client: %w", err)
}

@ -23,6 +23,7 @@ import (
"reflect"
"strings"
"testing"
"time"
"helm.sh/helm/v4/pkg/chart/common"
chart "helm.sh/helm/v4/pkg/chart/v2"
@ -408,7 +409,7 @@ func prepareMockRelease(t *testing.T, releaseName string) (func(n string, v int,
Description: "A Helm chart for Kubernetes",
Version: "0.1.0",
},
Templates: []*common.File{{Name: "templates/configmap.yaml", Data: configmapData}},
Templates: []*common.File{{Name: "templates/configmap.yaml", ModTime: time.Now(), Data: configmapData}},
}
chartPath := filepath.Join(tmpChart, cfile.Metadata.Name)
if err := chartutil.SaveDir(cfile, tmpChart); err != nil {
@ -513,6 +514,7 @@ func prepareMockReleaseWithSecret(t *testing.T, releaseName string) (func(n stri
if err != nil {
t.Fatalf("Error loading template yaml %v", err)
}
modTime := time.Now()
cfile := &chart.Chart{
Metadata: &chart.Metadata{
APIVersion: chart.APIVersionV1,
@ -520,7 +522,7 @@ func prepareMockReleaseWithSecret(t *testing.T, releaseName string) (func(n stri
Description: "A Helm chart for Kubernetes",
Version: "0.1.0",
},
Templates: []*common.File{{Name: "templates/configmap.yaml", Data: configmapData}, {Name: "templates/secret.yaml", Data: secretData}},
Templates: []*common.File{{Name: "templates/configmap.yaml", ModTime: modTime, Data: configmapData}, {Name: "templates/secret.yaml", ModTime: modTime, Data: secretData}},
}
chartPath := filepath.Join(tmpChart, cfile.Metadata.Name)
if err := chartutil.SaveDir(cfile, tmpChart); err != nil {

@ -377,7 +377,7 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
// Any failure to resolve/download a chart should fail:
// https://github.com/helm/helm/issues/1439
churl, username, password, insecureskiptlsverify, passcredentialsall, caFile, certFile, keyFile, err := m.findChartURL(dep.Name, dep.Version, dep.Repository, repos)
churl, username, password, insecureSkipTLSVerify, passCredentialsAll, caFile, certFile, keyFile, err := m.findChartURL(dep.Name, dep.Version, dep.Repository, repos)
if err != nil {
saveError = fmt.Errorf("could not find %s: %w", churl, err)
break
@ -401,8 +401,8 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
Getters: m.Getters,
Options: []getter.Option{
getter.WithBasicAuth(username, password),
getter.WithPassCredentialsAll(passcredentialsall),
getter.WithInsecureSkipVerifyTLS(insecureskiptlsverify),
getter.WithPassCredentialsAll(passCredentialsAll),
getter.WithInsecureSkipVerifyTLS(insecureSkipTLSVerify),
getter.WithTLSClientConfig(certFile, keyFile, caFile),
},
}
@ -787,7 +787,7 @@ func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) error {
// repoURL is the repository to search
//
// If it finds a URL that is "relative", it will prepend the repoURL.
func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (url, username, password string, insecureskiptlsverify, passcredentialsall bool, caFile, certFile, keyFile string, err error) {
func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (url, username, password string, insecureSkipTLSVerify, passCredentialsAll bool, caFile, certFile, keyFile string, err error) {
if registry.IsOCI(repoURL) {
return fmt.Sprintf("%s/%s:%s", repoURL, name, version), "", "", false, false, "", "", "", nil
}
@ -816,8 +816,8 @@ func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]*
}
username = cr.Config.Username
password = cr.Config.Password
passcredentialsall = cr.Config.PassCredentialsAll
insecureskiptlsverify = cr.Config.InsecureSkipTLSverify
passCredentialsAll = cr.Config.PassCredentialsAll
insecureSkipTLSVerify = cr.Config.InsecureSkipTLSVerify
caFile = cr.Config.CAFile
certFile = cr.Config.CertFile
keyFile = cr.Config.KeyFile

@ -23,6 +23,7 @@ import (
"sync"
"testing"
"text/template"
"time"
"github.com/stretchr/testify/assert"
@ -90,17 +91,18 @@ func TestFuncMap(t *testing.T) {
}
func TestRender(t *testing.T) {
modTime := time.Now()
c := &chart.Chart{
Metadata: &chart.Metadata{
Name: "moby",
Version: "1.2.3",
},
Templates: []*common.File{
{Name: "templates/test1", Data: []byte("{{.Values.outer | title }} {{.Values.inner | title}}")},
{Name: "templates/test2", Data: []byte("{{.Values.global.callme | lower }}")},
{Name: "templates/test3", Data: []byte("{{.noValue}}")},
{Name: "templates/test4", Data: []byte("{{toJson .Values}}")},
{Name: "templates/test5", Data: []byte("{{getHostByName \"helm.sh\"}}")},
{Name: "templates/test1", ModTime: modTime, Data: []byte("{{.Values.outer | title }} {{.Values.inner | title}}")},
{Name: "templates/test2", ModTime: modTime, Data: []byte("{{.Values.global.callme | lower }}")},
{Name: "templates/test3", ModTime: modTime, Data: []byte("{{.noValue}}")},
{Name: "templates/test4", ModTime: modTime, Data: []byte("{{toJson .Values}}")},
{Name: "templates/test5", ModTime: modTime, Data: []byte("{{getHostByName \"helm.sh\"}}")},
},
Values: map[string]interface{}{"outer": "DEFAULT", "inner": "DEFAULT"},
}
@ -140,14 +142,16 @@ func TestRender(t *testing.T) {
}
func TestRenderRefsOrdering(t *testing.T) {
modTime := time.Now()
parentChart := &chart.Chart{
Metadata: &chart.Metadata{
Name: "parent",
Version: "1.2.3",
},
Templates: []*common.File{
{Name: "templates/_helpers.tpl", Data: []byte(`{{- define "test" -}}parent value{{- end -}}`)},
{Name: "templates/test.yaml", Data: []byte(`{{ tpl "{{ include \"test\" . }}" . }}`)},
{Name: "templates/_helpers.tpl", ModTime: modTime, Data: []byte(`{{- define "test" -}}parent value{{- end -}}`)},
{Name: "templates/test.yaml", ModTime: modTime, Data: []byte(`{{ tpl "{{ include \"test\" . }}" . }}`)},
},
}
childChart := &chart.Chart{
@ -156,7 +160,7 @@ func TestRenderRefsOrdering(t *testing.T) {
Version: "1.2.3",
},
Templates: []*common.File{
{Name: "templates/_helpers.tpl", Data: []byte(`{{- define "test" -}}child value{{- end -}}`)},
{Name: "templates/_helpers.tpl", ModTime: modTime, Data: []byte(`{{- define "test" -}}child value{{- end -}}`)},
},
}
parentChart.AddDependency(childChart)
@ -220,7 +224,7 @@ func TestRenderWithDNS(t *testing.T) {
Version: "1.2.3",
},
Templates: []*common.File{
{Name: "templates/test1", Data: []byte("{{getHostByName \"helm.sh\"}}")},
{Name: "templates/test1", ModTime: time.Now(), Data: []byte("{{getHostByName \"helm.sh\"}}")},
},
Values: map[string]interface{}{},
}
@ -355,10 +359,12 @@ func TestRenderWithClientProvider(t *testing.T) {
Values: map[string]interface{}{},
}
modTime := time.Now()
for name, exp := range cases {
c.Templates = append(c.Templates, &common.File{
Name: path.Join("templates", name),
Data: []byte(exp.template),
Name: path.Join("templates", name),
ModTime: modTime,
Data: []byte(exp.template),
})
}
@ -393,7 +399,7 @@ func TestRenderWithClientProvider_error(t *testing.T) {
Version: "1.2.3",
},
Templates: []*common.File{
{Name: "templates/error", Data: []byte(`{{ lookup "v1" "Error" "" "" }}`)},
{Name: "templates/error", ModTime: time.Now(), Data: []byte(`{{ lookup "v1" "Error" "" "" }}`)},
},
Values: map[string]interface{}{},
}
@ -558,18 +564,19 @@ func TestFailErrors(t *testing.T) {
}
func TestAllTemplates(t *testing.T) {
modTime := time.Now()
ch1 := &chart.Chart{
Metadata: &chart.Metadata{Name: "ch1"},
Templates: []*common.File{
{Name: "templates/foo", Data: []byte("foo")},
{Name: "templates/bar", Data: []byte("bar")},
{Name: "templates/foo", ModTime: modTime, Data: []byte("foo")},
{Name: "templates/bar", ModTime: modTime, Data: []byte("bar")},
},
}
dep1 := &chart.Chart{
Metadata: &chart.Metadata{Name: "laboratory mice"},
Templates: []*common.File{
{Name: "templates/pinky", Data: []byte("pinky")},
{Name: "templates/brain", Data: []byte("brain")},
{Name: "templates/pinky", ModTime: modTime, Data: []byte("pinky")},
{Name: "templates/brain", ModTime: modTime, Data: []byte("brain")},
},
}
ch1.AddDependency(dep1)
@ -577,7 +584,7 @@ func TestAllTemplates(t *testing.T) {
dep2 := &chart.Chart{
Metadata: &chart.Metadata{Name: "same thing we do every night"},
Templates: []*common.File{
{Name: "templates/innermost", Data: []byte("innermost")},
{Name: "templates/innermost", ModTime: modTime, Data: []byte("innermost")},
},
}
dep1.AddDependency(dep2)
@ -589,16 +596,17 @@ func TestAllTemplates(t *testing.T) {
}
func TestChartValuesContainsIsRoot(t *testing.T) {
modTime := time.Now()
ch1 := &chart.Chart{
Metadata: &chart.Metadata{Name: "parent"},
Templates: []*common.File{
{Name: "templates/isroot", Data: []byte("{{.Chart.IsRoot}}")},
{Name: "templates/isroot", ModTime: modTime, Data: []byte("{{.Chart.IsRoot}}")},
},
}
dep1 := &chart.Chart{
Metadata: &chart.Metadata{Name: "child"},
Templates: []*common.File{
{Name: "templates/isroot", Data: []byte("{{.Chart.IsRoot}}")},
{Name: "templates/isroot", ModTime: modTime, Data: []byte("{{.Chart.IsRoot}}")},
},
}
ch1.AddDependency(dep1)
@ -621,16 +629,17 @@ func TestChartValuesContainsIsRoot(t *testing.T) {
func TestRenderDependency(t *testing.T) {
deptpl := `{{define "myblock"}}World{{end}}`
toptpl := `Hello {{template "myblock"}}`
modTime := time.Now()
ch := &chart.Chart{
Metadata: &chart.Metadata{Name: "outerchart"},
Templates: []*common.File{
{Name: "templates/outer", Data: []byte(toptpl)},
{Name: "templates/outer", ModTime: modTime, Data: []byte(toptpl)},
},
}
ch.AddDependency(&chart.Chart{
Metadata: &chart.Metadata{Name: "innerchart"},
Templates: []*common.File{
{Name: "templates/inner", Data: []byte(deptpl)},
{Name: "templates/inner", ModTime: modTime, Data: []byte(deptpl)},
},
})
@ -659,11 +668,12 @@ func TestRenderNestedValues(t *testing.T) {
// Ensure subcharts scopes are working.
subchartspath := "templates/subcharts.tpl"
modTime := time.Now()
deepest := &chart.Chart{
Metadata: &chart.Metadata{Name: "deepest"},
Templates: []*common.File{
{Name: deepestpath, Data: []byte(`And this same {{.Values.what}} that smiles {{.Values.global.when}}`)},
{Name: checkrelease, Data: []byte(`Tomorrow will be {{default "happy" .Release.Name }}`)},
{Name: deepestpath, ModTime: modTime, Data: []byte(`And this same {{.Values.what}} that smiles {{.Values.global.when}}`)},
{Name: checkrelease, ModTime: modTime, Data: []byte(`Tomorrow will be {{default "happy" .Release.Name }}`)},
},
Values: map[string]interface{}{"what": "milkshake", "where": "here"},
}
@ -671,7 +681,7 @@ func TestRenderNestedValues(t *testing.T) {
inner := &chart.Chart{
Metadata: &chart.Metadata{Name: "herrick"},
Templates: []*common.File{
{Name: innerpath, Data: []byte(`Old {{.Values.who}} is still a-flyin'`)},
{Name: innerpath, ModTime: modTime, Data: []byte(`Old {{.Values.who}} is still a-flyin'`)},
},
Values: map[string]interface{}{"who": "Robert", "what": "glasses"},
}
@ -680,8 +690,8 @@ func TestRenderNestedValues(t *testing.T) {
outer := &chart.Chart{
Metadata: &chart.Metadata{Name: "top"},
Templates: []*common.File{
{Name: outerpath, Data: []byte(`Gather ye {{.Values.what}} while ye may`)},
{Name: subchartspath, Data: []byte(`The glorious Lamp of {{.Subcharts.herrick.Subcharts.deepest.Values.where}}, the {{.Subcharts.herrick.Values.what}}`)},
{Name: outerpath, ModTime: modTime, Data: []byte(`Gather ye {{.Values.what}} while ye may`)},
{Name: subchartspath, ModTime: modTime, Data: []byte(`The glorious Lamp of {{.Subcharts.herrick.Subcharts.deepest.Values.where}}, the {{.Subcharts.herrick.Values.what}}`)},
},
Values: map[string]interface{}{
"what": "stinkweed",
@ -754,23 +764,24 @@ func TestRenderNestedValues(t *testing.T) {
}
func TestRenderBuiltinValues(t *testing.T) {
modTime := time.Now()
inner := &chart.Chart{
Metadata: &chart.Metadata{Name: "Latium", APIVersion: chart.APIVersionV2},
Templates: []*common.File{
{Name: "templates/Lavinia", Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)},
{Name: "templates/From", Data: []byte(`{{.Files.author | printf "%s"}} {{.Files.Get "book/title.txt"}}`)},
{Name: "templates/Lavinia", ModTime: modTime, Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)},
{Name: "templates/From", ModTime: modTime, Data: []byte(`{{.Files.author | printf "%s"}} {{.Files.Get "book/title.txt"}}`)},
},
Files: []*common.File{
{Name: "author", Data: []byte("Virgil")},
{Name: "book/title.txt", Data: []byte("Aeneid")},
{Name: "author", ModTime: modTime, Data: []byte("Virgil")},
{Name: "book/title.txt", ModTime: modTime, Data: []byte("Aeneid")},
},
}
outer := &chart.Chart{
Metadata: &chart.Metadata{Name: "Troy", APIVersion: chart.APIVersionV2},
Templates: []*common.File{
{Name: "templates/Aeneas", Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)},
{Name: "templates/Amata", Data: []byte(`{{.Subcharts.Latium.Chart.Name}} {{.Subcharts.Latium.Files.author | printf "%s"}}`)},
{Name: "templates/Aeneas", ModTime: modTime, Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)},
{Name: "templates/Amata", ModTime: modTime, Data: []byte(`{{.Subcharts.Latium.Chart.Name}} {{.Subcharts.Latium.Files.author | printf "%s"}}`)},
},
}
outer.AddDependency(inner)
@ -805,11 +816,12 @@ func TestRenderBuiltinValues(t *testing.T) {
}
func TestAlterFuncMap_include(t *testing.T) {
modTime := time.Now()
c := &chart.Chart{
Metadata: &chart.Metadata{Name: "conrad"},
Templates: []*common.File{
{Name: "templates/quote", Data: []byte(`{{include "conrad/templates/_partial" . | indent 2}} dead.`)},
{Name: "templates/_partial", Data: []byte(`{{.Release.Name}} - he`)},
{Name: "templates/quote", ModTime: modTime, Data: []byte(`{{include "conrad/templates/_partial" . | indent 2}} dead.`)},
{Name: "templates/_partial", ModTime: modTime, Data: []byte(`{{.Release.Name}} - he`)},
},
}
@ -817,8 +829,8 @@ func TestAlterFuncMap_include(t *testing.T) {
d := &chart.Chart{
Metadata: &chart.Metadata{Name: "nested"},
Templates: []*common.File{
{Name: "templates/quote", Data: []byte(`{{include "nested/templates/quote" . | indent 2}} dead.`)},
{Name: "templates/_partial", Data: []byte(`{{.Release.Name}} - he`)},
{Name: "templates/quote", ModTime: modTime, Data: []byte(`{{include "nested/templates/quote" . | indent 2}} dead.`)},
{Name: "templates/_partial", ModTime: modTime, Data: []byte(`{{.Release.Name}} - he`)},
},
}
@ -848,11 +860,12 @@ func TestAlterFuncMap_include(t *testing.T) {
}
func TestAlterFuncMap_require(t *testing.T) {
modTime := time.Now()
c := &chart.Chart{
Metadata: &chart.Metadata{Name: "conan"},
Templates: []*common.File{
{Name: "templates/quote", Data: []byte(`All your base are belong to {{ required "A valid 'who' is required" .Values.who }}`)},
{Name: "templates/bases", Data: []byte(`All {{ required "A valid 'bases' is required" .Values.bases }} of them!`)},
{Name: "templates/quote", ModTime: modTime, Data: []byte(`All your base are belong to {{ required "A valid 'who' is required" .Values.who }}`)},
{Name: "templates/bases", ModTime: modTime, Data: []byte(`All {{ required "A valid 'bases' is required" .Values.bases }} of them!`)},
},
}
@ -913,7 +926,7 @@ func TestAlterFuncMap_tpl(t *testing.T) {
c := &chart.Chart{
Metadata: &chart.Metadata{Name: "TplFunction"},
Templates: []*common.File{
{Name: "templates/base", Data: []byte(`Evaluate tpl {{tpl "Value: {{ .Values.value}}" .}}`)},
{Name: "templates/base", ModTime: time.Now(), Data: []byte(`Evaluate tpl {{tpl "Value: {{ .Values.value}}" .}}`)},
},
}
@ -942,7 +955,7 @@ func TestAlterFuncMap_tplfunc(t *testing.T) {
c := &chart.Chart{
Metadata: &chart.Metadata{Name: "TplFunction"},
Templates: []*common.File{
{Name: "templates/base", Data: []byte(`Evaluate tpl {{tpl "Value: {{ .Values.value | quote}}" .}}`)},
{Name: "templates/base", ModTime: time.Now(), Data: []byte(`Evaluate tpl {{tpl "Value: {{ .Values.value | quote}}" .}}`)},
},
}
@ -968,11 +981,12 @@ func TestAlterFuncMap_tplfunc(t *testing.T) {
}
func TestAlterFuncMap_tplinclude(t *testing.T) {
modTime := time.Now()
c := &chart.Chart{
Metadata: &chart.Metadata{Name: "TplFunction"},
Templates: []*common.File{
{Name: "templates/base", Data: []byte(`{{ tpl "{{include ` + "`" + `TplFunction/templates/_partial` + "`" + ` . | quote }}" .}}`)},
{Name: "templates/_partial", Data: []byte(`{{.Template.Name}}`)},
{Name: "templates/base", ModTime: modTime, Data: []byte(`{{ tpl "{{include ` + "`" + `TplFunction/templates/_partial` + "`" + ` . | quote }}" .}}`)},
{Name: "templates/_partial", ModTime: modTime, Data: []byte(`{{.Template.Name}}`)},
},
}
v := common.Values{
@ -998,12 +1012,14 @@ func TestAlterFuncMap_tplinclude(t *testing.T) {
}
func TestRenderRecursionLimit(t *testing.T) {
modTime := time.Now()
// endless recursion should produce an error
c := &chart.Chart{
Metadata: &chart.Metadata{Name: "bad"},
Templates: []*common.File{
{Name: "templates/base", Data: []byte(`{{include "recursion" . }}`)},
{Name: "templates/recursion", Data: []byte(`{{define "recursion"}}{{include "recursion" . }}{{end}}`)},
{Name: "templates/base", ModTime: modTime, Data: []byte(`{{include "recursion" . }}`)},
{Name: "templates/recursion", ModTime: modTime, Data: []byte(`{{define "recursion"}}{{include "recursion" . }}{{end}}`)},
},
}
v := common.Values{
@ -1032,8 +1048,8 @@ func TestRenderRecursionLimit(t *testing.T) {
d := &chart.Chart{
Metadata: &chart.Metadata{Name: "overlook"},
Templates: []*common.File{
{Name: "templates/quote", Data: []byte(repeatedIncl.String())},
{Name: "templates/_function", Data: []byte(printFunc)},
{Name: "templates/quote", ModTime: modTime, Data: []byte(repeatedIncl.String())},
{Name: "templates/_function", ModTime: modTime, Data: []byte(printFunc)},
},
}
@ -1053,15 +1069,16 @@ func TestRenderRecursionLimit(t *testing.T) {
}
func TestRenderLoadTemplateForTplFromFile(t *testing.T) {
modTime := time.Now()
c := &chart.Chart{
Metadata: &chart.Metadata{Name: "TplLoadFromFile"},
Templates: []*common.File{
{Name: "templates/base", Data: []byte(`{{ tpl (.Files.Get .Values.filename) . }}`)},
{Name: "templates/_function", Data: []byte(`{{define "test-function"}}test-function{{end}}`)},
{Name: "templates/base", ModTime: modTime, Data: []byte(`{{ tpl (.Files.Get .Values.filename) . }}`)},
{Name: "templates/_function", ModTime: modTime, Data: []byte(`{{define "test-function"}}test-function{{end}}`)},
},
Files: []*common.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" .}}`)},
{Name: "test", ModTime: modTime, Data: []byte(`{{ tpl (.Files.Get .Values.filename2) .}}`)},
{Name: "test2", ModTime: modTime, Data: []byte(`{{include "test-function" .}}{{define "nested-define"}}nested-define-content{{end}} {{include "nested-define" .}}`)},
},
}
@ -1088,12 +1105,13 @@ func TestRenderLoadTemplateForTplFromFile(t *testing.T) {
}
func TestRenderTplEmpty(t *testing.T) {
modTime := time.Now()
c := &chart.Chart{
Metadata: &chart.Metadata{Name: "TplEmpty"},
Templates: []*common.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}}" .}}`)},
{Name: "templates/empty-string", ModTime: modTime, Data: []byte(`{{tpl "" .}}`)},
{Name: "templates/empty-action", ModTime: modTime, Data: []byte(`{{tpl "{{ \"\"}}" .}}`)},
{Name: "templates/only-defines", ModTime: modTime, Data: []byte(`{{tpl "{{define \"not-invoked\"}}not-rendered{{end}}" .}}`)},
},
}
v := common.Values{
@ -1121,15 +1139,16 @@ func TestRenderTplEmpty(t *testing.T) {
}
func TestRenderTplTemplateNames(t *testing.T) {
modTime := time.Now()
// .Template.BasePath and .Name make it through
c := &chart.Chart{
Metadata: &chart.Metadata{Name: "TplTemplateNames"},
Templates: []*common.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}}`)},
{Name: "templates/modified-field", Data: []byte(`{{tpl "{{ .Template.Field }}" .Values.dot}}`)},
{Name: "templates/default-basepath", ModTime: modTime, Data: []byte(`{{tpl "{{ .Template.BasePath }}" .}}`)},
{Name: "templates/default-name", ModTime: modTime, Data: []byte(`{{tpl "{{ .Template.Name }}" .}}`)},
{Name: "templates/modified-basepath", ModTime: modTime, Data: []byte(`{{tpl "{{ .Template.BasePath }}" .Values.dot}}`)},
{Name: "templates/modified-name", ModTime: modTime, Data: []byte(`{{tpl "{{ .Template.Name }}" .Values.dot}}`)},
{Name: "templates/modified-field", ModTime: modTime, Data: []byte(`{{tpl "{{ .Template.Field }}" .Values.dot}}`)},
},
}
v := common.Values{
@ -1168,12 +1187,13 @@ func TestRenderTplTemplateNames(t *testing.T) {
}
func TestRenderTplRedefines(t *testing.T) {
modTime := time.Now()
// Redefining a template inside 'tpl' does not affect the outer definition
c := &chart.Chart{
Metadata: &chart.Metadata{Name: "TplRedefines"},
Templates: []*common.File{
{Name: "templates/_partials", Data: []byte(`{{define "partial"}}original-in-partial{{end}}`)},
{Name: "templates/partial", Data: []byte(
{Name: "templates/_partials", ModTime: modTime, Data: []byte(`{{define "partial"}}original-in-partial{{end}}`)},
{Name: "templates/partial", ModTime: modTime, Data: []byte(
`before: {{include "partial" .}}\n{{tpl .Values.partialText .}}\nafter: {{include "partial" .}}`,
)},
{Name: "templates/manifest", Data: []byte(
@ -1238,7 +1258,7 @@ func TestRenderTplMissingKey(t *testing.T) {
c := &chart.Chart{
Metadata: &chart.Metadata{Name: "TplMissingKey"},
Templates: []*common.File{
{Name: "templates/manifest", Data: []byte(
{Name: "templates/manifest", ModTime: time.Now(), Data: []byte(
`missingValue: {{tpl "{{.Values.noSuchKey}}" .}}`,
)},
},
@ -1271,7 +1291,7 @@ func TestRenderTplMissingKeyString(t *testing.T) {
c := &chart.Chart{
Metadata: &chart.Metadata{Name: "TplMissingKeyStrict"},
Templates: []*common.File{
{Name: "templates/manifest", Data: []byte(
{Name: "templates/manifest", ModTime: time.Now(), Data: []byte(
`missingValue: {{tpl "{{.Values.noSuchKey}}" .}}`,
)},
},
@ -1300,16 +1320,17 @@ func TestRenderTplMissingKeyString(t *testing.T) {
}
func TestNestedHelpersProducesMultilineStacktrace(t *testing.T) {
modTime := time.Now()
c := &chart.Chart{
Metadata: &chart.Metadata{Name: "NestedHelperFunctions"},
Templates: []*common.File{
{Name: "templates/svc.yaml", Data: []byte(
{Name: "templates/svc.yaml", ModTime: modTime, Data: []byte(
`name: {{ include "nested_helper.name" . }}`,
)},
{Name: "templates/_helpers_1.tpl", Data: []byte(
{Name: "templates/_helpers_1.tpl", ModTime: modTime, Data: []byte(
`{{- define "nested_helper.name" -}}{{- include "common.names.get_name" . -}}{{- end -}}`,
)},
{Name: "charts/common/templates/_helpers_2.tpl", Data: []byte(
{Name: "charts/common/templates/_helpers_2.tpl", ModTime: modTime, Data: []byte(
`{{- define "common.names.get_name" -}}{{- .Values.nonexistant.key | trunc 63 | trimSuffix "-" -}}{{- end -}}`,
)},
},
@ -1338,16 +1359,17 @@ NestedHelperFunctions/charts/common/templates/_helpers_2.tpl:1:49
}
func TestMultilineNoTemplateAssociatedError(t *testing.T) {
modTime := time.Now()
c := &chart.Chart{
Metadata: &chart.Metadata{Name: "multiline"},
Templates: []*common.File{
{Name: "templates/svc.yaml", Data: []byte(
{Name: "templates/svc.yaml", ModTime: modTime, Data: []byte(
`name: {{ include "nested_helper.name" . }}`,
)},
{Name: "templates/test.yaml", Data: []byte(
{Name: "templates/test.yaml", ModTime: modTime, Data: []byte(
`{{ toYaml .Values }}`,
)},
{Name: "charts/common/templates/_helpers_2.tpl", Data: []byte(
{Name: "charts/common/templates/_helpers_2.tpl", ModTime: modTime, Data: []byte(
`{{ toYaml .Values }}`,
)},
},
@ -1371,17 +1393,21 @@ template: no template "nested_helper.name" associated with template "gotpl"`
}
func TestRenderCustomTemplateFuncs(t *testing.T) {
modTime := time.Now()
// Create a chart with two templates that use custom functions
c := &chart.Chart{
Metadata: &chart.Metadata{Name: "CustomFunc"},
Templates: []*common.File{
{
Name: "templates/manifest",
Data: []byte(`{{exclaim .Values.message}}`),
Name: "templates/manifest",
ModTime: modTime,
Data: []byte(`{{exclaim .Values.message}}`),
},
{
Name: "templates/override",
Data: []byte(`{{ upper .Values.message }}`),
Name: "templates/override",
ModTime: modTime,
Data: []byte(`{{ upper .Values.message }}`),
},
},
}

@ -64,7 +64,7 @@ func (f files) Get(name string) string {
}
// Glob takes a glob pattern and returns another files object only containing
// matched files.
// matched files.
//
// This is designed to be called from a template.
//

@ -306,11 +306,11 @@ func TestDownload(t *testing.T) {
func TestDownloadTLS(t *testing.T) {
cd := "../../testdata"
ca, pub, priv := filepath.Join(cd, "rootca.crt"), filepath.Join(cd, "crt.pem"), filepath.Join(cd, "key.pem")
insecureSkipTLSverify := false
insecureSkipTLSVerify := false
tlsSrv := httptest.NewUnstartedServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}))
tlsConf, err := tlsutil.NewTLSConfig(
tlsutil.WithInsecureSkipVerify(insecureSkipTLSverify),
tlsutil.WithInsecureSkipVerify(insecureSkipTLSVerify),
tlsutil.WithCertKeyPairFiles(pub, priv),
tlsutil.WithCAFile(ca),
)
@ -359,14 +359,14 @@ func TestDownloadTLS(t *testing.T) {
func TestDownloadTLSWithRedirect(t *testing.T) {
cd := "../../testdata"
srv2Resp := "hello"
insecureSkipTLSverify := false
insecureSkipTLSVerify := false
// Server 2 that will actually fulfil the request.
ca, pub, priv := filepath.Join(cd, "rootca.crt"), filepath.Join(cd, "localhost-crt.pem"), filepath.Join(cd, "key.pem")
tlsConf, err := tlsutil.NewTLSConfig(
tlsutil.WithCAFile(ca),
tlsutil.WithCertKeyPairFiles(pub, priv),
tlsutil.WithInsecureSkipVerify(insecureSkipTLSverify),
tlsutil.WithInsecureSkipVerify(insecureSkipTLSVerify),
)
if err != nil {
@ -387,7 +387,7 @@ func TestDownloadTLSWithRedirect(t *testing.T) {
tlsConf, err = tlsutil.NewTLSConfig(
tlsutil.WithCAFile(ca),
tlsutil.WithCertKeyPairFiles(pub, priv),
tlsutil.WithInsecureSkipVerify(insecureSkipTLSverify),
tlsutil.WithInsecureSkipVerify(insecureSkipTLSVerify),
)
if err != nil {
@ -520,11 +520,11 @@ func TestHTTPGetterTarDownload(t *testing.T) {
b := make([]byte, 512)
f.Read(b)
//Get the file size
// Get the file size
FileStat, _ := f.Stat()
FileSize := strconv.FormatInt(FileStat.Size(), 10)
//Simulating improper header values from bitbucket
// Simulating improper header values from bitbucket
w.Header().Set("Content-Type", "application/x-tar")
w.Header().Set("Content-Encoding", "gzip")
w.Header().Set("Content-Length", FileSize)

@ -109,7 +109,7 @@ func (g *getterPlugin) Get(href string, options ...Option) (*bytes.Buffer, error
Protocol: u.Scheme,
},
// TODO should we pass Stdin, Stdout, and Stderr through Input here to getter plugins?
//Stdout: os.Stdout,
// Stdout: os.Stdout,
}
output, err := g.plg.Invoke(context.Background(), input)
if err != nil {

@ -39,6 +39,8 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
"helm.sh/helm/v4/internal/logging"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -82,8 +84,16 @@ type Client struct {
// Namespace allows to bypass the kubeconfig file for the choice of the namespace
Namespace string
// WaitContext is an optional context to use for wait operations.
// If not set, a context will be created internally using the
// timeout provided to the wait functions.
WaitContext context.Context
Waiter
kubeClient kubernetes.Interface
// Embed a LogHolder to provide logger functionality
logging.LogHolder
}
var _ Interface = (*Client)(nil)
@ -135,6 +145,7 @@ func (c *Client) newStatusWatcher() (*statusWaiter, error) {
return &statusWaiter{
restMapper: restMapper,
client: dynamicClient,
ctx: c.WaitContext,
}, nil
}
@ -145,7 +156,7 @@ func (c *Client) GetWaiter(strategy WaitStrategy) (Waiter, error) {
if err != nil {
return nil, err
}
return &legacyWaiter{kubeClient: kc}, nil
return &legacyWaiter{kubeClient: kc, ctx: c.WaitContext}, nil
case StatusWatcherStrategy:
return c.newStatusWatcher()
case HookOnlyStrategy:
@ -177,6 +188,7 @@ func New(getter genericclioptions.RESTClientGetter) *Client {
c := &Client{
Factory: factory,
}
c.SetLogger(slog.Default().Handler())
return c
}
@ -258,7 +270,7 @@ func ClientCreateOptionFieldValidationDirective(fieldValidationDirective FieldVa
// Create creates Kubernetes resources specified in the resource list.
func (c *Client) Create(resources ResourceList, options ...ClientCreateOption) (*Result, error) {
slog.Debug("creating resource(s)", "resources", len(resources))
c.Logger().Debug("creating resource(s)", "resources", len(resources))
createOptions := clientCreateOptions{
serverSideApply: true, // Default to server-side apply
@ -275,11 +287,11 @@ func (c *Client) Create(resources ResourceList, options ...ClientCreateOption) (
makeCreateApplyFunc := func() func(target *resource.Info) error {
if createOptions.serverSideApply {
slog.Debug("using server-side apply for resource creation", slog.Bool("forceConflicts", createOptions.forceConflicts), slog.Bool("dryRun", createOptions.dryRun), slog.String("fieldValidationDirective", string(createOptions.fieldValidationDirective)))
c.Logger().Debug("using server-side apply for resource creation", slog.Bool("forceConflicts", createOptions.forceConflicts), slog.Bool("dryRun", createOptions.dryRun), slog.String("fieldValidationDirective", string(createOptions.fieldValidationDirective)))
return func(target *resource.Info) error {
err := patchResourceServerSide(target, createOptions.dryRun, createOptions.forceConflicts, createOptions.fieldValidationDirective)
logger := slog.With(
logger := c.Logger().With(
slog.String("namespace", target.Namespace),
slog.String("name", target.Name),
slog.String("gvk", target.Mapping.GroupVersionKind.String()))
@ -294,7 +306,7 @@ func (c *Client) Create(resources ResourceList, options ...ClientCreateOption) (
}
}
slog.Debug("using client-side apply for resource creation")
c.Logger().Debug("using client-side apply for resource creation")
return createResource
}
@ -349,7 +361,7 @@ func (c *Client) Get(resources ResourceList, related bool) (map[string][]runtime
objs, err = c.getSelectRelationPod(info, objs, isTable, &podSelectors)
if err != nil {
slog.Warn("get the relation pod is failed", slog.Any("error", err))
c.Logger().Warn("get the relation pod is failed", slog.Any("error", err))
}
}
}
@ -367,7 +379,7 @@ func (c *Client) getSelectRelationPod(info *resource.Info, objs map[string][]run
if info == nil {
return objs, nil
}
slog.Debug("get relation pod of object", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind)
c.Logger().Debug("get relation pod of object", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind)
selector, ok, _ := getSelectorFromObject(info.Object)
if !ok {
return objs, nil
@ -504,7 +516,7 @@ func (c *Client) update(originals, targets ResourceList, updateApplyFunc UpdateA
updateErrors := []error{}
res := &Result{}
slog.Debug("checking resources for changes", "resources", len(targets))
c.Logger().Debug("checking resources for changes", "resources", len(targets))
err := targets.Visit(func(target *resource.Info, err error) error {
if err != nil {
return err
@ -525,7 +537,7 @@ func (c *Client) update(originals, targets ResourceList, updateApplyFunc UpdateA
}
kind := target.Mapping.GroupVersionKind.Kind
slog.Debug("created a new resource", "namespace", target.Namespace, "name", target.Name, "kind", kind)
c.Logger().Debug("created a new resource", "namespace", target.Namespace, "name", target.Name, "kind", kind)
return nil
}
@ -553,26 +565,33 @@ func (c *Client) update(originals, targets ResourceList, updateApplyFunc UpdateA
}
for _, info := range originals.Difference(targets) {
slog.Debug("deleting resource", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind)
c.Logger().Debug("deleting resource", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind)
if err := info.Get(); err != nil {
slog.Debug("unable to get object", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind, slog.Any("error", err))
c.Logger().Debug("unable to get object", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind, slog.Any("error", err))
continue
}
annotations, err := metadataAccessor.Annotations(info.Object)
if err != nil {
slog.Debug("unable to get annotations", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind, slog.Any("error", err))
c.Logger().Debug("unable to get annotations", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind, slog.Any("error", err))
}
if annotations != nil && annotations[ResourcePolicyAnno] == KeepPolicy {
slog.Debug("skipping delete due to annotation", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind, "annotation", ResourcePolicyAnno, "value", KeepPolicy)
c.Logger().Debug("skipping delete due to annotation", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind, "annotation", ResourcePolicyAnno, "value", KeepPolicy)
continue
}
if err := deleteResource(info, metav1.DeletePropagationBackground); err != nil {
slog.Debug("failed to delete resource", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind, slog.Any("error", err))
c.Logger().Debug("failed to delete resource", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind, slog.Any("error", err))
if !apierrors.IsNotFound(err) {
updateErrors = append(updateErrors, fmt.Errorf("failed to delete resource %s: %w", info.Name, err))
}
continue
}
res.Deleted = append(res.Deleted, info)
}
if len(updateErrors) != 0 {
return res, joinErrors(updateErrors, " && ")
}
return res, nil
}
@ -689,40 +708,40 @@ func (c *Client) Update(originals, targets ResourceList, options ...ClientUpdate
errs = append(errs, o(&updateOptions))
}
if err := errors.Join(errs...); err != nil {
return nil, fmt.Errorf("invalid client update option(s): %w", err)
return &Result{}, fmt.Errorf("invalid client update option(s): %w", err)
}
if updateOptions.threeWayMergeForUnstructured && updateOptions.serverSideApply {
return nil, fmt.Errorf("invalid operation: cannot use three-way merge for unstructured and server-side apply together")
return &Result{}, fmt.Errorf("invalid operation: cannot use three-way merge for unstructured and server-side apply together")
}
if updateOptions.forceConflicts && updateOptions.forceReplace {
return nil, fmt.Errorf("invalid operation: cannot use force conflicts and force replace together")
return &Result{}, fmt.Errorf("invalid operation: cannot use force conflicts and force replace together")
}
if updateOptions.serverSideApply && updateOptions.forceReplace {
return nil, fmt.Errorf("invalid operation: cannot use server-side apply and force replace together")
return &Result{}, fmt.Errorf("invalid operation: cannot use server-side apply and force replace together")
}
makeUpdateApplyFunc := func() UpdateApplyFunc {
if updateOptions.forceReplace {
slog.Debug(
c.Logger().Debug(
"using resource replace update strategy",
slog.String("fieldValidationDirective", string(updateOptions.fieldValidationDirective)))
return func(original, target *resource.Info) error {
if err := replaceResource(target, updateOptions.fieldValidationDirective); err != nil {
slog.Debug("error replacing the resource", "namespace", target.Namespace, "name", target.Name, "kind", target.Mapping.GroupVersionKind.Kind, slog.Any("error", err))
c.Logger().Debug("error replacing the resource", "namespace", target.Namespace, "name", target.Name, "kind", target.Mapping.GroupVersionKind.Kind, slog.Any("error", err))
return err
}
originalObject := original.Object
kind := target.Mapping.GroupVersionKind.Kind
slog.Debug("replace succeeded", "name", original.Name, "initialKind", originalObject.GetObjectKind().GroupVersionKind().Kind, "kind", kind)
c.Logger().Debug("replace succeeded", "name", original.Name, "initialKind", originalObject.GetObjectKind().GroupVersionKind().Kind, "kind", kind)
return nil
}
} else if updateOptions.serverSideApply {
slog.Debug(
c.Logger().Debug(
"using server-side apply for resource update",
slog.Bool("forceConflicts", updateOptions.forceConflicts),
slog.Bool("dryRun", updateOptions.dryRun),
@ -730,7 +749,7 @@ func (c *Client) Update(originals, targets ResourceList, options ...ClientUpdate
slog.Bool("upgradeClientSideFieldManager", updateOptions.upgradeClientSideFieldManager))
return func(original, target *resource.Info) error {
logger := slog.With(
logger := c.Logger().With(
slog.String("namespace", target.Namespace),
slog.String("name", target.Name),
slog.String("gvk", target.Mapping.GroupVersionKind.String()))
@ -738,7 +757,7 @@ func (c *Client) Update(originals, targets ResourceList, options ...ClientUpdate
if updateOptions.upgradeClientSideFieldManager {
patched, err := upgradeClientSideFieldManager(original, updateOptions.dryRun, updateOptions.fieldValidationDirective)
if err != nil {
slog.Debug("Error patching resource to replace CSA field management", slog.Any("error", err))
c.Logger().Debug("Error patching resource to replace CSA field management", slog.Any("error", err))
return err
}
@ -758,7 +777,7 @@ func (c *Client) Update(originals, targets ResourceList, options ...ClientUpdate
}
}
slog.Debug("using client-side apply for resource update", slog.Bool("threeWayMergeForUnstructured", updateOptions.threeWayMergeForUnstructured))
c.Logger().Debug("using client-side apply for resource update", slog.Bool("threeWayMergeForUnstructured", updateOptions.threeWayMergeForUnstructured))
return func(original, target *resource.Info) error {
return patchResourceClientSide(original.Object, target, updateOptions.threeWayMergeForUnstructured)
}
@ -776,11 +795,11 @@ func (c *Client) Delete(resources ResourceList, policy metav1.DeletionPropagatio
res := &Result{}
mtx := sync.Mutex{}
err := perform(resources, func(target *resource.Info) error {
slog.Debug("starting delete resource", "namespace", target.Namespace, "name", target.Name, "kind", target.Mapping.GroupVersionKind.Kind)
c.Logger().Debug("starting delete resource", "namespace", target.Namespace, "name", target.Name, "kind", target.Mapping.GroupVersionKind.Kind)
err := deleteResource(target, policy)
if err == nil || apierrors.IsNotFound(err) {
if err != nil {
slog.Debug("ignoring delete failure", "namespace", target.Namespace, "name", target.Name, "kind", target.Mapping.GroupVersionKind.Kind, slog.Any("error", err))
c.Logger().Debug("ignoring delete failure", "namespace", target.Namespace, "name", target.Name, "kind", target.Mapping.GroupVersionKind.Kind, slog.Any("error", err))
}
mtx.Lock()
defer mtx.Unlock()

@ -18,6 +18,7 @@ package kube
import (
"bytes"
"context"
"errors"
"fmt"
"io"
@ -321,6 +322,7 @@ func TestUpdate(t *testing.T) {
ThreeWayMergeForUnstructured bool
ServerSideApply bool
ExpectedActions []string
ExpectedError string
}
expectedActionsClientSideApply := []string{
@ -336,6 +338,8 @@ func TestUpdate(t *testing.T) {
"/namespaces/default/pods:POST", // retry due to 409
"/namespaces/default/pods/squid:GET",
"/namespaces/default/pods/squid:DELETE",
"/namespaces/default/pods/notfound:GET",
"/namespaces/default/pods/notfound:DELETE",
}
expectedActionsServerSideApply := []string{
@ -351,11 +355,13 @@ func TestUpdate(t *testing.T) {
"/namespaces/default/pods:POST", // retry due to 409
"/namespaces/default/pods/squid:GET",
"/namespaces/default/pods/squid:DELETE",
"/namespaces/default/pods/notfound:GET",
"/namespaces/default/pods/notfound:DELETE",
}
testCases := map[string]testCase{
"client-side apply": {
OriginalPods: newPodList("starfish", "otter", "squid"),
OriginalPods: newPodList("starfish", "otter", "squid", "notfound"),
TargetPods: func() v1.PodList {
listTarget := newPodList("starfish", "otter", "dolphin")
listTarget.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}}
@ -365,9 +371,10 @@ func TestUpdate(t *testing.T) {
ThreeWayMergeForUnstructured: false,
ServerSideApply: false,
ExpectedActions: expectedActionsClientSideApply,
ExpectedError: "",
},
"client-side apply (three-way merge for unstructured)": {
OriginalPods: newPodList("starfish", "otter", "squid"),
OriginalPods: newPodList("starfish", "otter", "squid", "notfound"),
TargetPods: func() v1.PodList {
listTarget := newPodList("starfish", "otter", "dolphin")
listTarget.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}}
@ -377,9 +384,10 @@ func TestUpdate(t *testing.T) {
ThreeWayMergeForUnstructured: true,
ServerSideApply: false,
ExpectedActions: expectedActionsClientSideApply,
ExpectedError: "",
},
"serverSideApply": {
OriginalPods: newPodList("starfish", "otter", "squid"),
OriginalPods: newPodList("starfish", "otter", "squid", "notfound"),
TargetPods: func() v1.PodList {
listTarget := newPodList("starfish", "otter", "dolphin")
listTarget.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}}
@ -389,6 +397,23 @@ func TestUpdate(t *testing.T) {
ThreeWayMergeForUnstructured: false,
ServerSideApply: true,
ExpectedActions: expectedActionsServerSideApply,
ExpectedError: "",
},
"serverSideApply with forbidden deletion": {
OriginalPods: newPodList("starfish", "otter", "squid", "notfound", "forbidden"),
TargetPods: func() v1.PodList {
listTarget := newPodList("starfish", "otter", "dolphin")
listTarget.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}}
return listTarget
}(),
ThreeWayMergeForUnstructured: false,
ServerSideApply: true,
ExpectedActions: append(expectedActionsServerSideApply,
"/namespaces/default/pods/forbidden:GET",
"/namespaces/default/pods/forbidden:DELETE",
),
ExpectedError: "failed to delete resource forbidden:",
},
}
@ -444,6 +469,22 @@ func TestUpdate(t *testing.T) {
return newResponse(http.StatusOK, &listTarget.Items[1])
case p == "/namespaces/default/pods/squid" && m == http.MethodGet:
return newResponse(http.StatusOK, &listTarget.Items[2])
case p == "/namespaces/default/pods/notfound" && m == http.MethodGet:
// Resource exists in original but will simulate not found on delete
return newResponse(http.StatusOK, &listOriginal.Items[3])
case p == "/namespaces/default/pods/notfound" && m == http.MethodDelete:
// Simulate a not found during deletion; should not cause update to fail
return newResponse(http.StatusNotFound, notFoundBody())
case p == "/namespaces/default/pods/forbidden" && m == http.MethodGet:
return newResponse(http.StatusOK, &listOriginal.Items[4])
case p == "/namespaces/default/pods/forbidden" && m == http.MethodDelete:
// Simulate RBAC forbidden that should cause update to fail
return newResponse(http.StatusForbidden, &metav1.Status{
Status: metav1.StatusFailure,
Message: "pods \"forbidden\" is forbidden: User \"test-user\" cannot delete resource \"pods\" in API group \"\" in the namespace \"default\"",
Reason: metav1.StatusReasonForbidden,
Code: http.StatusForbidden,
})
default:
}
@ -471,7 +512,13 @@ func TestUpdate(t *testing.T) {
ClientUpdateOptionForceReplace(false),
ClientUpdateOptionServerSideApply(tc.ServerSideApply, false),
ClientUpdateOptionUpgradeClientSideFieldManager(true))
require.NoError(t, err)
if tc.ExpectedError != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tc.ExpectedError)
} else {
require.NoError(t, err)
}
assert.Len(t, result.Created, 1, "expected 1 resource created, got %d", len(result.Created))
assert.Len(t, result.Updated, 2, "expected 2 resource updated, got %d", len(result.Updated))
@ -1752,3 +1799,387 @@ func TestDetermineFieldValidationDirective(t *testing.T) {
assert.Equal(t, FieldValidationDirectiveIgnore, determineFieldValidationDirective(false))
assert.Equal(t, FieldValidationDirectiveStrict, determineFieldValidationDirective(true))
}
func TestClientWaitContextCancellationLegacy(t *testing.T) {
podList := newPodList("starfish", "otter")
ctx, cancel := context.WithCancel(t.Context())
c := newTestClient(t)
c.WaitContext = ctx
requestCount := 0
c.Factory.(*cmdtesting.TestFactory).Client = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
requestCount++
p, m := req.URL.Path, req.Method
t.Logf("got request %s %s", p, m)
if requestCount == 2 {
cancel()
}
switch {
case p == "/api/v1/namespaces/default/pods/starfish" && m == http.MethodGet:
pod := &podList.Items[0]
pod.Status.Conditions = []v1.PodCondition{
{
Type: v1.PodReady,
Status: v1.ConditionFalse,
},
}
return newResponse(http.StatusOK, pod)
case p == "/api/v1/namespaces/default/pods/otter" && m == http.MethodGet:
pod := &podList.Items[1]
pod.Status.Conditions = []v1.PodCondition{
{
Type: v1.PodReady,
Status: v1.ConditionFalse,
},
}
return newResponse(http.StatusOK, pod)
case p == "/namespaces/default/pods" && m == http.MethodPost:
resources, err := c.Build(req.Body, false)
if err != nil {
t.Fatal(err)
}
return newResponse(http.StatusOK, resources[0].Object)
default:
t.Logf("unexpected request: %s %s", req.Method, req.URL.Path)
return newResponse(http.StatusNotFound, notFoundBody())
}
}),
}
var err error
c.Waiter, err = c.GetWaiter(LegacyStrategy)
require.NoError(t, err)
resources, err := c.Build(objBody(&podList), false)
require.NoError(t, err)
result, err := c.Create(
resources,
ClientCreateOptionServerSideApply(false, false))
require.NoError(t, err)
assert.Len(t, result.Created, 2, "expected 2 resources created, got %d", len(result.Created))
err = c.Wait(resources, time.Second*30)
require.Error(t, err)
assert.Contains(t, err.Error(), "context canceled", "expected context canceled error, got: %v", err)
}
func TestClientWaitWithJobsContextCancellationLegacy(t *testing.T) {
job := newJob("starfish", 0, intToInt32(1), 0, 0)
ctx, cancel := context.WithCancel(t.Context())
c := newTestClient(t)
c.WaitContext = ctx
requestCount := 0
c.Factory.(*cmdtesting.TestFactory).Client = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
requestCount++
p, m := req.URL.Path, req.Method
t.Logf("got request %s %s", p, m)
if requestCount == 2 {
cancel()
}
switch {
case p == "/apis/batch/v1/namespaces/default/jobs/starfish" && m == http.MethodGet:
job.Status.Succeeded = 0
return newResponse(http.StatusOK, job)
case p == "/namespaces/default/jobs" && m == http.MethodPost:
resources, err := c.Build(req.Body, false)
if err != nil {
t.Fatal(err)
}
return newResponse(http.StatusOK, resources[0].Object)
default:
t.Logf("unexpected request: %s %s", req.Method, req.URL.Path)
return newResponse(http.StatusNotFound, notFoundBody())
}
}),
}
var err error
c.Waiter, err = c.GetWaiter(LegacyStrategy)
require.NoError(t, err)
resources, err := c.Build(objBody(job), false)
require.NoError(t, err)
result, err := c.Create(
resources,
ClientCreateOptionServerSideApply(false, false))
require.NoError(t, err)
assert.Len(t, result.Created, 1, "expected 1 resource created, got %d", len(result.Created))
err = c.WaitWithJobs(resources, time.Second*30)
require.Error(t, err)
assert.Contains(t, err.Error(), "context canceled", "expected context canceled error, got: %v", err)
}
func TestClientWaitForDeleteContextCancellationLegacy(t *testing.T) {
pod := newPod("starfish")
ctx, cancel := context.WithCancel(t.Context())
c := newTestClient(t)
c.WaitContext = ctx
deleted := false
requestCount := 0
c.Factory.(*cmdtesting.TestFactory).Client = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
requestCount++
p, m := req.URL.Path, req.Method
t.Logf("got request %s %s", p, m)
if requestCount == 3 {
cancel()
}
switch {
case p == "/namespaces/default/pods/starfish" && m == http.MethodGet:
if deleted {
return newResponse(http.StatusOK, &pod)
}
return newResponse(http.StatusOK, &pod)
case p == "/namespaces/default/pods/starfish" && m == http.MethodDelete:
deleted = true
return newResponse(http.StatusOK, &pod)
case p == "/namespaces/default/pods" && m == http.MethodPost:
resources, err := c.Build(req.Body, false)
if err != nil {
t.Fatal(err)
}
return newResponse(http.StatusOK, resources[0].Object)
default:
t.Logf("unexpected request: %s %s", req.Method, req.URL.Path)
return newResponse(http.StatusNotFound, notFoundBody())
}
}),
}
var err error
c.Waiter, err = c.GetWaiter(LegacyStrategy)
require.NoError(t, err)
resources, err := c.Build(objBody(&pod), false)
require.NoError(t, err)
result, err := c.Create(
resources,
ClientCreateOptionServerSideApply(false, false))
require.NoError(t, err)
assert.Len(t, result.Created, 1, "expected 1 resource created, got %d", len(result.Created))
if _, err := c.Delete(resources, metav1.DeletePropagationBackground); err != nil {
t.Fatal(err)
}
err = c.WaitForDelete(resources, time.Second*30)
require.Error(t, err)
assert.Contains(t, err.Error(), "context canceled", "expected context canceled error, got: %v", err)
}
func TestClientWaitContextNilDoesNotPanic(t *testing.T) {
podList := newPodList("starfish")
var created *time.Time
c := newTestClient(t)
c.WaitContext = nil
c.Factory.(*cmdtesting.TestFactory).Client = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
p, m := req.URL.Path, req.Method
t.Logf("got request %s %s", p, m)
switch {
case p == "/api/v1/namespaces/default/pods/starfish" && m == http.MethodGet:
pod := &podList.Items[0]
if created != nil && time.Since(*created) >= time.Second*2 {
pod.Status.Conditions = []v1.PodCondition{
{
Type: v1.PodReady,
Status: v1.ConditionTrue,
},
}
}
return newResponse(http.StatusOK, pod)
case p == "/namespaces/default/pods" && m == http.MethodPost:
resources, err := c.Build(req.Body, false)
if err != nil {
t.Fatal(err)
}
now := time.Now()
created = &now
return newResponse(http.StatusOK, resources[0].Object)
default:
t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path)
return nil, nil
}
}),
}
var err error
c.Waiter, err = c.GetWaiter(LegacyStrategy)
require.NoError(t, err)
resources, err := c.Build(objBody(&podList), false)
require.NoError(t, err)
result, err := c.Create(
resources,
ClientCreateOptionServerSideApply(false, false))
require.NoError(t, err)
assert.Len(t, result.Created, 1, "expected 1 resource created, got %d", len(result.Created))
err = c.Wait(resources, time.Second*30)
require.NoError(t, err)
assert.GreaterOrEqual(t, time.Since(*created), time.Second*2, "expected to wait at least 2 seconds")
}
func TestClientWaitContextPreCancelledLegacy(t *testing.T) {
podList := newPodList("starfish")
ctx, cancel := context.WithCancel(t.Context())
cancel()
c := newTestClient(t)
c.WaitContext = ctx
c.Factory.(*cmdtesting.TestFactory).Client = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
p, m := req.URL.Path, req.Method
t.Logf("got request %s %s", p, m)
switch {
case p == "/api/v1/namespaces/default/pods/starfish" && m == http.MethodGet:
pod := &podList.Items[0]
return newResponse(http.StatusOK, pod)
case p == "/namespaces/default/pods" && m == http.MethodPost:
resources, err := c.Build(req.Body, false)
if err != nil {
t.Fatal(err)
}
return newResponse(http.StatusOK, resources[0].Object)
default:
t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path)
return nil, nil
}
}),
}
var err error
c.Waiter, err = c.GetWaiter(LegacyStrategy)
require.NoError(t, err)
resources, err := c.Build(objBody(&podList), false)
require.NoError(t, err)
result, err := c.Create(
resources,
ClientCreateOptionServerSideApply(false, false))
require.NoError(t, err)
assert.Len(t, result.Created, 1, "expected 1 resource created, got %d", len(result.Created))
err = c.Wait(resources, time.Second*30)
require.Error(t, err)
assert.Contains(t, err.Error(), "context canceled", "expected context canceled error, got: %v", err)
}
func TestClientWaitContextCancellationStatusWatcher(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
c := newTestClient(t)
c.WaitContext = ctx
podManifest := `
apiVersion: v1
kind: Pod
metadata:
name: test-pod
namespace: default
`
var err error
c.Waiter, err = c.GetWaiter(StatusWatcherStrategy)
require.NoError(t, err)
resources, err := c.Build(strings.NewReader(podManifest), false)
require.NoError(t, err)
cancel()
err = c.Wait(resources, time.Second*30)
require.Error(t, err)
assert.Contains(t, err.Error(), "context canceled", "expected context canceled error, got: %v", err)
}
func TestClientWaitWithJobsContextCancellationStatusWatcher(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
c := newTestClient(t)
c.WaitContext = ctx
jobManifest := `
apiVersion: batch/v1
kind: Job
metadata:
name: test-job
namespace: default
`
var err error
c.Waiter, err = c.GetWaiter(StatusWatcherStrategy)
require.NoError(t, err)
resources, err := c.Build(strings.NewReader(jobManifest), false)
require.NoError(t, err)
cancel()
err = c.WaitWithJobs(resources, time.Second*30)
require.Error(t, err)
assert.Contains(t, err.Error(), "context canceled", "expected context canceled error, got: %v", err)
}
func TestClientWaitForDeleteContextCancellationStatusWatcher(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
c := newTestClient(t)
c.WaitContext = ctx
podManifest := `
apiVersion: v1
kind: Pod
metadata:
name: test-pod
namespace: default
status:
conditions:
- type: Ready
status: "True"
phase: Running
`
var err error
c.Waiter, err = c.GetWaiter(StatusWatcherStrategy)
require.NoError(t, err)
resources, err := c.Build(strings.NewReader(podManifest), false)
require.NoError(t, err)
cancel()
err = c.WaitForDelete(resources, time.Second*30)
require.Error(t, err)
assert.Contains(t, err.Error(), "context canceled", "expected context canceled error, got: %v", err)
}

@ -36,6 +36,7 @@ import (
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/client-go/dynamic"
watchtools "k8s.io/client-go/tools/watch"
helmStatusReaders "helm.sh/helm/v4/internal/statusreaders"
)
@ -43,6 +44,7 @@ import (
type statusWaiter struct {
client dynamic.Interface
restMapper meta.RESTMapper
ctx context.Context
}
func alwaysReady(_ *unstructured.Unstructured) (*status.Result, error) {
@ -53,7 +55,7 @@ func alwaysReady(_ *unstructured.Unstructured) (*status.Result, error) {
}
func (w *statusWaiter) WatchUntilReady(resourceList ResourceList, timeout time.Duration) error {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
ctx, cancel := w.contextWithTimeout(timeout)
defer cancel()
slog.Debug("waiting for resources", "count", len(resourceList), "timeout", timeout)
sw := watcher.NewDefaultStatusWatcher(w.client, w.restMapper)
@ -74,7 +76,7 @@ func (w *statusWaiter) WatchUntilReady(resourceList ResourceList, timeout time.D
}
func (w *statusWaiter) Wait(resourceList ResourceList, timeout time.Duration) error {
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
ctx, cancel := w.contextWithTimeout(timeout)
defer cancel()
slog.Debug("waiting for resources", "count", len(resourceList), "timeout", timeout)
sw := watcher.NewDefaultStatusWatcher(w.client, w.restMapper)
@ -82,7 +84,7 @@ func (w *statusWaiter) Wait(resourceList ResourceList, timeout time.Duration) er
}
func (w *statusWaiter) WaitWithJobs(resourceList ResourceList, timeout time.Duration) error {
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
ctx, cancel := w.contextWithTimeout(timeout)
defer cancel()
slog.Debug("waiting for resources", "count", len(resourceList), "timeout", timeout)
sw := watcher.NewDefaultStatusWatcher(w.client, w.restMapper)
@ -93,7 +95,7 @@ func (w *statusWaiter) WaitWithJobs(resourceList ResourceList, timeout time.Dura
}
func (w *statusWaiter) WaitForDelete(resourceList ResourceList, timeout time.Duration) error {
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
ctx, cancel := w.contextWithTimeout(timeout)
defer cancel()
slog.Debug("waiting for resources to be deleted", "count", len(resourceList), "timeout", timeout)
sw := watcher.NewDefaultStatusWatcher(w.client, w.restMapper)
@ -179,6 +181,17 @@ func (w *statusWaiter) wait(ctx context.Context, resourceList ResourceList, sw w
return nil
}
func (w *statusWaiter) contextWithTimeout(timeout time.Duration) (context.Context, context.CancelFunc) {
return contextWithTimeout(w.ctx, timeout)
}
func contextWithTimeout(ctx context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
if ctx == nil {
ctx = context.Background()
}
return watchtools.ContextWithOptionalTimeout(ctx, timeout)
}
func statusObserver(cancel context.CancelFunc, desired status.Status) collector.ObserverFunc {
return func(statusCollector *collector.ResourceStatusCollector, _ event.Event) {
var rss []*event.ResourceStatus

@ -49,6 +49,7 @@ import (
type legacyWaiter struct {
c ReadyChecker
kubeClient *kubernetes.Clientset
ctx context.Context
}
func (hw *legacyWaiter) Wait(resources ResourceList, timeout time.Duration) error {
@ -66,7 +67,7 @@ func (hw *legacyWaiter) WaitWithJobs(resources ResourceList, timeout time.Durati
func (hw *legacyWaiter) waitForResources(created ResourceList, timeout time.Duration) error {
slog.Debug("beginning wait for resources", "count", len(created), "timeout", timeout)
ctx, cancel := context.WithTimeout(context.Background(), timeout)
ctx, cancel := hw.contextWithTimeout(timeout)
defer cancel()
numberOfErrors := make([]int, len(created))
@ -121,7 +122,7 @@ func (hw *legacyWaiter) WaitForDelete(deleted ResourceList, timeout time.Duratio
slog.Debug("beginning wait for resources to be deleted", "count", len(deleted), "timeout", timeout)
startTime := time.Now()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
ctx, cancel := hw.contextWithTimeout(timeout)
defer cancel()
err := wait.PollUntilContextCancel(ctx, 2*time.Second, true, func(_ context.Context) (bool, error) {
@ -246,7 +247,7 @@ func (hw *legacyWaiter) watchUntilReady(timeout time.Duration, info *resource.In
// In the future, we might want to add some special logic for types
// like Ingress, Volume, etc.
ctx, cancel := watchtools.ContextWithOptionalTimeout(context.Background(), timeout)
ctx, cancel := hw.contextWithTimeout(timeout)
defer cancel()
_, err = watchtools.UntilWithSync(ctx, lw, &unstructured.Unstructured{}, nil, func(e watch.Event) (bool, error) {
// Make sure the incoming object is versioned as we use unstructured
@ -327,3 +328,7 @@ func (hw *legacyWaiter) waitForPodSuccess(obj runtime.Object, name string) (bool
return false, nil
}
func (hw *legacyWaiter) contextWithTimeout(timeout time.Duration) (context.Context, context.CancelFunc) {
return contextWithTimeout(hw.ctx, timeout)
}

@ -109,9 +109,9 @@ func NewOCIPusher(ops ...Option) (Pusher, error) {
}
func (pusher *OCIPusher) newRegistryClient() (*registry.Client, error) {
if (pusher.opts.certFile != "" && pusher.opts.keyFile != "") || pusher.opts.caFile != "" || pusher.opts.insecureSkipTLSverify {
if (pusher.opts.certFile != "" && pusher.opts.keyFile != "") || pusher.opts.caFile != "" || pusher.opts.insecureSkipTLSVerify {
tlsConf, err := tlsutil.NewTLSConfig(
tlsutil.WithInsecureSkipVerify(pusher.opts.insecureSkipTLSverify),
tlsutil.WithInsecureSkipVerify(pusher.opts.insecureSkipTLSVerify),
tlsutil.WithCertKeyPairFiles(pusher.opts.certFile, pusher.opts.keyFile),
tlsutil.WithCAFile(pusher.opts.caFile),
)

@ -40,13 +40,13 @@ func TestNewOCIPusher(t *testing.T) {
cd := "../../testdata"
join := filepath.Join
ca, pub, priv := join(cd, "rootca.crt"), join(cd, "crt.pem"), join(cd, "key.pem")
insecureSkipTLSverify := false
insecureSkipTLSVerify := false
plainHTTP := false
// Test with options
p, err = NewOCIPusher(
WithTLSClientConfig(pub, priv, ca),
WithInsecureSkipTLSVerify(insecureSkipTLSverify),
WithInsecureSkipTLSVerify(insecureSkipTLSVerify),
WithPlainHTTP(plainHTTP),
)
if err != nil {
@ -74,8 +74,8 @@ func TestNewOCIPusher(t *testing.T) {
t.Errorf("Expected NewOCIPusher to have plainHTTP as %t, got %t", plainHTTP, op.opts.plainHTTP)
}
if op.opts.insecureSkipTLSverify != insecureSkipTLSverify {
t.Errorf("Expected NewOCIPusher to have insecureSkipVerifyTLS as %t, got %t", insecureSkipTLSverify, op.opts.insecureSkipTLSverify)
if op.opts.insecureSkipTLSVerify != insecureSkipTLSVerify {
t.Errorf("Expected NewOCIPusher to have insecureSkipVerifyTLS as %t, got %t", insecureSkipTLSVerify, op.opts.insecureSkipTLSVerify)
}
// Test if setting registryClient is being passed to the ops
@ -422,7 +422,7 @@ func TestOCIPusher_Push_MultipleOptions(t *testing.T) {
if !op.opts.plainHTTP {
t.Error("Expected plainHTTP option to be applied")
}
if !op.opts.insecureSkipTLSverify {
t.Error("Expected insecureSkipTLSverify option to be applied")
if !op.opts.insecureSkipTLSVerify {
t.Error("Expected insecureSkipTLSVerify option to be applied")
}
}

@ -32,7 +32,7 @@ type options struct {
certFile string
keyFile string
caFile string
insecureSkipTLSverify bool
insecureSkipTLSVerify bool
plainHTTP bool
}
@ -59,7 +59,7 @@ func WithTLSClientConfig(certFile, keyFile, caFile string) Option {
// WithInsecureSkipTLSVerify determines if a TLS Certificate will be checked
func WithInsecureSkipTLSVerify(insecureSkipTLSVerify bool) Option {
return func(opts *options) {
opts.insecureSkipTLSverify = insecureSkipTLSVerify
opts.insecureSkipTLSVerify = insecureSkipTLSVerify
}
}

@ -236,12 +236,12 @@ func TestGenerateOCICreatedAnnotations(t *testing.T) {
nowTime := time.Now()
nowTimeString := nowTime.Format(time.RFC3339)
chart := &chart.Metadata{
testChart := &chart.Metadata{
Name: "oci",
Version: "0.0.1",
}
result := generateOCIAnnotations(chart, nowTimeString)
result := generateOCIAnnotations(testChart, nowTimeString)
// Check that created annotation exists
if _, ok := result[ocispec.AnnotationCreated]; !ok {
@ -254,7 +254,7 @@ func TestGenerateOCICreatedAnnotations(t *testing.T) {
}
// Verify default creation time set
result = generateOCIAnnotations(chart, "")
result = generateOCIAnnotations(testChart, "")
// Check that created annotation exists
if _, ok := result[ocispec.AnnotationCreated]; !ok {

@ -75,11 +75,11 @@ type (
credentialsStore credentials.Store
httpClient *http.Client
plainHTTP bool
err error // pass any errors from the ClientOption functions
}
// ClientOption allows specifying various settings configurable by the user for overriding the defaults
// used when creating a new default client
// TODO(TerryHowe): ClientOption should return error in v5
ClientOption func(*Client)
)
@ -90,9 +90,6 @@ func NewClient(options ...ClientOption) (*Client, error) {
}
for _, option := range options {
option(client)
if client.err != nil {
return nil, client.err
}
}
if client.credentialsFile == "" {
client.credentialsFile = helmpath.ConfigPath(CredentialsFileBasename)
@ -258,7 +255,7 @@ func (c *Client) Login(host string, options ...LoginOption) error {
return err
}
fmt.Fprintln(c.out, "Login Succeeded")
_, _ = fmt.Fprintln(c.out, "Login Succeeded")
return nil
}
@ -386,7 +383,7 @@ func (c *Client) Logout(host string, opts ...LogoutOption) error {
if err := credentials.Logout(context.Background(), c.credentialsStore, host); err != nil {
return err
}
fmt.Fprintf(c.out, "Removing login credentials for %s\n", host)
_, _ = fmt.Fprintf(c.out, "Removing login credentials for %s\n", host)
return nil
}
@ -456,7 +453,7 @@ func (c *Client) processChartPull(genericResult *GenericPullResult, operation *p
provDescriptor = &d
case LegacyChartLayerMediaType:
chartDescriptor = &d
fmt.Fprintf(c.out, "Warning: chart media type %s is deprecated\n", LegacyChartLayerMediaType)
_, _ = fmt.Fprintf(c.out, "Warning: chart media type %s is deprecated\n", LegacyChartLayerMediaType)
}
}
@ -529,12 +526,12 @@ func (c *Client) processChartPull(genericResult *GenericPullResult, operation *p
result.Prov.Size = provDescriptor.Size
}
fmt.Fprintf(c.out, "Pulled: %s\n", result.Ref)
fmt.Fprintf(c.out, "Digest: %s\n", result.Manifest.Digest)
_, _ = fmt.Fprintf(c.out, "Pulled: %s\n", result.Ref)
_, _ = fmt.Fprintf(c.out, "Digest: %s\n", result.Manifest.Digest)
if strings.Contains(result.Ref, "_") {
fmt.Fprintf(c.out, "%s contains an underscore.\n", result.Ref)
fmt.Fprint(c.out, registryUnderscoreMessage+"\n")
_, _ = fmt.Fprintf(c.out, "%s contains an underscore.\n", result.Ref)
_, _ = fmt.Fprint(c.out, registryUnderscoreMessage+"\n")
}
return result, nil
@ -731,11 +728,11 @@ func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResu
Size: provDescriptor.Size,
}
}
fmt.Fprintf(c.out, "Pushed: %s\n", result.Ref)
fmt.Fprintf(c.out, "Digest: %s\n", result.Manifest.Digest)
_, _ = fmt.Fprintf(c.out, "Pushed: %s\n", result.Ref)
_, _ = fmt.Fprintf(c.out, "Digest: %s\n", result.Manifest.Digest)
if strings.Contains(parsedRef.orasReference.Reference, "_") {
fmt.Fprintf(c.out, "%s contains an underscore.\n", result.Ref)
fmt.Fprint(c.out, registryUnderscoreMessage+"\n")
_, _ = fmt.Fprintf(c.out, "%s contains an underscore.\n", result.Ref)
_, _ = fmt.Fprint(c.out, registryUnderscoreMessage+"\n")
}
return result, err

@ -27,17 +27,17 @@ import (
)
type HTTPRegistryClientTestSuite struct {
TestSuite
TestRegistry
}
func (suite *HTTPRegistryClientTestSuite) SetupSuite() {
// init test client
setup(&suite.TestSuite, false, false)
setup(&suite.TestRegistry, false, false)
}
func (suite *HTTPRegistryClientTestSuite) TearDownSuite() {
teardown(&suite.TestSuite)
os.RemoveAll(suite.WorkspaceDir)
teardown(&suite.TestRegistry)
_ = os.RemoveAll(suite.WorkspaceDir)
}
func (suite *HTTPRegistryClientTestSuite) Test_0_Login() {
@ -53,15 +53,15 @@ func (suite *HTTPRegistryClientTestSuite) Test_0_Login() {
}
func (suite *HTTPRegistryClientTestSuite) Test_1_Push() {
testPush(&suite.TestSuite)
testPush(&suite.TestRegistry)
}
func (suite *HTTPRegistryClientTestSuite) Test_2_Pull() {
testPull(&suite.TestSuite)
testPull(&suite.TestRegistry)
}
func (suite *HTTPRegistryClientTestSuite) Test_3_Tags() {
testTags(&suite.TestSuite)
testTags(&suite.TestRegistry)
}
func (suite *HTTPRegistryClientTestSuite) Test_4_ManInTheMiddle() {

@ -24,17 +24,17 @@ import (
)
type InsecureTLSRegistryClientTestSuite struct {
TestSuite
TestRegistry
}
func (suite *InsecureTLSRegistryClientTestSuite) SetupSuite() {
// init test client
setup(&suite.TestSuite, true, true)
setup(&suite.TestRegistry, true, true)
}
func (suite *InsecureTLSRegistryClientTestSuite) TearDownSuite() {
teardown(&suite.TestSuite)
os.RemoveAll(suite.WorkspaceDir)
teardown(&suite.TestRegistry)
_ = os.RemoveAll(suite.WorkspaceDir)
}
func (suite *InsecureTLSRegistryClientTestSuite) Test_0_Login() {
@ -50,15 +50,15 @@ func (suite *InsecureTLSRegistryClientTestSuite) Test_0_Login() {
}
func (suite *InsecureTLSRegistryClientTestSuite) Test_1_Push() {
testPush(&suite.TestSuite)
testPush(&suite.TestRegistry)
}
func (suite *InsecureTLSRegistryClientTestSuite) Test_2_Pull() {
testPull(&suite.TestSuite)
testPull(&suite.TestRegistry)
}
func (suite *InsecureTLSRegistryClientTestSuite) Test_3_Tags() {
testTags(&suite.TestSuite)
testTags(&suite.TestRegistry)
}
func (suite *InsecureTLSRegistryClientTestSuite) Test_4_Logout() {

@ -26,17 +26,17 @@ import (
)
type TLSRegistryClientTestSuite struct {
TestSuite
TestRegistry
}
func (suite *TLSRegistryClientTestSuite) SetupSuite() {
// init test client
setup(&suite.TestSuite, true, false)
setup(&suite.TestRegistry, true, false)
}
func (suite *TLSRegistryClientTestSuite) TearDownSuite() {
teardown(&suite.TestSuite)
os.RemoveAll(suite.WorkspaceDir)
teardown(&suite.TestRegistry)
_ = os.RemoveAll(suite.WorkspaceDir)
}
func (suite *TLSRegistryClientTestSuite) Test_0_Login() {
@ -76,15 +76,15 @@ func (suite *TLSRegistryClientTestSuite) Test_1_Login() {
}
func (suite *TLSRegistryClientTestSuite) Test_1_Push() {
testPush(&suite.TestSuite)
testPush(&suite.TestRegistry)
}
func (suite *TLSRegistryClientTestSuite) Test_2_Pull() {
testPull(&suite.TestSuite)
testPull(&suite.TestRegistry)
}
func (suite *TLSRegistryClientTestSuite) Test_3_Tags() {
testTags(&suite.TestSuite)
testTags(&suite.TestRegistry)
}
func (suite *TLSRegistryClientTestSuite) Test_4_Logout() {

@ -92,7 +92,7 @@ func (c *GenericClient) PullGeneric(ref string, options GenericPullOptions) (*Ge
memoryStore := memory.New()
var descriptors []ocispec.Descriptor
// Set up repository with authentication and configuration
// Set up a repository with authentication and configuration
repository, err := remote.NewRepository(parsedRef.String())
if err != nil {
return nil, err
@ -114,7 +114,7 @@ func (c *GenericClient) PullGeneric(ref string, options GenericPullOptions) (*Ge
manifest, err := oras.Copy(ctx, repository, parsedRef.String(), memoryStore, "", oras.CopyOptions{
CopyGraphOptions: oras.CopyGraphOptions{
PreCopy: func(ctx context.Context, desc ocispec.Descriptor) error {
// Apply custom PreCopy function if provided
// Apply a custom PreCopy function if provided
if options.PreCopy != nil {
if err := options.PreCopy(ctx, desc); err != nil {
return err

@ -147,15 +147,15 @@ func (c *Client) processPluginPull(genericResult *GenericPullResult, pluginName
}
}
fmt.Fprintf(c.out, "Pulled plugin: %s\n", result.Ref)
fmt.Fprintf(c.out, "Digest: %s\n", result.Manifest.Digest)
_, _ = fmt.Fprintf(c.out, "Pulled plugin: %s\n", result.Ref)
_, _ = fmt.Fprintf(c.out, "Digest: %s\n", result.Manifest.Digest)
if result.Prov.Data != nil {
fmt.Fprintf(c.out, "Provenance: %s\n", foundProvenanceName)
_, _ = fmt.Fprintf(c.out, "Provenance: %s\n", foundProvenanceName)
}
if strings.Contains(result.Ref, "_") {
fmt.Fprintf(c.out, "%s contains an underscore.\n", result.Ref)
fmt.Fprint(c.out, registryUnderscoreMessage+"\n")
_, _ = fmt.Fprintf(c.out, "%s contains an underscore.\n", result.Ref)
_, _ = fmt.Fprint(c.out, registryUnderscoreMessage+"\n")
}
return result, nil

@ -32,11 +32,11 @@ type reference struct {
}
// newReference will parse and validate the reference, and clean tags when
// applicable tags are only cleaned when plus (+) signs are present, and are
// applicable tags are only cleaned when plus (+) signs are present and are
// converted to underscores (_) before pushing
// See https://github.com/helm/helm/issues/10166
func newReference(raw string) (result reference, err error) {
// Remove oci:// prefix if it is there
// Remove the oci:// prefix if it is there
raw = strings.TrimPrefix(raw, OCIScheme+"://")
// The sole possible reference modification is replacing plus (+) signs

@ -35,6 +35,7 @@ import (
"github.com/distribution/distribution/v3/registry"
_ "github.com/distribution/distribution/v3/registry/auth/htpasswd"
_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"golang.org/x/crypto/bcrypt"
@ -56,7 +57,7 @@ var (
testPassword = "mypass"
)
type TestSuite struct {
type TestRegistry struct {
suite.Suite
Out io.Writer
DockerRegistryHost string
@ -66,15 +67,15 @@ type TestSuite struct {
dockerRegistry *registry.Registry
}
func setup(suite *TestSuite, tlsEnabled, insecure bool) {
func setup(suite *TestRegistry, tlsEnabled, insecure bool) {
suite.WorkspaceDir = testWorkspaceDir
os.RemoveAll(suite.WorkspaceDir)
os.Mkdir(suite.WorkspaceDir, 0700)
err := os.RemoveAll(suite.WorkspaceDir)
require.NoError(suite.T(), err, "no error removing test workspace dir")
err = os.Mkdir(suite.WorkspaceDir, 0700)
require.NoError(suite.T(), err, "no error creating test workspace dir")
var out bytes.Buffer
var (
out bytes.Buffer
err error
)
suite.Out = &out
credentialsFile := filepath.Join(suite.WorkspaceDir, CredentialsFileBasename)
@ -124,7 +125,7 @@ func setup(suite *TestSuite, tlsEnabled, insecure bool) {
config := &configuration.Configuration{}
ln, err := net.Listen("tcp", "127.0.0.1:0")
suite.Nil(err, "no error finding free port for test registry")
defer ln.Close()
defer func() { _ = ln.Close() }()
// Change the registry host to another host which is not localhost.
// This is required because Docker enforces HTTP if the registry
@ -164,7 +165,7 @@ func setup(suite *TestSuite, tlsEnabled, insecure bool) {
}()
}
func teardown(suite *TestSuite) {
func teardown(suite *TestRegistry) {
if suite.dockerRegistry != nil {
_ = suite.dockerRegistry.Shutdown(context.Background())
}
@ -176,7 +177,7 @@ func initCompromisedRegistryTestServer() string {
w.Header().Set("Content-Type", "application/vnd.oci.image.manifest.v1+json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `{ "schemaVersion": 2, "config": {
_, _ = fmt.Fprintf(w, `{ "schemaVersion": 2, "config": {
"mediaType": "%s",
"digest": "sha256:a705ee2789ab50a5ba20930f246dbd5cc01ff9712825bb98f57ee8414377f133",
"size": 181
@ -192,13 +193,13 @@ func initCompromisedRegistryTestServer() string {
} else if r.URL.Path == "/v2/testrepo/supposedlysafechart/blobs/sha256:a705ee2789ab50a5ba20930f246dbd5cc01ff9712825bb98f57ee8414377f133" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte("{\"name\":\"mychart\",\"version\":\"0.1.0\",\"description\":\"A Helm chart for Kubernetes\\n" +
_, _ = w.Write([]byte("{\"name\":\"mychart\",\"version\":\"0.1.0\",\"description\":\"A Helm chart for Kubernetes\\n" +
"an 'application' or a 'library' chart.\",\"apiVersion\":\"v2\",\"appVersion\":\"1.16.0\",\"type\":" +
"\"application\"}"))
} else if r.URL.Path == "/v2/testrepo/supposedlysafechart/blobs/sha256:ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb" {
w.Header().Set("Content-Type", ChartLayerMediaType)
w.WriteHeader(http.StatusOK)
w.Write([]byte("b"))
_, _ = w.Write([]byte("b"))
} else {
w.WriteHeader(http.StatusInternalServerError)
}
@ -208,7 +209,7 @@ func initCompromisedRegistryTestServer() string {
return fmt.Sprintf("localhost:%s", u.Port())
}
func testPush(suite *TestSuite) {
func testPush(suite *TestRegistry) {
testingChartCreationTime := "1977-09-02T22:04:05Z"
@ -295,7 +296,7 @@ func testPush(suite *TestSuite) {
result.Prov.Digest)
}
func testPull(suite *TestSuite) {
func testPull(suite *TestRegistry) {
// bad/missing ref
ref := fmt.Sprintf("%s/testrepo/no-existy:1.2.3", suite.DockerRegistryHost)
_, err := suite.RegistryClient.Pull(ref)
@ -374,7 +375,7 @@ func testPull(suite *TestSuite) {
suite.Equal(provData, result.Prov.Data)
}
func testTags(suite *TestSuite) {
func testTags(suite *TestRegistry) {
// Load test chart (to build ref pushed in previous test)
chartData, err := os.ReadFile("../downloader/testdata/local-subchart-0.1.0.tgz")
suite.Nil(err, "no error loading test chart")

@ -25,10 +25,10 @@ import (
func GetTagMatchingVersionOrConstraint(tags []string, versionString string) (string, error) {
var constraint *semver.Constraints
if versionString == "" {
// If string is empty, set wildcard constraint
// If the string is empty, set a wildcard constraint
constraint, _ = semver.NewConstraint("*")
} else {
// when customer inputs specific version, check whether there's an exact match first
// when customer inputs a specific version, check whether there's an exact match first
for _, v := range tags {
if versionString == v {
return v, nil

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save