fix(logging): replace global slog usage with dependency-injected loggers

Signed-off-by: JaeguKim <rlaworn1993@gmail.com>
pull/31833/head
JaeguKim 8 months ago
parent 609e1ca72c
commit 5d84ee32ce

@ -88,7 +88,7 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string
// lint ignores import-values // lint ignores import-values
// See https://github.com/helm/helm/issues/9658 // See https://github.com/helm/helm/issues/9658
if err := chartutil.ProcessDependencies(chart, values); err != nil { if err := chartutil.ProcessDependencies(chart, values, nil); err != nil {
return return
} }

@ -78,7 +78,7 @@ func validateValuesFile(valuesPath string, overrides map[string]any, skipSchemaV
} }
if !skipSchemaValidation { if !skipSchemaValidation {
return util.ValidateAgainstSingleSchema(coalescedValues, schema) return util.ValidateAgainstSingleSchema(coalescedValues, schema, nil)
} }
return nil return nil

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

@ -30,14 +30,15 @@ import (
// ProcessDependencies checks through this chart's dependencies, processing accordingly. // ProcessDependencies checks through this chart's dependencies, processing accordingly.
func ProcessDependencies(c *chart.Chart, v common.Values) error { func ProcessDependencies(c *chart.Chart, v common.Values) error {
if err := processDependencyEnabled(c, v, ""); err != nil { logger := slog.Default()
if err := processDependencyEnabled(c, v, "", logger); err != nil {
return err return err
} }
return processDependencyImportValues(c, true) return processDependencyImportValues(c, true, logger)
} }
// processDependencyConditions disables charts based on condition path value in values // processDependencyConditions disables charts based on condition path value in values
func processDependencyConditions(reqs []*chart.Dependency, cvals common.Values, cpath string) { func processDependencyConditions(reqs []*chart.Dependency, cvals common.Values, cpath string, logger *slog.Logger) {
if reqs == nil { if reqs == nil {
return return
} }
@ -53,10 +54,10 @@ func processDependencyConditions(reqs []*chart.Dependency, cvals common.Values,
r.Enabled = bv r.Enabled = bv
break break
} }
slog.Warn("returned non-bool value", "path", c, "chart", r.Name) logger.Warn("returned non-bool value", "path", c, "chart", r.Name)
} else if errors.As(err, &errNoValue) { } else if errors.As(err, &errNoValue) {
// this is a real error // this is a real error
slog.Warn("the method PathValue returned error", slog.Any("error", err)) logger.Warn("the method PathValue returned error", slog.Any("error", err))
} }
} }
} }
@ -64,7 +65,7 @@ func processDependencyConditions(reqs []*chart.Dependency, cvals common.Values,
} }
// processDependencyTags disables charts based on tags in values // processDependencyTags disables charts based on tags in values
func processDependencyTags(reqs []*chart.Dependency, cvals common.Values) { func processDependencyTags(reqs []*chart.Dependency, cvals common.Values, logger *slog.Logger) {
if reqs == nil { if reqs == nil {
return return
} }
@ -84,7 +85,7 @@ func processDependencyTags(reqs []*chart.Dependency, cvals common.Values) {
hasFalse = true hasFalse = true
} }
} else { } else {
slog.Warn("returned non-bool value", "tag", k, "chart", r.Name) logger.Warn("returned non-bool value", "tag", k, "chart", r.Name)
} }
} }
} }
@ -143,7 +144,7 @@ func copyMetadata(metadata *chart.Metadata) *chart.Metadata {
} }
// processDependencyEnabled removes disabled charts from dependencies // processDependencyEnabled removes disabled charts from dependencies
func processDependencyEnabled(c *chart.Chart, v map[string]any, path string) error { func processDependencyEnabled(c *chart.Chart, v map[string]any, path string, logger *slog.Logger) error {
if c.Metadata.Dependencies == nil { if c.Metadata.Dependencies == nil {
return nil return nil
} }
@ -186,8 +187,8 @@ Loop:
return err return err
} }
// flag dependencies as enabled/disabled // flag dependencies as enabled/disabled
processDependencyTags(c.Metadata.Dependencies, cvals) processDependencyTags(c.Metadata.Dependencies, cvals, logger)
processDependencyConditions(c.Metadata.Dependencies, cvals, path) processDependencyConditions(c.Metadata.Dependencies, cvals, path, logger)
// make a map of charts to remove // make a map of charts to remove
rm := map[string]struct{}{} rm := map[string]struct{}{}
for _, r := range c.Metadata.Dependencies { for _, r := range c.Metadata.Dependencies {
@ -216,7 +217,7 @@ Loop:
// recursively call self to process sub dependencies // recursively call self to process sub dependencies
for _, t := range cd { for _, t := range cd {
subpath := path + t.Metadata.Name + "." subpath := path + t.Metadata.Name + "."
if err := processDependencyEnabled(t, cvals, subpath); err != nil { if err := processDependencyEnabled(t, cvals, subpath, logger); err != nil {
return err return err
} }
} }
@ -250,7 +251,7 @@ func set(path []string, data map[string]any) map[string]any {
} }
// processImportValues merges values from child to parent based on the chart's dependencies' ImportValues field. // processImportValues merges values from child to parent based on the chart's dependencies' ImportValues field.
func processImportValues(c *chart.Chart, merge bool) error { func processImportValues(c *chart.Chart, merge bool, logger *slog.Logger) error {
if c.Metadata.Dependencies == nil { if c.Metadata.Dependencies == nil {
return nil return nil
} }
@ -283,7 +284,7 @@ func processImportValues(c *chart.Chart, merge bool) error {
// get child table // get child table
vv, err := cvals.Table(r.Name + "." + child) vv, err := cvals.Table(r.Name + "." + child)
if err != nil { if err != nil {
slog.Warn( logger.Warn(
"ImportValues missing table from chart", "ImportValues missing table from chart",
slog.String("chart", "chart"), slog.String("chart", "chart"),
slog.String("name", r.Name), slog.String("name", r.Name),
@ -305,7 +306,7 @@ func processImportValues(c *chart.Chart, merge bool) error {
}) })
vm, err := cvals.Table(r.Name + "." + child) vm, err := cvals.Table(r.Name + "." + child)
if err != nil { if err != nil {
slog.Warn("ImportValues missing table", slog.Any("error", err)) logger.Warn("ImportValues missing table", slog.Any("error", err))
continue continue
} }
if merge { if merge {
@ -373,12 +374,12 @@ func istable(v any) bool {
} }
// processDependencyImportValues imports specified chart values from child to parent. // processDependencyImportValues imports specified chart values from child to parent.
func processDependencyImportValues(c *chart.Chart, merge bool) error { func processDependencyImportValues(c *chart.Chart, merge bool, logger *slog.Logger) error {
for _, d := range c.Dependencies() { for _, d := range c.Dependencies() {
// recurse // recurse
if err := processDependencyImportValues(d, merge); err != nil { if err := processDependencyImportValues(d, merge, logger); err != nil {
return err return err
} }
} }
return processImportValues(c, merge) return processImportValues(c, merge, logger)
} }

@ -15,6 +15,7 @@ limitations under the License.
package util package util
import ( import (
"log/slog"
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
@ -117,7 +118,7 @@ func TestDependencyEnabled(t *testing.T) {
for _, tc := range tests { for _, tc := range tests {
c := loadChart(t, "testdata/subpop") c := loadChart(t, "testdata/subpop")
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
if err := processDependencyEnabled(c, tc.v, ""); err != nil { if err := processDependencyEnabled(c, tc.v, "", slog.New(slog.DiscardHandler)); err != nil {
t.Fatalf("error processing enabled dependencies %v", err) t.Fatalf("error processing enabled dependencies %v", err)
} }
@ -219,7 +220,7 @@ func TestProcessDependencyImportValues(t *testing.T) {
e["SCBexported2A"] = "blaster" e["SCBexported2A"] = "blaster"
e["global.SC1exported2.all.SC1exported3"] = "SC1expstr" e["global.SC1exported2.all.SC1exported3"] = "SC1expstr"
if err := processDependencyImportValues(c, false); err != nil { if err := processDependencyImportValues(c, false, slog.New(slog.DiscardHandler)); err != nil {
t.Fatalf("processing import values dependencies %v", err) t.Fatalf("processing import values dependencies %v", err)
} }
cc := common.Values(c.Values) cc := common.Values(c.Values)
@ -259,7 +260,7 @@ func TestProcessDependencyImportValues(t *testing.T) {
} }
c = loadChart(t, "testdata/subpop") c = loadChart(t, "testdata/subpop")
if err := processDependencyImportValues(c, true); err != nil { if err := processDependencyImportValues(c, true, slog.New(slog.DiscardHandler)); err != nil {
t.Fatalf("processing import values dependencies %v", err) t.Fatalf("processing import values dependencies %v", err)
} }
cc = common.Values(c.Values) cc = common.Values(c.Values)
@ -275,10 +276,10 @@ func TestProcessDependencyImportValues(t *testing.T) {
func TestProcessDependencyImportValuesFromSharedDependencyToAliases(t *testing.T) { func TestProcessDependencyImportValuesFromSharedDependencyToAliases(t *testing.T) {
c := loadChart(t, "testdata/chart-with-import-from-aliased-dependencies") c := loadChart(t, "testdata/chart-with-import-from-aliased-dependencies")
if err := processDependencyEnabled(c, c.Values, ""); err != nil { if err := processDependencyEnabled(c, c.Values, "", slog.New(slog.DiscardHandler)); err != nil {
t.Fatalf("expected no errors but got %q", err) t.Fatalf("expected no errors but got %q", err)
} }
if err := processDependencyImportValues(c, true); err != nil { if err := processDependencyImportValues(c, true, slog.New(slog.DiscardHandler)); err != nil {
t.Fatalf("processing import values dependencies %v", err) t.Fatalf("processing import values dependencies %v", err)
} }
e := make(map[string]string) e := make(map[string]string)
@ -327,7 +328,7 @@ func TestProcessDependencyImportValuesMultiLevelPrecedence(t *testing.T) {
e["app2.service.port"] = "8080" e["app2.service.port"] = "8080"
e["app3.service.port"] = "9090" e["app3.service.port"] = "9090"
e["app4.service.port"] = "1234" e["app4.service.port"] = "1234"
if err := processDependencyImportValues(c, true); err != nil { if err := processDependencyImportValues(c, true, slog.New(slog.DiscardHandler)); err != nil {
t.Fatalf("processing import values dependencies %v", err) t.Fatalf("processing import values dependencies %v", err)
} }
cc := common.Values(c.Values) cc := common.Values(c.Values)
@ -354,7 +355,7 @@ func TestProcessDependencyImportValuesForEnabledCharts(t *testing.T) {
c := loadChart(t, "testdata/import-values-from-enabled-subchart/parent-chart") c := loadChart(t, "testdata/import-values-from-enabled-subchart/parent-chart")
nameOverride := "parent-chart-prod" nameOverride := "parent-chart-prod"
if err := processDependencyImportValues(c, true); err != nil { if err := processDependencyImportValues(c, true, slog.New(slog.DiscardHandler)); err != nil {
t.Fatalf("processing import values dependencies %v", err) t.Fatalf("processing import values dependencies %v", err)
} }
@ -362,7 +363,7 @@ func TestProcessDependencyImportValuesForEnabledCharts(t *testing.T) {
t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies())) t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies()))
} }
if err := processDependencyEnabled(c, c.Values, ""); err != nil { if err := processDependencyEnabled(c, c.Values, "", slog.New(slog.DiscardHandler)); err != nil {
t.Fatalf("expected no errors but got %q", err) t.Fatalf("expected no errors but got %q", err)
} }
@ -427,7 +428,7 @@ func TestDependentChartAliases(t *testing.T) {
t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies())) t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies()))
} }
if err := processDependencyEnabled(c, c.Values, ""); err != nil { if err := processDependencyEnabled(c, c.Values, "", slog.New(slog.DiscardHandler)); err != nil {
t.Fatalf("expected no errors but got %q", err) t.Fatalf("expected no errors but got %q", err)
} }
@ -469,7 +470,7 @@ func TestDependentChartWithSubChartsAbsentInDependency(t *testing.T) {
t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies())) t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies()))
} }
if err := processDependencyEnabled(c, c.Values, ""); err != nil { if err := processDependencyEnabled(c, c.Values, "", slog.New(slog.DiscardHandler)); err != nil {
t.Fatalf("expected no errors but got %q", err) t.Fatalf("expected no errors but got %q", err)
} }
@ -506,7 +507,7 @@ func TestDependentChartsWithSubchartsAllSpecifiedInDependency(t *testing.T) {
t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies())) t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies()))
} }
if err := processDependencyEnabled(c, c.Values, ""); err != nil { if err := processDependencyEnabled(c, c.Values, "", slog.New(slog.DiscardHandler)); err != nil {
t.Fatalf("expected no errors but got %q", err) t.Fatalf("expected no errors but got %q", err)
} }
@ -526,7 +527,7 @@ func TestDependentChartsWithSomeSubchartsSpecifiedInDependency(t *testing.T) {
t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies())) t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies()))
} }
if err := processDependencyEnabled(c, c.Values, ""); err != nil { if err := processDependencyEnabled(c, c.Values, "", slog.New(slog.DiscardHandler)); err != nil {
t.Fatalf("expected no errors but got %q", err) t.Fatalf("expected no errors but got %q", err)
} }
@ -559,7 +560,7 @@ func TestChartWithDependencyAliasedTwiceAndDoublyReferencedSubDependency(t *test
t.Fatalf("expected one dependency for this chart, but got %d", len(c.Dependencies())) t.Fatalf("expected one dependency for this chart, but got %d", len(c.Dependencies()))
} }
if err := processDependencyEnabled(c, c.Values, ""); err != nil { if err := processDependencyEnabled(c, c.Values, "", slog.New(slog.DiscardHandler)); err != nil {
t.Fatalf("expected no errors but got %q", err) t.Fatalf("expected no errors but got %q", err)
} }

@ -42,6 +42,14 @@ type HTTPInstaller struct {
// Cached data to avoid duplicate downloads // Cached data to avoid duplicate downloads
pluginData []byte pluginData []byte
provData []byte provData []byte
logger *slog.Logger
}
func (i *HTTPInstaller) log() *slog.Logger {
if i.logger != nil {
return i.logger
}
return slog.New(slog.DiscardHandler)
} }
// NewHTTPInstaller creates a new HttpInstaller. // NewHTTPInstaller creates a new HttpInstaller.
@ -113,7 +121,7 @@ func (i *HTTPInstaller) Install() error {
if i.provData != nil { if i.provData != nil {
provPath := tarballPath + ".prov" provPath := tarballPath + ".prov"
if err := os.WriteFile(provPath, i.provData, 0644); err != nil { if err := os.WriteFile(provPath, i.provData, 0644); err != nil {
slog.Debug("failed to save provenance file", "error", err) i.log().Debug("failed to save provenance file", "error", err)
} }
} }
@ -137,7 +145,7 @@ func (i *HTTPInstaller) Install() error {
return err return err
} }
slog.Debug("copying", "source", src, "path", i.Path()) i.log().Debug("copying", "source", src, "path", i.Path())
return fs.CopyDir(src, i.Path()) return fs.CopyDir(src, i.Path())
} }

@ -31,6 +31,17 @@ import (
// ErrMissingMetadata indicates that plugin.yaml is missing. // ErrMissingMetadata indicates that plugin.yaml is missing.
var ErrMissingMetadata = errors.New("plugin metadata (plugin.yaml) missing") var ErrMissingMetadata = errors.New("plugin metadata (plugin.yaml) missing")
// installerLogger extracts a *slog.Logger from an Installer if it provides one.
func installerLogger(i Installer) *slog.Logger {
type logProvider interface {
log() *slog.Logger
}
if lp, ok := i.(logProvider); ok {
return lp.log()
}
return slog.New(slog.DiscardHandler)
}
// Options contains options for plugin installation. // Options contains options for plugin installation.
type Options struct { type Options struct {
// Verify enables signature verification before installation // Verify enables signature verification before installation
@ -77,7 +88,7 @@ func InstallWithOptions(i Installer, opts Options) (*VerificationResult, error)
return nil, err return nil, err
} }
if _, pathErr := os.Stat(i.Path()); !os.IsNotExist(pathErr) { if _, pathErr := os.Stat(i.Path()); !os.IsNotExist(pathErr) {
slog.Warn("plugin already exists", slog.String("path", i.Path()), slog.Any("error", pathErr)) installerLogger(i).Warn("plugin already exists", slog.String("path", i.Path()), slog.Any("error", pathErr))
return nil, errors.New("plugin already exists") return nil, errors.New("plugin already exists")
} }
@ -128,7 +139,7 @@ func InstallWithOptions(i Installer, opts Options) (*VerificationResult, error)
// Update updates a plugin. // Update updates a plugin.
func Update(i Installer) error { func Update(i Installer) error {
if _, pathErr := os.Stat(i.Path()); os.IsNotExist(pathErr) { if _, pathErr := os.Stat(i.Path()); os.IsNotExist(pathErr) {
slog.Warn("plugin does not exist", slog.String("path", i.Path()), slog.Any("error", pathErr)) installerLogger(i).Warn("plugin does not exist", slog.String("path", i.Path()), slog.Any("error", pathErr))
return errors.New("plugin does not exist") return errors.New("plugin does not exist")
} }
return i.Update() return i.Update()
@ -159,7 +170,7 @@ func NewForSource(source, version string) (installer Installer, err error) {
func FindSource(location string) (Installer, error) { func FindSource(location string) (Installer, error) {
installer, err := existingVCSRepo(location) installer, err := existingVCSRepo(location)
if err != nil && err.Error() == "Cannot detect VCS" { if err != nil && err.Error() == "Cannot detect VCS" {
slog.Warn( installerLogger(installer).Warn(
"cannot get information about plugin source", "cannot get information about plugin source",
slog.String("location", location), slog.String("location", location),
slog.Any("error", err), slog.Any("error", err),

@ -39,6 +39,14 @@ type LocalInstaller struct {
extractor Extractor extractor Extractor
pluginData []byte // Cached plugin data pluginData []byte // Cached plugin data
provData []byte // Cached provenance data provData []byte // Cached provenance data
logger *slog.Logger
}
func (i *LocalInstaller) log() *slog.Logger {
if i.logger != nil {
return i.logger
}
return slog.New(slog.DiscardHandler)
} }
// NewLocalInstaller creates a new LocalInstaller. // NewLocalInstaller creates a new LocalInstaller.
@ -97,7 +105,7 @@ func (i *LocalInstaller) installFromDirectory() error {
if !isPlugin(i.Source) { if !isPlugin(i.Source) {
return ErrMissingMetadata return ErrMissingMetadata
} }
slog.Debug("symlinking", "source", i.Source, "path", i.Path()) i.log().Debug("symlinking", "source", i.Source, "path", i.Path())
return os.Symlink(i.Source, i.Path()) return os.Symlink(i.Source, i.Path())
} }
@ -129,7 +137,7 @@ func (i *LocalInstaller) installFromArchive() error {
if provData, err := os.ReadFile(provSource); err == nil { if provData, err := os.ReadFile(provSource); err == nil {
provPath := tarballPath + ".prov" provPath := tarballPath + ".prov"
if err := os.WriteFile(provPath, provData, 0644); err != nil { if err := os.WriteFile(provPath, provData, 0644); err != nil {
slog.Debug("failed to save provenance file", "error", err) i.log().Debug("failed to save provenance file", "error", err)
} }
} }
@ -154,13 +162,13 @@ func (i *LocalInstaller) installFromArchive() error {
} }
// Copy to the final destination // Copy to the final destination
slog.Debug("copying", "source", pluginDir, "path", i.Path()) i.log().Debug("copying", "source", pluginDir, "path", i.Path())
return fs.CopyDir(pluginDir, i.Path()) return fs.CopyDir(pluginDir, i.Path())
} }
// Update updates a local repository // Update updates a local repository
func (i *LocalInstaller) Update() error { func (i *LocalInstaller) Update() error {
slog.Debug("local repository is auto-updated") i.log().Debug("local repository is auto-updated")
return nil return nil
} }

@ -48,6 +48,14 @@ type OCIInstaller struct {
// Cached data to avoid duplicate downloads // Cached data to avoid duplicate downloads
pluginData []byte pluginData []byte
provData []byte provData []byte
logger *slog.Logger
}
func (i *OCIInstaller) log() *slog.Logger {
if i.logger != nil {
return i.logger
}
return slog.New(slog.DiscardHandler)
} }
// NewOCIInstaller creates a new OCIInstaller with optional getter options // NewOCIInstaller creates a new OCIInstaller with optional getter options
@ -85,7 +93,7 @@ func NewOCIInstaller(source string, options ...getter.Option) (*OCIInstaller, er
// Install downloads and installs a plugin from OCI registry // Install downloads and installs a plugin from OCI registry
// Implements Installer. // Implements Installer.
func (i *OCIInstaller) Install() error { func (i *OCIInstaller) Install() error {
slog.Debug("pulling OCI plugin", "source", i.Source) i.log().Debug("pulling OCI plugin", "source", i.Source)
// Ensure plugin data is cached // Ensure plugin data is cached
if i.pluginData == nil { if i.pluginData == nil {
@ -124,7 +132,7 @@ func (i *OCIInstaller) Install() error {
if i.provData != nil { if i.provData != nil {
provPath := tarballPath + ".prov" provPath := tarballPath + ".prov"
if err := os.WriteFile(provPath, i.provData, 0644); err != nil { if err := os.WriteFile(provPath, i.provData, 0644); err != nil {
slog.Debug("failed to save provenance file", "error", err) i.log().Debug("failed to save provenance file", "error", err)
} }
} }
@ -177,7 +185,7 @@ func (i *OCIInstaller) Install() error {
return err return err
} }
slog.Debug("copying", "source", src, "path", i.Path()) i.log().Debug("copying", "source", src, "path", i.Path())
return fs.CopyDir(src, i.Path()) return fs.CopyDir(src, i.Path())
} }
@ -264,7 +272,7 @@ func (i *OCIInstaller) SupportsVerification() bool {
// GetVerificationData downloads and caches plugin and provenance data from OCI registry for verification // GetVerificationData downloads and caches plugin and provenance data from OCI registry for verification
func (i *OCIInstaller) GetVerificationData() (archiveData, provData []byte, filename string, err error) { func (i *OCIInstaller) GetVerificationData() (archiveData, provData []byte, filename string, err error) {
slog.Debug("getting verification data for OCI plugin", "source", i.Source) i.log().Debug("getting verification data for OCI plugin", "source", i.Source)
// Download plugin data once and cache it // Download plugin data once and cache it
if i.pluginData == nil { if i.pluginData == nil {
@ -297,6 +305,6 @@ func (i *OCIInstaller) GetVerificationData() (archiveData, provData []byte, file
} }
filename = fmt.Sprintf("%s-%s.tgz", metadata.Name, metadata.Version) filename = fmt.Sprintf("%s-%s.tgz", metadata.Name, metadata.Version)
slog.Debug("got verification data for OCI plugin", "filename", filename) i.log().Debug("got verification data for OCI plugin", "filename", filename)
return i.pluginData, i.provData, filename, nil return i.pluginData, i.provData, filename, nil
} }

@ -36,6 +36,14 @@ type VCSInstaller struct {
Repo vcs.Repo Repo vcs.Repo
Version string Version string
base base
logger *slog.Logger
}
func (i *VCSInstaller) log() *slog.Logger {
if i.logger != nil {
return i.logger
}
return slog.New(slog.DiscardHandler)
} }
func existingVCSRepo(location string) (Installer, error) { func existingVCSRepo(location string) (Installer, error) {
@ -91,13 +99,13 @@ func (i *VCSInstaller) Install() error {
return ErrMissingMetadata return ErrMissingMetadata
} }
slog.Debug("copying files", "source", i.Repo.LocalPath(), "destination", i.Path()) i.log().Debug("copying files", "source", i.Repo.LocalPath(), "destination", i.Path())
return fs.CopyDir(i.Repo.LocalPath(), i.Path()) return fs.CopyDir(i.Repo.LocalPath(), i.Path())
} }
// Update updates a remote repository // Update updates a remote repository
func (i *VCSInstaller) Update() error { func (i *VCSInstaller) Update() error {
slog.Debug("updating", "source", i.Repo.Remote()) i.log().Debug("updating", "source", i.Repo.Remote())
if i.Repo.IsDirty() { if i.Repo.IsDirty() {
return errors.New("plugin repo was modified") return errors.New("plugin repo was modified")
} }
@ -131,7 +139,7 @@ func (i *VCSInstaller) solveVersion(repo vcs.Repo) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
slog.Debug("found refs", "refs", refs) i.log().Debug("found refs", "refs", refs)
// Convert and filter the list to semver.Version instances // Convert and filter the list to semver.Version instances
semvers := getSemVers(refs) semvers := getSemVers(refs)
@ -142,7 +150,7 @@ func (i *VCSInstaller) solveVersion(repo vcs.Repo) (string, error) {
if constraint.Check(v) { if constraint.Check(v) {
// If the constraint passes get the original reference // If the constraint passes get the original reference
ver := v.Original() ver := v.Original()
slog.Debug("setting to version", "version", ver) i.log().Debug("setting to version", "version", ver)
return ver, nil return ver, nil
} }
} }
@ -152,17 +160,17 @@ func (i *VCSInstaller) solveVersion(repo vcs.Repo) (string, error) {
// setVersion attempts to checkout the version // setVersion attempts to checkout the version
func (i *VCSInstaller) setVersion(repo vcs.Repo, ref string) error { func (i *VCSInstaller) setVersion(repo vcs.Repo, ref string) error {
slog.Debug("setting version", "version", i.Version) i.log().Debug("setting version", "version", i.Version)
return repo.UpdateVersion(ref) return repo.UpdateVersion(ref)
} }
// sync will clone or update a remote repo. // sync will clone or update a remote repo.
func (i *VCSInstaller) sync(repo vcs.Repo) error { func (i *VCSInstaller) sync(repo vcs.Repo) error {
if _, err := os.Stat(repo.LocalPath()); errors.Is(err, stdfs.ErrNotExist) { if _, err := os.Stat(repo.LocalPath()); errors.Is(err, stdfs.ErrNotExist) {
slog.Debug("cloning", "source", repo.Remote(), "destination", repo.LocalPath()) i.log().Debug("cloning", "source", repo.Remote(), "destination", repo.LocalPath())
return repo.Get() return repo.Get()
} }
slog.Debug("updating", "source", repo.Remote(), "destination", repo.LocalPath()) i.log().Debug("updating", "source", repo.Remote(), "destination", repo.LocalPath())
return repo.Update() return repo.Update()
} }

@ -126,6 +126,14 @@ type ExtismV1PluginRuntime struct {
dir string dir string
rc *RuntimeConfigExtismV1 rc *RuntimeConfigExtismV1
r *RuntimeExtismV1 r *RuntimeExtismV1
logger *slog.Logger
}
func (p *ExtismV1PluginRuntime) log() *slog.Logger {
if p.logger != nil {
return p.logger
}
return slog.New(slog.DiscardHandler)
} }
var _ Plugin = (*ExtismV1PluginRuntime)(nil) var _ Plugin = (*ExtismV1PluginRuntime)(nil)
@ -143,13 +151,13 @@ func (p *ExtismV1PluginRuntime) Invoke(ctx context.Context, input *Input) (*Outp
var tmpDir string var tmpDir string
if p.rc.FileSystem.CreateTempDir { if p.rc.FileSystem.CreateTempDir {
tmpDirInner, err := os.MkdirTemp(os.TempDir(), "helm-plugin-*") tmpDirInner, err := os.MkdirTemp(os.TempDir(), "helm-plugin-*")
slog.Debug("created plugin temp dir", slog.String("dir", tmpDirInner), slog.String("plugin", p.metadata.Name)) p.log().Debug("created plugin temp dir", slog.String("dir", tmpDirInner), slog.String("plugin", p.metadata.Name))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create temp dir for extism compilation cache: %w", err) return nil, fmt.Errorf("failed to create temp dir for extism compilation cache: %w", err)
} }
defer func() { defer func() {
if err := os.RemoveAll(tmpDir); err != nil { if err := os.RemoveAll(tmpDir); err != nil {
slog.Warn("failed to remove plugin temp dir", slog.String("dir", tmpDir), slog.String("plugin", p.metadata.Name), slog.String("error", err.Error())) p.log().Warn("failed to remove plugin temp dir", slog.String("dir", tmpDir), slog.String("plugin", p.metadata.Name), slog.String("error", err.Error()))
} }
}() }()
@ -174,7 +182,7 @@ func (p *ExtismV1PluginRuntime) Invoke(ctx context.Context, input *Input) (*Outp
} }
pe.SetLogger(func(logLevel extism.LogLevel, s string) { pe.SetLogger(func(logLevel extism.LogLevel, s string) {
slog.Debug(s, slog.String("level", logLevel.String()), slog.String("plugin", p.metadata.Name)) p.log().Debug(s, slog.String("level", logLevel.String()), slog.String("plugin", p.metadata.Name))
}) })
inputData, err := json.Marshal(input.Message) inputData, err := json.Marshal(input.Message)
@ -182,7 +190,7 @@ func (p *ExtismV1PluginRuntime) Invoke(ctx context.Context, input *Input) (*Outp
return nil, fmt.Errorf("failed to json marshal plugin input message: %T: %w", input.Message, err) return nil, fmt.Errorf("failed to json marshal plugin input message: %T: %w", input.Message, err)
} }
slog.Debug("plugin input", slog.String("plugin", p.metadata.Name), slog.String("inputData", string(inputData))) p.log().Debug("plugin input", slog.String("plugin", p.metadata.Name), slog.String("inputData", string(inputData)))
entryFuncName := p.rc.EntryFuncName entryFuncName := p.rc.EntryFuncName
if entryFuncName == "" { if entryFuncName == "" {
@ -200,7 +208,7 @@ func (p *ExtismV1PluginRuntime) Invoke(ctx context.Context, input *Input) (*Outp
} }
} }
slog.Debug("plugin output", slog.String("plugin", p.metadata.Name), slog.Int("exitCode", int(exitCode)), slog.String("outputData", string(outputData))) p.log().Debug("plugin output", slog.String("plugin", p.metadata.Name), slog.Int("exitCode", int(exitCode)), slog.String("outputData", string(outputData)))
outputMessage := reflect.New(pluginTypesIndex[p.metadata.Type].outputType) outputMessage := reflect.New(pluginTypesIndex[p.metadata.Type].outputType)
if err := json.Unmarshal(outputData, outputMessage.Interface()); err != nil { if err := json.Unmarshal(outputData, outputMessage.Interface()); err != nil {

@ -84,6 +84,14 @@ type SubprocessPluginRuntime struct {
pluginDir string pluginDir string
RuntimeConfig RuntimeConfigSubprocess RuntimeConfig RuntimeConfigSubprocess
EnvVars map[string]string EnvVars map[string]string
logger *slog.Logger
}
func (r *SubprocessPluginRuntime) log() *slog.Logger {
if r.logger != nil {
return r.logger
}
return slog.New(slog.DiscardHandler)
} }
var _ Plugin = (*SubprocessPluginRuntime)(nil) var _ Plugin = (*SubprocessPluginRuntime)(nil)
@ -125,7 +133,7 @@ func (r *SubprocessPluginRuntime) InvokeWithEnv(main string, argv []string, env
cmd.Stdout = stdout cmd.Stdout = stdout
cmd.Stderr = stderr cmd.Stderr = stderr
if err := executeCmd(cmd, r.metadata.Name); err != nil { if err := executeCmd(cmd, r.metadata.Name, r.logger); err != nil {
return err return err
} }
@ -154,7 +162,7 @@ func (r *SubprocessPluginRuntime) InvokeHook(event string) error {
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
slog.Debug("executing plugin hook command", slog.String("pluginName", r.metadata.Name), slog.String("command", cmd.String())) r.log().Debug("executing plugin hook command", slog.String("pluginName", r.metadata.Name), slog.String("command", cmd.String()))
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
if eerr, ok := err.(*exec.ExitError); ok { if eerr, ok := err.(*exec.ExitError); ok {
os.Stderr.Write(eerr.Stderr) os.Stderr.Write(eerr.Stderr)
@ -168,10 +176,13 @@ func (r *SubprocessPluginRuntime) InvokeHook(event string) error {
// TODO decide the best way to handle this code // TODO decide the best way to handle this code
// right now we implement status and error return in 3 slightly different ways in this file // right now we implement status and error return in 3 slightly different ways in this file
// then replace the other three with a call to this func // then replace the other three with a call to this func
func executeCmd(prog *exec.Cmd, pluginName string) error { func executeCmd(prog *exec.Cmd, pluginName string, logger *slog.Logger) error {
if logger == nil {
logger = slog.New(slog.DiscardHandler)
}
if err := prog.Run(); err != nil { if err := prog.Run(); err != nil {
if eerr, ok := err.(*exec.ExitError); ok { if eerr, ok := err.(*exec.ExitError); ok {
slog.Debug( logger.Debug(
"plugin execution failed", "plugin execution failed",
slog.String("pluginName", pluginName), slog.String("pluginName", pluginName),
slog.String("error", err.Error()), slog.String("error", err.Error()),
@ -216,8 +227,8 @@ func (r *SubprocessPluginRuntime) runCLI(input *Input) (*Output, error) {
cmd.Stdout = input.Stdout cmd.Stdout = input.Stdout
cmd.Stderr = input.Stderr cmd.Stderr = input.Stderr
slog.Debug("executing plugin command", slog.String("pluginName", r.metadata.Name), slog.String("command", cmd.String())) r.log().Debug("executing plugin command", slog.String("pluginName", r.metadata.Name), slog.String("command", cmd.String()))
if err := executeCmd(cmd, r.metadata.Name); err != nil { if err := executeCmd(cmd, r.metadata.Name, r.logger); err != nil {
return nil, err return nil, err
} }
@ -265,8 +276,8 @@ func (r *SubprocessPluginRuntime) runPostrenderer(input *Input) (*Output, error)
cmd.Stdout = postRendered cmd.Stdout = postRendered
cmd.Stderr = stderr cmd.Stderr = stderr
slog.Debug("executing plugin command", slog.String("pluginName", r.metadata.Name), slog.String("command", cmd.String())) r.log().Debug("executing plugin command", slog.String("pluginName", r.metadata.Name), slog.String("command", cmd.String()))
if err := executeCmd(cmd, r.metadata.Name); err != nil { if err := executeCmd(cmd, r.metadata.Name, r.logger); err != nil {
return nil, err return nil, err
} }

@ -88,8 +88,8 @@ func (r *SubprocessPluginRuntime) runGetter(input *Input) (*Output, error) {
cmd.Stdout = &buf cmd.Stdout = &buf
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
slog.Debug("executing plugin command", slog.String("pluginName", r.metadata.Name), slog.String("command", cmd.String())) r.log().Debug("executing plugin command", slog.String("pluginName", r.metadata.Name), slog.String("command", cmd.String()))
if err := executeCmd(cmd, r.metadata.Name); err != nil { if err := executeCmd(cmd, r.metadata.Name, r.logger); err != nil {
return nil, err return nil, err
} }

@ -74,7 +74,10 @@ var events = map[string]v2.HookEvent{
// //
// Files that do not parse into the expected format are simply placed into a map and // Files that do not parse into the expected format are simply placed into a map and
// returned. // returned.
func SortManifests(files map[string]string, _ common.VersionSet, ordering KindSortOrder) ([]*v2.Hook, []Manifest, error) { func SortManifests(files map[string]string, _ common.VersionSet, ordering KindSortOrder, logger *slog.Logger) ([]*v2.Hook, []Manifest, error) {
if logger == nil {
logger = slog.New(slog.DiscardHandler)
}
result := &result{} result := &result{}
var sortedFilePaths []string var sortedFilePaths []string
@ -101,7 +104,7 @@ func SortManifests(files map[string]string, _ common.VersionSet, ordering KindSo
path: filePath, path: filePath,
} }
if err := manifestFile.sort(result); err != nil { if err := manifestFile.sort(result, logger); err != nil {
return result.hooks, result.generic, err return result.hooks, result.generic, err
} }
} }
@ -136,7 +139,7 @@ func SortManifests(files map[string]string, _ common.VersionSet, ordering KindSo
// metadata: // metadata:
// annotations: // annotations:
// helm.sh/hook-output-log-policy: hook-succeeded,hook-failed // helm.sh/hook-output-log-policy: hook-succeeded,hook-failed
func (file *manifestFile) sort(result *result) error { func (file *manifestFile) sort(result *result, logger *slog.Logger) error {
// Go through manifests in order found in file (function `SplitManifests` creates integer-sortable keys) // Go through manifests in order found in file (function `SplitManifests` creates integer-sortable keys)
var sortedEntryKeys []string var sortedEntryKeys []string
for entryKey := range file.entries { for entryKey := range file.entries {
@ -196,7 +199,7 @@ func (file *manifestFile) sort(result *result) error {
} }
if isUnknownHook { if isUnknownHook {
slog.Info("skipping unknown hooks", "hookTypes", hookTypes) logger.Info("skipping unknown hooks", "hookTypes", hookTypes)
continue continue
} }

@ -138,7 +138,7 @@ metadata:
manifests[o.path] = o.manifest manifests[o.path] = o.manifest
} }
hs, generic, err := SortManifests(manifests, nil, InstallOrder) hs, generic, err := SortManifests(manifests, nil, InstallOrder, nil)
if err != nil { if err != nil {
t.Fatalf("Unexpected error: %s", err) t.Fatalf("Unexpected error: %s", err)
} }

@ -33,12 +33,15 @@ import (
// are filtered by walkFn. The files are walked in lexical order, which makes the // are filtered by walkFn. The files are walked in lexical order, which makes the
// output deterministic but means that for very large directories Walk can be // output deterministic but means that for very large directories Walk can be
// inefficient. Walk follows symbolic links. // inefficient. Walk follows symbolic links.
func Walk(root string, walkFn filepath.WalkFunc) error { func Walk(root string, walkFn filepath.WalkFunc, logger *slog.Logger) error {
if logger == nil {
logger = slog.New(slog.DiscardHandler)
}
info, err := os.Lstat(root) info, err := os.Lstat(root)
if err != nil { if err != nil {
err = walkFn(root, nil, err) err = walkFn(root, nil, err)
} else { } else {
err = symwalk(root, info, walkFn) err = symwalk(root, info, walkFn, logger)
} }
if err == filepath.SkipDir { if err == filepath.SkipDir {
return nil return nil
@ -63,7 +66,7 @@ func readDirNames(dirname string) ([]string, error) {
} }
// symwalk recursively descends path, calling walkFn. // symwalk recursively descends path, calling walkFn.
func symwalk(path string, info os.FileInfo, walkFn filepath.WalkFunc) error { func symwalk(path string, info os.FileInfo, walkFn filepath.WalkFunc, logger *slog.Logger) error {
// Recursively walk symlinked directories. // Recursively walk symlinked directories.
if IsSymlink(info) { if IsSymlink(info) {
resolved, err := filepath.EvalSymlinks(path) resolved, err := filepath.EvalSymlinks(path)
@ -71,11 +74,11 @@ func symwalk(path string, info os.FileInfo, walkFn filepath.WalkFunc) error {
return fmt.Errorf("error evaluating symlink %s: %w", path, err) 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) logger.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 { if info, err = os.Lstat(resolved); err != nil {
return err return err
} }
if err := symwalk(path, info, walkFn); err != nil && err != filepath.SkipDir { if err := symwalk(path, info, walkFn, logger); err != nil && err != filepath.SkipDir {
return err return err
} }
return nil return nil
@ -102,7 +105,7 @@ func symwalk(path string, info os.FileInfo, walkFn filepath.WalkFunc) error {
return err return err
} }
} else { } else {
err = symwalk(filename, fileInfo, walkFn) err = symwalk(filename, fileInfo, walkFn, logger)
if err != nil { if err != nil {
if (!fileInfo.IsDir() && !IsSymlink(fileInfo)) || err != filepath.SkipDir { if (!fileInfo.IsDir() && !IsSymlink(fileInfo)) || err != filepath.SkipDir {
return err return err

@ -136,7 +136,7 @@ func TestWalk(t *testing.T) {
return mark(info, err, &errors, true) return mark(info, err, &errors, true)
} }
// Expect no errors. // Expect no errors.
err := Walk(tree.name, markFn) err := Walk(tree.name, markFn, nil)
if err != nil { if err != nil {
t.Fatalf("no error expected, found: %s", err) t.Fatalf("no error expected, found: %s", err)
} }

@ -75,8 +75,13 @@ func GetUserAgent() string {
return "Helm/" + strings.TrimPrefix(GetVersion(), "v") return "Helm/" + strings.TrimPrefix(GetVersion(), "v")
} }
// Get returns build info // Get returns build info.
func Get() BuildInfo { // An optional logger may be provided for diagnostic messages. If nil, a discard
// logger is used.
func Get(logger *slog.Logger) BuildInfo {
if logger == nil {
logger = slog.New(slog.DiscardHandler)
}
makeKubeClientVersionString := func() string { makeKubeClientVersionString := func() string {
// Test builds don't include debug info / module info // Test builds don't include debug info / module info
@ -88,13 +93,13 @@ func Get() BuildInfo {
vstr, err := K8sIOClientGoModVersion() vstr, err := K8sIOClientGoModVersion()
if err != nil { if err != nil {
slog.Error("failed to retrieve k8s.io/client-go version", slog.Any("error", err)) logger.Error("failed to retrieve k8s.io/client-go version", slog.Any("error", err))
return "" return ""
} }
v, err := semver.NewVersion(vstr) v, err := semver.NewVersion(vstr)
if err != nil { if err != nil {
slog.Error("unable to parse k8s.io/client-go version", slog.String("version", vstr), slog.Any("error", err)) logger.Error("unable to parse k8s.io/client-go version", slog.String("version", vstr), slog.Any("error", err))
return "" return ""
} }

@ -157,7 +157,6 @@ func ConfigurationSetLogger(h slog.Handler) ConfigurationOption {
func NewConfiguration(options ...ConfigurationOption) *Configuration { func NewConfiguration(options ...ConfigurationOption) *Configuration {
c := &Configuration{} c := &Configuration{}
c.SetLogger(slog.Default().Handler())
for _, o := range options { for _, o := range options {
o(c) o(c)
@ -461,7 +460,7 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values common.Values,
// Sort hooks, manifests, and partials. Only hooks and manifests are returned, // Sort hooks, manifests, and partials. Only hooks and manifests are returned,
// as partials are not used after renderer.Render. Empty manifests are also // as partials are not used after renderer.Render. Empty manifests are also
// removed here. // removed here.
hs, manifests, err := releaseutil.SortManifests(files, nil, releaseutil.InstallOrder) hs, manifests, err := releaseutil.SortManifests(files, nil, releaseutil.InstallOrder, nil)
if err != nil { if err != nil {
// By catching parse errors here, we can prevent bogus releases from going // By catching parse errors here, we can prevent bogus releases from going
// to Kubernetes. // to Kubernetes.

@ -52,6 +52,10 @@ type Metadata struct {
Status string `json:"status" yaml:"status"` Status string `json:"status" yaml:"status"`
DeployedAt string `json:"deployedAt" yaml:"deployedAt"` DeployedAt string `json:"deployedAt" yaml:"deployedAt"`
ApplyMethod string `json:"applyMethod,omitempty" yaml:"applyMethod,omitempty"` ApplyMethod string `json:"applyMethod,omitempty" yaml:"applyMethod,omitempty"`
// logger is the structured logger for this metadata instance.
// It is not serialized.
logger *slog.Logger `json:"-" yaml:"-"`
} }
// NewGetMetadata creates a new GetMetadata object with the given configuration. // NewGetMetadata creates a new GetMetadata object with the given configuration.
@ -106,16 +110,21 @@ func (g *GetMetadata) Run(name string) (*Metadata, error) {
Status: rac.Status(), Status: rac.Status(),
DeployedAt: rac.DeployedAt().Format(time.RFC3339), DeployedAt: rac.DeployedAt().Format(time.RFC3339),
ApplyMethod: rac.ApplyMethod(), ApplyMethod: rac.ApplyMethod(),
logger: g.cfg.Logger(),
}, nil }, nil
} }
// FormattedDepNames formats metadata.dependencies names into a comma-separated list. // FormattedDepNames formats metadata.dependencies names into a comma-separated list.
func (m *Metadata) FormattedDepNames() string { func (m *Metadata) FormattedDepNames() string {
logger := m.logger
if logger == nil {
logger = slog.New(slog.DiscardHandler)
}
depsNames := make([]string, 0, len(m.Dependencies)) depsNames := make([]string, 0, len(m.Dependencies))
for _, dep := range m.Dependencies { for _, dep := range m.Dependencies {
ac, err := ci.NewDependencyAccessor(dep) ac, err := ci.NewDependencyAccessor(dep)
if err != nil { if err != nil {
slog.Error("unable to access dependency metadata", "error", err) logger.Error("unable to access dependency metadata", "error", err)
continue continue
} }
depsNames = append(depsNames, ac.Name()) depsNames = append(depsNames, ac.Name())

@ -258,7 +258,7 @@ func (u *Uninstall) deleteRelease(rel *release.Release) (kube.ResourceList, stri
var errs []error var errs []error
manifests := releaseutil.SplitManifests(rel.Manifest) manifests := releaseutil.SplitManifests(rel.Manifest)
_, files, err := releaseutil.SortManifests(manifests, nil, releaseutil.UninstallOrder) _, files, err := releaseutil.SortManifests(manifests, nil, releaseutil.UninstallOrder, nil)
if err != nil { if err != nil {
// We could instead just delete everything in no particular order. // We could instead just delete everything in no particular order.
// FIXME: One way to delete at this point would be to try a label-based // FIXME: One way to delete at this point would be to try a label-based
@ -283,12 +283,12 @@ func (u *Uninstall) deleteRelease(rel *release.Release) (kube.ResourceList, stri
return nil, "", []error{fmt.Errorf("unable to build kubernetes objects for delete: %w", err)} return nil, "", []error{fmt.Errorf("unable to build kubernetes objects for delete: %w", err)}
} }
if len(resources) > 0 { if len(resources) > 0 {
_, errs = u.cfg.KubeClient.Delete(resources, parseCascadingFlag(u.DeletionPropagation)) _, errs = u.cfg.KubeClient.Delete(resources, u.parseCascadingFlag(u.DeletionPropagation))
} }
return resources, kept.String(), errs return resources, kept.String(), errs
} }
func parseCascadingFlag(cascadingFlag string) v1.DeletionPropagation { func (u *Uninstall) parseCascadingFlag(cascadingFlag string) v1.DeletionPropagation {
switch cascadingFlag { switch cascadingFlag {
case "orphan": case "orphan":
return v1.DeletePropagationOrphan return v1.DeletePropagationOrphan
@ -297,7 +297,7 @@ func parseCascadingFlag(cascadingFlag string) v1.DeletionPropagation {
case "background": case "background":
return v1.DeletePropagationBackground return v1.DeletePropagationBackground
default: default:
slog.Debug("uninstall: given cascade value, defaulting to delete propagation background", "value", cascadingFlag) u.cfg.Logger().Debug("uninstall: given cascade value, defaulting to delete propagation background", "value", cascadingFlag)
return v1.DeletePropagationBackground return v1.DeletePropagationBackground
} }
} }

@ -277,7 +277,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[str
return nil, nil, false, err return nil, nil, false, err
} }
if err := chartutil.ProcessDependencies(chart, vals); err != nil { if err := chartutil.ProcessDependencies(chart, vals, nil); err != nil {
return nil, nil, false, err return nil, nil, false, err
} }

@ -31,13 +31,13 @@ var NewAccessor func(chrt Charter) (Accessor, error) = NewDefaultAccessor //noli
func NewDefaultAccessor(chrt Charter) (Accessor, error) { func NewDefaultAccessor(chrt Charter) (Accessor, error) {
switch v := chrt.(type) { switch v := chrt.(type) {
case v2chart.Chart: case v2chart.Chart:
return &v2Accessor{&v}, nil return &v2Accessor{chrt: &v}, nil
case *v2chart.Chart: case *v2chart.Chart:
return &v2Accessor{v}, nil return &v2Accessor{chrt: v}, nil
case v3chart.Chart: case v3chart.Chart:
return &v3Accessor{&v}, nil return &v3Accessor{chrt: &v}, nil
case *v3chart.Chart: case *v3chart.Chart:
return &v3Accessor{v}, nil return &v3Accessor{chrt: v}, nil
default: default:
return nil, errors.New("unsupported chart type") return nil, errors.New("unsupported chart type")
} }
@ -45,6 +45,14 @@ func NewDefaultAccessor(chrt Charter) (Accessor, error) {
type v2Accessor struct { type v2Accessor struct {
chrt *v2chart.Chart chrt *v2chart.Chart
logger *slog.Logger
}
func (r *v2Accessor) log() *slog.Logger {
if r.logger != nil {
return r.logger
}
return slog.New(slog.DiscardHandler)
} }
func (r *v2Accessor) Name() string { func (r *v2Accessor) Name() string {
@ -63,7 +71,7 @@ func (r *v2Accessor) MetadataAsMap() map[string]any {
ret, err := structToMap(r.chrt.Metadata) ret, err := structToMap(r.chrt.Metadata)
if err != nil { if err != nil {
slog.Error("error converting metadata to map", "error", err) r.log().Error("error converting metadata to map", "error", err)
} }
return ret return ret
} }
@ -114,6 +122,14 @@ func (r *v2Accessor) Deprecated() bool {
type v3Accessor struct { type v3Accessor struct {
chrt *v3chart.Chart chrt *v3chart.Chart
logger *slog.Logger
}
func (r *v3Accessor) log() *slog.Logger {
if r.logger != nil {
return r.logger
}
return slog.New(slog.DiscardHandler)
} }
func (r *v3Accessor) Name() string { func (r *v3Accessor) Name() string {
@ -132,7 +148,7 @@ func (r *v3Accessor) MetadataAsMap() map[string]any {
ret, err := structToMap(r.chrt.Metadata) ret, err := structToMap(r.chrt.Metadata)
if err != nil { if err != nil {
slog.Error("error converting metadata to map", "error", err) r.log().Error("error converting metadata to map", "error", err)
} }
return ret return ret
} }

@ -177,6 +177,6 @@ func newCapabilities(kubeVersionMajor, kubeVersionMinor uint64) (*Capabilities,
Minor: strconv.FormatUint(kubeVersionMinor, 10), Minor: strconv.FormatUint(kubeVersionMinor, 10),
}, },
APIVersions: DefaultVersionSet, APIVersions: DefaultVersionSet,
HelmVersion: helmversion.Get(), HelmVersion: helmversion.Get(nil),
}, nil }, nil
} }

@ -74,20 +74,21 @@ func newHTTPURLLoader() *HTTPURLLoader {
// ValidateAgainstSchema checks that values does not violate the structure laid out in schema // ValidateAgainstSchema checks that values does not violate the structure laid out in schema
func ValidateAgainstSchema(ch chart.Charter, values map[string]any) error { func ValidateAgainstSchema(ch chart.Charter, values map[string]any) error {
logger := slog.Default()
chrt, err := chart.NewAccessor(ch) chrt, err := chart.NewAccessor(ch)
if err != nil { if err != nil {
return err return err
} }
var sb strings.Builder var sb strings.Builder
if chrt.Schema() != nil { if chrt.Schema() != nil {
slog.Debug("chart name", "chart-name", chrt.Name()) logger.Debug("chart name", "chart-name", chrt.Name())
err := ValidateAgainstSingleSchema(values, chrt.Schema()) err := ValidateAgainstSingleSchema(values, chrt.Schema())
if err != nil { if err != nil {
fmt.Fprintf(&sb, "%s:\n", chrt.Name()) fmt.Fprintf(&sb, "%s:\n", chrt.Name())
sb.WriteString(err.Error()) sb.WriteString(err.Error())
} }
} }
slog.Debug("number of dependencies in the chart", "chart", chrt.Name(), "dependencies", len(chrt.Dependencies())) logger.Debug("number of dependencies in the chart", "chart", chrt.Name(), "dependencies", len(chrt.Dependencies()))
// For each dependency, recursively call this function with the coalesced values // For each dependency, recursively call this function with the coalesced values
for _, subchart := range chrt.Dependencies() { for _, subchart := range chrt.Dependencies() {
sub, err := chart.NewAccessor(subchart) sub, err := chart.NewAccessor(subchart)
@ -122,6 +123,7 @@ func ValidateAgainstSchema(ch chart.Charter, values map[string]any) error {
// ValidateAgainstSingleSchema checks that values does not violate the structure laid out in this schema // ValidateAgainstSingleSchema checks that values does not violate the structure laid out in this schema
func ValidateAgainstSingleSchema(values common.Values, schemaJSON []byte) (reterr error) { func ValidateAgainstSingleSchema(values common.Values, schemaJSON []byte) (reterr error) {
logger := slog.Default()
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
reterr = fmt.Errorf("unable to validate schema: %s", r) reterr = fmt.Errorf("unable to validate schema: %s", r)
@ -134,14 +136,14 @@ func ValidateAgainstSingleSchema(values common.Values, schemaJSON []byte) (reter
if err != nil { if err != nil {
return err return err
} }
slog.Debug("unmarshalled JSON schema", "schema", schemaJSON) logger.Debug("unmarshalled JSON schema", "schema", schemaJSON)
// Configure compiler with loaders for different URL schemes // Configure compiler with loaders for different URL schemes
loader := jsonschema.SchemeURLLoader{ loader := jsonschema.SchemeURLLoader{
"file": jsonschema.FileLoader{}, "file": jsonschema.FileLoader{},
"http": newHTTPURLLoader(), "http": newHTTPURLLoader(),
"https": newHTTPURLLoader(), "https": newHTTPURLLoader(),
"urn": urnLoader{}, "urn": urnLoader{logger: logger},
} }
compiler := jsonschema.NewCompiler() compiler := jsonschema.NewCompiler()
@ -178,7 +180,9 @@ var URNResolver URNResolverFunc = func(urn string) (any, error) {
// urnLoader implements resolution for the urn: scheme by delegating to // urnLoader implements resolution for the urn: scheme by delegating to
// URNResolver. If unresolved, it logs a warning and returns a permissive // URNResolver. If unresolved, it logs a warning and returns a permissive
// boolean-true schema to avoid hard failures (back-compat behavior). // boolean-true schema to avoid hard failures (back-compat behavior).
type urnLoader struct{} type urnLoader struct {
logger *slog.Logger
}
// warnedURNs ensures we log the unresolved-URN warning only once per URN. // warnedURNs ensures we log the unresolved-URN warning only once per URN.
var warnedURNs sync.Map var warnedURNs sync.Map
@ -188,7 +192,7 @@ func (l urnLoader) Load(urlStr string) (any, error) {
return doc, nil return doc, nil
} }
if _, loaded := warnedURNs.LoadOrStore(urlStr, struct{}{}); !loaded { if _, loaded := warnedURNs.LoadOrStore(urlStr, struct{}{}); !loaded {
slog.Warn("unresolved URN reference ignored; using permissive schema", "urn", urlStr) l.logger.Warn("unresolved URN reference ignored; using permissive schema", "urn", urlStr)
} }
return jsonschema.UnmarshalJSON(strings.NewReader("true")) return jsonschema.UnmarshalJSON(strings.NewReader("true"))
} }

@ -235,8 +235,13 @@ func TestValidateAgainstSchema2020Negative(t *testing.T) {
} }
var errString string var errString string
<<<<<<< HEAD
if err := ValidateAgainstSchema(chrt, vals); err == nil { if err := ValidateAgainstSchema(chrt, vals); err == nil {
t.Fatal("Expected an error, but got nil") t.Fatal("Expected an error, but got nil")
=======
if err := ValidateAgainstSchema(chrt, vals, nil); err == nil {
t.Fatalf("Expected an error, but got nil")
>>>>>>> 6f8673662 (fix(logging): replace global slog usage with dependency-injected loggers)
} else { } else {
errString = err.Error() errString = err.Error()
} }
@ -295,7 +300,7 @@ func TestValidateAgainstSingleSchema_UnresolvedURN_Ignored(t *testing.T) {
"$ref": "urn:example:helm:schemas:v1:helm-schema-validation-conditions:v1/helmSchemaValidation-true" "$ref": "urn:example:helm:schemas:v1:helm-schema-validation-conditions:v1/helmSchemaValidation-true"
}`) }`)
vals := map[string]any{"any": "value"} vals := map[string]any{"any": "value"}
if err := ValidateAgainstSingleSchema(vals, schema); err != nil { if err := ValidateAgainstSingleSchema(vals, schema, nil); err != nil {
t.Fatalf("expected no error when URN unresolved is ignored, got: %v", err) t.Fatalf("expected no error when URN unresolved is ignored, got: %v", err)
} }
} }
@ -328,7 +333,7 @@ func TestValidateAgainstSchema_MissingSubchartValues_NoPanic(t *testing.T) {
} }
}() }()
if err := ValidateAgainstSchema(chrt, vals); err != nil { if err := ValidateAgainstSchema(chrt, vals, nil); err != nil {
t.Fatalf("expected no error when subchart values are missing, got: %v", err) t.Fatalf("expected no error when subchart values are missing, got: %v", err)
} }
} }
@ -356,7 +361,7 @@ func TestValidateAgainstSchema_SubchartNil_NoPanic(t *testing.T) {
} }
}() }()
if err := ValidateAgainstSchema(chrt, vals); err != nil { if err := ValidateAgainstSchema(chrt, vals, nil); err != nil {
t.Fatalf("expected no error when subchart values are nil, got: %v", err) t.Fatalf("expected no error when subchart values are nil, got: %v", err)
} }
} }
@ -385,7 +390,12 @@ func TestValidateAgainstSchema_InvalidSubchartValuesType_NoPanic(t *testing.T) {
}() }()
// We expect a non-nil error (invalid type), but crucially no panic. // We expect a non-nil error (invalid type), but crucially no panic.
<<<<<<< HEAD
if err := ValidateAgainstSchema(chrt, vals); err == nil { if err := ValidateAgainstSchema(chrt, vals); err == nil {
t.Fatal("expected an error when subchart values have invalid type, got nil") t.Fatal("expected an error when subchart values have invalid type, got nil")
=======
if err := ValidateAgainstSchema(chrt, vals, nil); err == nil {
t.Fatalf("expected an error when subchart values have invalid type, got nil")
>>>>>>> 6f8673662 (fix(logging): replace global slog usage with dependency-injected loggers)
} }
} }

@ -119,7 +119,7 @@ func (t *templateLinter) Lint() {
// lint ignores import-values // lint ignores import-values
// See https://github.com/helm/helm/issues/9658 // See https://github.com/helm/helm/issues/9658
if err := chartutil.ProcessDependencies(chart, t.values); err != nil { if err := chartutil.ProcessDependencies(chart, t.values, nil); err != nil {
return return
} }

@ -78,7 +78,7 @@ func validateValuesFile(valuesPath string, overrides map[string]any, skipSchemaV
} }
if !skipSchemaValidation { if !skipSchemaValidation {
return util.ValidateAgainstSingleSchema(coalescedValues, schema) return util.ValidateAgainstSingleSchema(coalescedValues, schema, nil)
} }
return nil return nil

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

@ -30,14 +30,15 @@ import (
// ProcessDependencies checks through this chart's dependencies, processing accordingly. // ProcessDependencies checks through this chart's dependencies, processing accordingly.
func ProcessDependencies(c *chart.Chart, v common.Values) error { func ProcessDependencies(c *chart.Chart, v common.Values) error {
if err := processDependencyEnabled(c, v, ""); err != nil { logger := slog.Default()
if err := processDependencyEnabled(c, v, "", logger); err != nil {
return err return err
} }
return processDependencyImportValues(c, true) return processDependencyImportValues(c, true, logger)
} }
// processDependencyConditions disables charts based on condition path value in values // processDependencyConditions disables charts based on condition path value in values
func processDependencyConditions(reqs []*chart.Dependency, cvals common.Values, cpath string) { func processDependencyConditions(reqs []*chart.Dependency, cvals common.Values, cpath string, logger *slog.Logger) {
if reqs == nil { if reqs == nil {
return return
} }
@ -53,10 +54,10 @@ func processDependencyConditions(reqs []*chart.Dependency, cvals common.Values,
r.Enabled = bv r.Enabled = bv
break break
} }
slog.Warn("returned non-bool value", "path", c, "chart", r.Name) logger.Warn("returned non-bool value", "path", c, "chart", r.Name)
} else if !errors.As(err, &errNoValue) { } else if !errors.As(err, &errNoValue) {
// this is a real error // this is a real error
slog.Warn("the method PathValue returned error", slog.Any("error", err)) logger.Warn("the method PathValue returned error", slog.Any("error", err))
} }
} }
} }
@ -64,7 +65,7 @@ func processDependencyConditions(reqs []*chart.Dependency, cvals common.Values,
} }
// processDependencyTags disables charts based on tags in values // processDependencyTags disables charts based on tags in values
func processDependencyTags(reqs []*chart.Dependency, cvals common.Values) { func processDependencyTags(reqs []*chart.Dependency, cvals common.Values, logger *slog.Logger) {
if reqs == nil { if reqs == nil {
return return
} }
@ -84,7 +85,7 @@ func processDependencyTags(reqs []*chart.Dependency, cvals common.Values) {
hasFalse = true hasFalse = true
} }
} else { } else {
slog.Warn("returned non-bool value", "tag", k, "chart", r.Name) logger.Warn("returned non-bool value", "tag", k, "chart", r.Name)
} }
} }
} }
@ -143,7 +144,7 @@ func copyMetadata(metadata *chart.Metadata) *chart.Metadata {
} }
// processDependencyEnabled removes disabled charts from dependencies // processDependencyEnabled removes disabled charts from dependencies
func processDependencyEnabled(c *chart.Chart, v map[string]any, path string) error { func processDependencyEnabled(c *chart.Chart, v map[string]any, path string, logger *slog.Logger) error {
if c.Metadata.Dependencies == nil { if c.Metadata.Dependencies == nil {
return nil return nil
} }
@ -186,8 +187,8 @@ Loop:
return err return err
} }
// flag dependencies as enabled/disabled // flag dependencies as enabled/disabled
processDependencyTags(c.Metadata.Dependencies, cvals) processDependencyTags(c.Metadata.Dependencies, cvals, logger)
processDependencyConditions(c.Metadata.Dependencies, cvals, path) processDependencyConditions(c.Metadata.Dependencies, cvals, path, logger)
// make a map of charts to remove // make a map of charts to remove
rm := map[string]struct{}{} rm := map[string]struct{}{}
for _, r := range c.Metadata.Dependencies { for _, r := range c.Metadata.Dependencies {
@ -216,7 +217,7 @@ Loop:
// recursively call self to process sub dependencies // recursively call self to process sub dependencies
for _, t := range cd { for _, t := range cd {
subpath := path + t.Metadata.Name + "." subpath := path + t.Metadata.Name + "."
if err := processDependencyEnabled(t, cvals, subpath); err != nil { if err := processDependencyEnabled(t, cvals, subpath, logger); err != nil {
return err return err
} }
} }
@ -250,7 +251,7 @@ func set(path []string, data map[string]any) map[string]any {
} }
// processImportValues merges values from child to parent based on the chart's dependencies' ImportValues field. // processImportValues merges values from child to parent based on the chart's dependencies' ImportValues field.
func processImportValues(c *chart.Chart, merge bool) error { func processImportValues(c *chart.Chart, merge bool, logger *slog.Logger) error {
if c.Metadata.Dependencies == nil { if c.Metadata.Dependencies == nil {
return nil return nil
} }
@ -283,7 +284,7 @@ func processImportValues(c *chart.Chart, merge bool) error {
// get child table // get child table
vv, err := cvals.Table(r.Name + "." + child) vv, err := cvals.Table(r.Name + "." + child)
if err != nil { if err != nil {
slog.Warn( logger.Warn(
"ImportValues missing table from chart", "ImportValues missing table from chart",
slog.String("chart", r.Name), slog.String("chart", r.Name),
slog.Any("error", err), slog.Any("error", err),
@ -304,7 +305,7 @@ func processImportValues(c *chart.Chart, merge bool) error {
}) })
vm, err := cvals.Table(r.Name + "." + child) vm, err := cvals.Table(r.Name + "." + child)
if err != nil { if err != nil {
slog.Warn("ImportValues missing table", slog.Any("error", err)) logger.Warn("ImportValues missing table", slog.Any("error", err))
continue continue
} }
if merge { if merge {
@ -372,12 +373,12 @@ func istable(v any) bool {
} }
// processDependencyImportValues imports specified chart values from child to parent. // processDependencyImportValues imports specified chart values from child to parent.
func processDependencyImportValues(c *chart.Chart, merge bool) error { func processDependencyImportValues(c *chart.Chart, merge bool, logger *slog.Logger) error {
for _, d := range c.Dependencies() { for _, d := range c.Dependencies() {
// recurse // recurse
if err := processDependencyImportValues(d, merge); err != nil { if err := processDependencyImportValues(d, merge, logger); err != nil {
return err return err
} }
} }
return processImportValues(c, merge) return processImportValues(c, merge, logger)
} }

@ -15,6 +15,7 @@ limitations under the License.
package util package util
import ( import (
"log/slog"
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
@ -117,7 +118,7 @@ func TestDependencyEnabled(t *testing.T) {
for _, tc := range tests { for _, tc := range tests {
c := loadChart(t, "testdata/subpop") c := loadChart(t, "testdata/subpop")
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
if err := processDependencyEnabled(c, tc.v, ""); err != nil { if err := processDependencyEnabled(c, tc.v, "", slog.New(slog.DiscardHandler)); err != nil {
t.Fatalf("error processing enabled dependencies %v", err) t.Fatalf("error processing enabled dependencies %v", err)
} }
@ -219,7 +220,7 @@ func TestProcessDependencyImportValues(t *testing.T) {
e["SCBexported2A"] = "blaster" e["SCBexported2A"] = "blaster"
e["global.SC1exported2.all.SC1exported3"] = "SC1expstr" e["global.SC1exported2.all.SC1exported3"] = "SC1expstr"
if err := processDependencyImportValues(c, false); err != nil { if err := processDependencyImportValues(c, false, slog.New(slog.DiscardHandler)); err != nil {
t.Fatalf("processing import values dependencies %v", err) t.Fatalf("processing import values dependencies %v", err)
} }
cc := common.Values(c.Values) cc := common.Values(c.Values)
@ -259,7 +260,7 @@ func TestProcessDependencyImportValues(t *testing.T) {
} }
c = loadChart(t, "testdata/subpop") c = loadChart(t, "testdata/subpop")
if err := processDependencyImportValues(c, true); err != nil { if err := processDependencyImportValues(c, true, slog.New(slog.DiscardHandler)); err != nil {
t.Fatalf("processing import values dependencies %v", err) t.Fatalf("processing import values dependencies %v", err)
} }
cc = common.Values(c.Values) cc = common.Values(c.Values)
@ -275,10 +276,10 @@ func TestProcessDependencyImportValues(t *testing.T) {
func TestProcessDependencyImportValuesFromSharedDependencyToAliases(t *testing.T) { func TestProcessDependencyImportValuesFromSharedDependencyToAliases(t *testing.T) {
c := loadChart(t, "testdata/chart-with-import-from-aliased-dependencies") c := loadChart(t, "testdata/chart-with-import-from-aliased-dependencies")
if err := processDependencyEnabled(c, c.Values, ""); err != nil { if err := processDependencyEnabled(c, c.Values, "", slog.New(slog.DiscardHandler)); err != nil {
t.Fatalf("expected no errors but got %q", err) t.Fatalf("expected no errors but got %q", err)
} }
if err := processDependencyImportValues(c, true); err != nil { if err := processDependencyImportValues(c, true, slog.New(slog.DiscardHandler)); err != nil {
t.Fatalf("processing import values dependencies %v", err) t.Fatalf("processing import values dependencies %v", err)
} }
e := make(map[string]string) e := make(map[string]string)
@ -327,7 +328,7 @@ func TestProcessDependencyImportValuesMultiLevelPrecedence(t *testing.T) {
e["app2.service.port"] = "8080" e["app2.service.port"] = "8080"
e["app3.service.port"] = "9090" e["app3.service.port"] = "9090"
e["app4.service.port"] = "1234" e["app4.service.port"] = "1234"
if err := processDependencyImportValues(c, true); err != nil { if err := processDependencyImportValues(c, true, slog.New(slog.DiscardHandler)); err != nil {
t.Fatalf("processing import values dependencies %v", err) t.Fatalf("processing import values dependencies %v", err)
} }
cc := common.Values(c.Values) cc := common.Values(c.Values)
@ -354,7 +355,7 @@ func TestProcessDependencyImportValuesForEnabledCharts(t *testing.T) {
c := loadChart(t, "testdata/import-values-from-enabled-subchart/parent-chart") c := loadChart(t, "testdata/import-values-from-enabled-subchart/parent-chart")
nameOverride := "parent-chart-prod" nameOverride := "parent-chart-prod"
if err := processDependencyImportValues(c, true); err != nil { if err := processDependencyImportValues(c, true, slog.New(slog.DiscardHandler)); err != nil {
t.Fatalf("processing import values dependencies %v", err) t.Fatalf("processing import values dependencies %v", err)
} }
@ -362,7 +363,7 @@ func TestProcessDependencyImportValuesForEnabledCharts(t *testing.T) {
t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies())) t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies()))
} }
if err := processDependencyEnabled(c, c.Values, ""); err != nil { if err := processDependencyEnabled(c, c.Values, "", slog.New(slog.DiscardHandler)); err != nil {
t.Fatalf("expected no errors but got %q", err) t.Fatalf("expected no errors but got %q", err)
} }
@ -427,7 +428,7 @@ func TestDependentChartAliases(t *testing.T) {
t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies())) t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies()))
} }
if err := processDependencyEnabled(c, c.Values, ""); err != nil { if err := processDependencyEnabled(c, c.Values, "", slog.New(slog.DiscardHandler)); err != nil {
t.Fatalf("expected no errors but got %q", err) t.Fatalf("expected no errors but got %q", err)
} }
@ -469,7 +470,7 @@ func TestDependentChartWithSubChartsAbsentInDependency(t *testing.T) {
t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies())) t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies()))
} }
if err := processDependencyEnabled(c, c.Values, ""); err != nil { if err := processDependencyEnabled(c, c.Values, "", slog.New(slog.DiscardHandler)); err != nil {
t.Fatalf("expected no errors but got %q", err) t.Fatalf("expected no errors but got %q", err)
} }
@ -506,7 +507,7 @@ func TestDependentChartsWithSubchartsAllSpecifiedInDependency(t *testing.T) {
t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies())) t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies()))
} }
if err := processDependencyEnabled(c, c.Values, ""); err != nil { if err := processDependencyEnabled(c, c.Values, "", slog.New(slog.DiscardHandler)); err != nil {
t.Fatalf("expected no errors but got %q", err) t.Fatalf("expected no errors but got %q", err)
} }
@ -526,7 +527,7 @@ func TestDependentChartsWithSomeSubchartsSpecifiedInDependency(t *testing.T) {
t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies())) t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies()))
} }
if err := processDependencyEnabled(c, c.Values, ""); err != nil { if err := processDependencyEnabled(c, c.Values, "", slog.New(slog.DiscardHandler)); err != nil {
t.Fatalf("expected no errors but got %q", err) t.Fatalf("expected no errors but got %q", err)
} }
@ -559,7 +560,7 @@ func TestChartWithDependencyAliasedTwiceAndDoublyReferencedSubDependency(t *test
t.Fatalf("expected one dependency for this chart, but got %d", len(c.Dependencies())) t.Fatalf("expected one dependency for this chart, but got %d", len(c.Dependencies()))
} }
if err := processDependencyEnabled(c, c.Values, ""); err != nil { if err := processDependencyEnabled(c, c.Values, "", slog.New(slog.DiscardHandler)); err != nil {
t.Fatalf("expected no errors but got %q", err) t.Fatalf("expected no errors but got %q", err)
} }

@ -56,41 +56,44 @@ func addValueOptionsFlags(f *pflag.FlagSet, v *values.Options) {
f.StringArrayVar(&v.LiteralValues, "set-literal", []string{}, "set a literal STRING value on the command line") f.StringArrayVar(&v.LiteralValues, "set-literal", []string{}, "set a literal STRING value on the command line")
} }
func AddWaitFlag(cmd *cobra.Command, wait *kube.WaitStrategy) { func AddWaitFlag(cmd *cobra.Command, wait *kube.WaitStrategy, logger *slog.Logger) {
cmd.Flags().Var( cmd.Flags().Var(
newWaitValue(kube.HookOnlyStrategy, wait), newWaitValue(kube.HookOnlyStrategy, wait, logger),
"wait", "wait",
"wait until resources are ready (up to --timeout). Use '--wait' alone for 'watcher' strategy, or specify one of: 'watcher', 'hookOnly', 'legacy'. Default when flag is omitted: 'hookOnly'.", "wait until resources are ready (up to --timeout). Use '--wait' alone for 'watcher' strategy, or specify one of: 'watcher', 'hookOnly', 'legacy'. Default when flag is omitted: 'hookOnly'.",
) )
cmd.Flags().Lookup("wait").NoOptDefVal = string(kube.StatusWatcherStrategy) cmd.Flags().Lookup("wait").NoOptDefVal = string(kube.StatusWatcherStrategy)
} }
type waitValue kube.WaitStrategy type waitValue struct {
ws *kube.WaitStrategy
logger *slog.Logger
}
func newWaitValue(defaultValue kube.WaitStrategy, ws *kube.WaitStrategy) *waitValue { func newWaitValue(defaultValue kube.WaitStrategy, ws *kube.WaitStrategy, logger *slog.Logger) *waitValue {
*ws = defaultValue *ws = defaultValue
return (*waitValue)(ws) return &waitValue{ws: ws, logger: logger}
} }
func (ws *waitValue) String() string { func (ws *waitValue) String() string {
if ws == nil { if ws == nil || ws.ws == nil {
return "" return ""
} }
return string(*ws) return string(*ws.ws)
} }
func (ws *waitValue) Set(s string) error { func (ws *waitValue) Set(s string) error {
switch s { switch s {
case string(kube.StatusWatcherStrategy), string(kube.LegacyStrategy), string(kube.HookOnlyStrategy): case string(kube.StatusWatcherStrategy), string(kube.LegacyStrategy), string(kube.HookOnlyStrategy):
*ws = waitValue(s) *ws.ws = kube.WaitStrategy(s)
return nil return nil
case "true": case "true":
slog.Warn("--wait=true is deprecated (boolean value) and can be replaced with --wait=watcher") ws.logger.Warn("--wait=true is deprecated (boolean value) and can be replaced with --wait=watcher")
*ws = waitValue(kube.StatusWatcherStrategy) *ws.ws = kube.StatusWatcherStrategy
return nil return nil
case "false": case "false":
slog.Warn("--wait=false is deprecated (boolean value) and can be replaced with --wait=hookOnly") ws.logger.Warn("--wait=false is deprecated (boolean value) and can be replaced with --wait=hookOnly")
*ws = waitValue(kube.HookOnlyStrategy) *ws.ws = kube.HookOnlyStrategy
return nil return nil
default: default:
return fmt.Errorf("invalid wait input %q. Valid inputs are %s, %s, and %s", s, kube.StatusWatcherStrategy, kube.HookOnlyStrategy, kube.LegacyStrategy) return fmt.Errorf("invalid wait input %q. Valid inputs are %s, %s, and %s", s, kube.StatusWatcherStrategy, kube.HookOnlyStrategy, kube.LegacyStrategy)

@ -42,14 +42,14 @@ func addDryRunFlag(cmd *cobra.Command) {
// Determine the `action.DryRunStrategy` given -dry-run=<value>` flag (or absence of) // Determine the `action.DryRunStrategy` given -dry-run=<value>` flag (or absence of)
// Legacy usage of the flag: boolean values, and `--dry-run` (without value) are supported, and log warnings emitted // Legacy usage of the flag: boolean values, and `--dry-run` (without value) are supported, and log warnings emitted
func cmdGetDryRunFlagStrategy(cmd *cobra.Command, isTemplate bool) (action.DryRunStrategy, error) { func cmdGetDryRunFlagStrategy(cmd *cobra.Command, isTemplate bool, logger *slog.Logger) (action.DryRunStrategy, error) {
f := cmd.Flag("dry-run") f := cmd.Flag("dry-run")
v := f.Value.String() v := f.Value.String()
switch v { switch v {
case f.NoOptDefVal: case f.NoOptDefVal:
slog.Warn(`--dry-run is deprecated and should be replaced with '--dry-run=client'`) logger.Warn(`--dry-run is deprecated and should be replaced with '--dry-run=client'`)
return action.DryRunClient, nil return action.DryRunClient, nil
case string(action.DryRunClient): case string(action.DryRunClient):
return action.DryRunClient, nil return action.DryRunClient, nil
@ -77,7 +77,7 @@ func cmdGetDryRunFlagStrategy(cmd *cobra.Command, isTemplate bool) (action.DryRu
if b { if b {
result = action.DryRunClient result = action.DryRunClient
} }
slog.Warn(fmt.Sprintf(`boolean '--dry-run=%v' flag is deprecated and must be replaced with '--dry-run=%s'`, v, result)) logger.Warn(fmt.Sprintf(`boolean '--dry-run=%v' flag is deprecated and must be replaced with '--dry-run=%s'`, v, result))
return result, nil return result, nil
} }

@ -286,7 +286,7 @@ func TestCmdGetDryRunFlagStrategy(t *testing.T) {
cmd.Flags().Parse([]string{"helm", tc.DryRunFlagArg}) cmd.Flags().Parse([]string{"helm", tc.DryRunFlagArg})
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, tc.IsTemplate) dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, tc.IsTemplate, logger)
if tc.ExpectedError { if tc.ExpectedError {
assert.Error(t, err) assert.Error(t, err)
} else { } else {

@ -150,13 +150,13 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} }
client.SetRegistryClient(registryClient) client.SetRegistryClient(registryClient)
dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, false) dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, false, cfg.Logger())
if err != nil { if err != nil {
return err return err
} }
client.DryRunStrategy = dryRunStrategy client.DryRunStrategy = dryRunStrategy
rel, err := runInstall(args, client, valueOpts, out) rel, err := runInstall(args, client, valueOpts, out, cfg.Logger())
if err != nil { if err != nil {
return fmt.Errorf("INSTALLATION FAILED: %w", err) return fmt.Errorf("INSTALLATION FAILED: %w", err)
} }
@ -172,7 +172,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} }
f := cmd.Flags() f := cmd.Flags()
addInstallFlags(cmd, f, client, valueOpts) addInstallFlags(cmd, f, client, valueOpts, cfg.Logger())
// hide-secret is not available in all places the install flags are used so // hide-secret is not available in all places the install flags are used so
// it is added separately // it is added separately
f.BoolVar(&client.HideSecret, "hide-secret", false, "hide Kubernetes Secrets when also using the --dry-run flag") f.BoolVar(&client.HideSecret, "hide-secret", false, "hide Kubernetes Secrets when also using the --dry-run flag")
@ -183,7 +183,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return cmd return cmd
} }
func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Install, valueOpts *values.Options) { func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Install, valueOpts *values.Options, logger *slog.Logger) {
f.BoolVar(&client.CreateNamespace, "create-namespace", false, "create the release namespace if not present") f.BoolVar(&client.CreateNamespace, "create-namespace", false, "create the release namespace if not present")
f.BoolVar(&client.ForceReplace, "force-replace", false, "force resource updates by replacement") f.BoolVar(&client.ForceReplace, "force-replace", false, "force resource updates by replacement")
f.BoolVar(&client.ForceReplace, "force", false, "deprecated") f.BoolVar(&client.ForceReplace, "force", false, "deprecated")
@ -232,7 +232,7 @@ func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Instal
addValueOptionsFlags(f, valueOpts) addValueOptionsFlags(f, valueOpts)
addChartPathOptionsFlags(f, &client.ChartPathOptions) addChartPathOptionsFlags(f, &client.ChartPathOptions)
AddWaitFlag(cmd, &client.WaitStrategy) AddWaitFlag(cmd, &client.WaitStrategy, logger)
cmd.MarkFlagsMutuallyExclusive("force-replace", "force-conflicts") cmd.MarkFlagsMutuallyExclusive("force-replace", "force-conflicts")
cmd.MarkFlagsMutuallyExclusive("force", "force-conflicts") cmd.MarkFlagsMutuallyExclusive("force", "force-conflicts")
@ -251,10 +251,10 @@ func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Instal
} }
} }
func runInstall(args []string, client *action.Install, valueOpts *values.Options, out io.Writer) (*release.Release, error) { func runInstall(args []string, client *action.Install, valueOpts *values.Options, out io.Writer, logger *slog.Logger) (*release.Release, error) {
slog.Debug("Original chart version", "version", client.Version) logger.Debug("Original chart version", "version", client.Version)
if client.Version == "" && client.Devel { if client.Version == "" && client.Devel {
slog.Debug("setting version to >0.0.0-0") logger.Debug("setting version to >0.0.0-0")
client.Version = ">0.0.0-0" client.Version = ">0.0.0-0"
} }
@ -269,7 +269,7 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
return nil, err return nil, err
} }
slog.Debug("Chart path", "path", cp) logger.Debug("Chart path", "path", cp)
p := getter.All(settings) p := getter.All(settings)
vals, err := valueOpts.MergeValues(p) vals, err := valueOpts.MergeValues(p)
@ -293,7 +293,7 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
} }
if ac.Deprecated() { if ac.Deprecated() {
slog.Warn("this chart is deprecated") logger.Warn("this chart is deprecated")
} }
if req := ac.MetaDependencies(); len(req) > 0 { if req := ac.MetaDependencies(); len(req) > 0 {

@ -52,7 +52,7 @@ const (
// This follows a different pattern than the other commands because it has // This follows a different pattern than the other commands because it has
// to inspect its environment and then add commands to the base command // to inspect its environment and then add commands to the base command
// as it finds them. // as it finds them.
func loadCLIPlugins(baseCmd *cobra.Command, out io.Writer) { func loadCLIPlugins(baseCmd *cobra.Command, out io.Writer, logger *slog.Logger) {
// If HELM_NO_PLUGINS is set to 1, do not load plugins. // If HELM_NO_PLUGINS is set to 1, do not load plugins.
if os.Getenv("HELM_NO_PLUGINS") == "1" { if os.Getenv("HELM_NO_PLUGINS") == "1" {
return return
@ -64,7 +64,7 @@ func loadCLIPlugins(baseCmd *cobra.Command, out io.Writer) {
} }
found, err := plugin.FindPlugins(dirs, descriptor) found, err := plugin.FindPlugins(dirs, descriptor)
if err != nil { if err != nil {
slog.Error("failed to load plugins", slog.String("error", err.Error())) logger.Error("failed to load plugins", slog.String("error", err.Error()))
return return
} }
@ -136,7 +136,7 @@ func loadCLIPlugins(baseCmd *cobra.Command, out io.Writer) {
for _, cmd := range baseCmd.Commands() { for _, cmd := range baseCmd.Commands() {
if cmd.Name() == c.Name() { if cmd.Name() == c.Name() {
slog.Error("failed to load plugins: name conflicts", slog.String("name", c.Name())) logger.Error("failed to load plugins: name conflicts", slog.String("name", c.Name()))
return return
} }
} }
@ -151,7 +151,7 @@ func loadCLIPlugins(baseCmd *cobra.Command, out io.Writer) {
if (err == nil && if (err == nil &&
((subCmd.HasParent() && subCmd.Parent().Name() == "completion") || subCmd.Name() == cobra.ShellCompRequestCmd)) || ((subCmd.HasParent() && subCmd.Parent().Name() == "completion") || subCmd.Name() == cobra.ShellCompRequestCmd)) ||
/* for the tests */ subCmd == baseCmd.Root() { /* for the tests */ subCmd == baseCmd.Root() {
loadCompletionForPlugin(c, plug) loadCompletionForPlugin(c, plug, logger)
} }
} }
} }
@ -218,14 +218,14 @@ type pluginCommand struct {
// loadCompletionForPlugin will load and parse any completion.yaml provided by the plugin // loadCompletionForPlugin will load and parse any completion.yaml provided by the plugin
// and add the dynamic completion hook to call the optional plugin.complete // and add the dynamic completion hook to call the optional plugin.complete
func loadCompletionForPlugin(pluginCmd *cobra.Command, plug plugin.Plugin) { func loadCompletionForPlugin(pluginCmd *cobra.Command, plug plugin.Plugin, logger *slog.Logger) {
// Parse the yaml file providing the plugin's sub-commands and flags // Parse the yaml file providing the plugin's sub-commands and flags
cmds, err := loadFile(strings.Join( cmds, err := loadFile(strings.Join(
[]string{plug.Dir(), pluginStaticCompletionFile}, string(filepath.Separator))) []string{plug.Dir(), pluginStaticCompletionFile}, string(filepath.Separator)))
if err != nil { if err != nil {
// The file could be missing or invalid. No static completion for this plugin. // The file could be missing or invalid. No static completion for this plugin.
slog.Debug("plugin completion file loading", slog.String("error", err.Error())) logger.Debug("plugin completion file loading", slog.String("error", err.Error()))
// Continue to setup dynamic completion. // Continue to setup dynamic completion.
cmds = &pluginCommand{} cmds = &pluginCommand{}
} }
@ -233,18 +233,18 @@ func loadCompletionForPlugin(pluginCmd *cobra.Command, plug plugin.Plugin) {
// Preserve the Usage string specified for the plugin // Preserve the Usage string specified for the plugin
cmds.Name = pluginCmd.Use cmds.Name = pluginCmd.Use
addPluginCommands(plug, pluginCmd, cmds) addPluginCommands(plug, pluginCmd, cmds, logger)
} }
// addPluginCommands is a recursive method that adds each different level // addPluginCommands is a recursive method that adds each different level
// of sub-commands and flags for the plugins that have provided such information // of sub-commands and flags for the plugins that have provided such information
func addPluginCommands(plug plugin.Plugin, baseCmd *cobra.Command, cmds *pluginCommand) { func addPluginCommands(plug plugin.Plugin, baseCmd *cobra.Command, cmds *pluginCommand, logger *slog.Logger) {
if cmds == nil { if cmds == nil {
return return
} }
if len(cmds.Name) == 0 { if len(cmds.Name) == 0 {
slog.Debug("sub-command name field missing", slog.String("commandPath", baseCmd.CommandPath())) logger.Debug("sub-command name field missing", slog.String("commandPath", baseCmd.CommandPath()))
return return
} }
@ -313,7 +313,7 @@ func addPluginCommands(plug plugin.Plugin, baseCmd *cobra.Command, cmds *pluginC
Run: func(_ *cobra.Command, _ []string) {}, Run: func(_ *cobra.Command, _ []string) {},
} }
baseCmd.AddCommand(subCmd) baseCmd.AddCommand(subCmd)
addPluginCommands(plug, subCmd, &cmd) addPluginCommands(plug, subCmd, &cmd, logger)
} }
} }

@ -17,6 +17,7 @@ package cmd
import ( import (
"io" "io"
"log/slog"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -27,17 +28,17 @@ const pluginHelp = `
Manage client-side Helm plugins. Manage client-side Helm plugins.
` `
func newPluginCmd(out io.Writer) *cobra.Command { func newPluginCmd(out io.Writer, logger *slog.Logger) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "plugin", Use: "plugin",
Short: "install, list, or uninstall Helm plugins", Short: "install, list, or uninstall Helm plugins",
Long: pluginHelp, Long: pluginHelp,
} }
cmd.AddCommand( cmd.AddCommand(
newPluginInstallCmd(out), newPluginInstallCmd(out, logger),
newPluginListCmd(out), newPluginListCmd(out, logger),
newPluginUninstallCmd(out), newPluginUninstallCmd(out, logger),
newPluginUpdateCmd(out), newPluginUpdateCmd(out, logger),
newPluginPackageCmd(out), newPluginPackageCmd(out),
newPluginVerifyCmd(out), newPluginVerifyCmd(out),
) )

@ -45,6 +45,7 @@ type pluginInstallOptions struct {
plainHTTP bool plainHTTP bool
password string password string
username string username string
logger *slog.Logger
} }
const pluginInstallDesc = ` const pluginInstallDesc = `
@ -58,8 +59,8 @@ treated as "local dev" and do not require signatures.
Use --verify=false to explicitly skip signature verification (NOT recommended). Use --verify=false to explicitly skip signature verification (NOT recommended).
` `
func newPluginInstallCmd(out io.Writer) *cobra.Command { func newPluginInstallCmd(out io.Writer, logger *slog.Logger) *cobra.Command {
o := &pluginInstallOptions{} o := &pluginInstallOptions{logger: logger}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "install [options] <path|url>", Use: "install [options] <path|url>",
Short: "install a Helm plugin", Short: "install a Helm plugin",
@ -169,7 +170,7 @@ func (o *pluginInstallOptions) run(out io.Writer) error {
fmt.Fprintf(out, "Plugin Hash Verified: %s\n", verifyResult.FileHash) fmt.Fprintf(out, "Plugin Hash Verified: %s\n", verifyResult.FileHash)
} }
slog.Debug("loading plugin", "path", i.Path()) o.logger.Debug("loading plugin", "path", i.Path())
p, err := plugin.LoadDir(i.Path()) p, err := plugin.LoadDir(i.Path())
if err != nil { if err != nil {
return fmt.Errorf("plugin is installed but unusable: %w", err) return fmt.Errorf("plugin is installed but unusable: %w", err)

@ -29,7 +29,7 @@ import (
"helm.sh/helm/v4/internal/plugin/schema" "helm.sh/helm/v4/internal/plugin/schema"
) )
func newPluginListCmd(out io.Writer) *cobra.Command { func newPluginListCmd(out io.Writer, logger *slog.Logger) *cobra.Command {
var pluginType string var pluginType string
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "list", Use: "list",
@ -37,7 +37,7 @@ func newPluginListCmd(out io.Writer) *cobra.Command {
Short: "list installed Helm plugins", Short: "list installed Helm plugins",
ValidArgsFunction: noMoreArgsCompFunc, ValidArgsFunction: noMoreArgsCompFunc,
RunE: func(_ *cobra.Command, _ []string) error { RunE: func(_ *cobra.Command, _ []string) error {
slog.Debug("pluginDirs", "directory", settings.PluginsDirectory) logger.Debug("pluginDirs", "directory", settings.PluginsDirectory)
dirs := filepath.SplitList(settings.PluginsDirectory) dirs := filepath.SplitList(settings.PluginsDirectory)
descriptor := plugin.Descriptor{ descriptor := plugin.Descriptor{
Type: pluginType, Type: pluginType,

@ -18,6 +18,7 @@ package cmd
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"log/slog"
"os" "os"
"runtime" "runtime"
"strings" "strings"
@ -92,7 +93,7 @@ func TestLoadCLIPlugins(t *testing.T) {
out bytes.Buffer out bytes.Buffer
cmd cobra.Command cmd cobra.Command
) )
loadCLIPlugins(&cmd, &out) loadCLIPlugins(&cmd, &out, slog.Default())
fullEnvOutput := strings.Join([]string{ fullEnvOutput := strings.Join([]string{
"HELM_PLUGIN_NAME=fullenv", "HELM_PLUGIN_NAME=fullenv",
@ -171,7 +172,7 @@ func TestLoadPluginsWithSpace(t *testing.T) {
out bytes.Buffer out bytes.Buffer
cmd cobra.Command cmd cobra.Command
) )
loadCLIPlugins(&cmd, &out) loadCLIPlugins(&cmd, &out, slog.Default())
envs := strings.Join([]string{ envs := strings.Join([]string{
"fullenv", "fullenv",
@ -250,7 +251,7 @@ func TestLoadCLIPluginsForCompletion(t *testing.T) {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "completion", Use: "completion",
} }
loadCLIPlugins(cmd, &out) loadCLIPlugins(cmd, &out, slog.Default())
tests := []staticCompletionDetails{ tests := []staticCompletionDetails{
{"args", []string{}, []string{}, []staticCompletionDetails{}}, {"args", []string{}, []string{}, []staticCompletionDetails{}},
@ -343,7 +344,7 @@ func TestLoadCLIPlugins_HelmNoPlugins(t *testing.T) {
out := bytes.NewBuffer(nil) out := bytes.NewBuffer(nil)
cmd := &cobra.Command{} cmd := &cobra.Command{}
loadCLIPlugins(cmd, out) loadCLIPlugins(cmd, out, slog.Default())
plugins := cmd.Commands() plugins := cmd.Commands()
if len(plugins) != 0 { if len(plugins) != 0 {

@ -30,10 +30,11 @@ import (
type pluginUninstallOptions struct { type pluginUninstallOptions struct {
names []string names []string
logger *slog.Logger
} }
func newPluginUninstallCmd(out io.Writer) *cobra.Command { func newPluginUninstallCmd(out io.Writer, logger *slog.Logger) *cobra.Command {
o := &pluginUninstallOptions{} o := &pluginUninstallOptions{logger: logger}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "uninstall <plugin>...", Use: "uninstall <plugin>...",
@ -61,7 +62,7 @@ func (o *pluginUninstallOptions) complete(args []string) error {
} }
func (o *pluginUninstallOptions) run(out io.Writer) error { func (o *pluginUninstallOptions) run(out io.Writer) error {
slog.Debug("loading installer plugins", "dir", settings.PluginsDirectory) o.logger.Debug("loading installer plugins", "dir", settings.PluginsDirectory)
plugins, err := plugin.LoadAllDir(settings.PluginsDirectory, plugin.LogIgnorePluginLoadErrorFilterFunc) plugins, err := plugin.LoadAllDir(settings.PluginsDirectory, plugin.LogIgnorePluginLoadErrorFilterFunc)
if err != nil { if err != nil {
return err return err
@ -69,7 +70,7 @@ func (o *pluginUninstallOptions) run(out io.Writer) error {
var errorPlugins []error var errorPlugins []error
for _, name := range o.names { for _, name := range o.names {
if found := findPlugin(plugins, name); found != nil { if found := findPlugin(plugins, name); found != nil {
if err := uninstallPlugin(found); err != nil { if err := uninstallPlugin(found, o.logger); err != nil {
errorPlugins = append(errorPlugins, fmt.Errorf("failed to uninstall plugin %s, got error (%v)", name, err)) errorPlugins = append(errorPlugins, fmt.Errorf("failed to uninstall plugin %s, got error (%v)", name, err))
} else { } else {
fmt.Fprintf(out, "Uninstalled plugin: %s\n", name) fmt.Fprintf(out, "Uninstalled plugin: %s\n", name)
@ -84,7 +85,7 @@ func (o *pluginUninstallOptions) run(out io.Writer) error {
return nil return nil
} }
func uninstallPlugin(p plugin.Plugin) error { func uninstallPlugin(p plugin.Plugin, logger *slog.Logger) error {
if err := os.RemoveAll(p.Dir()); err != nil { if err := os.RemoveAll(p.Dir()); err != nil {
return err return err
} }
@ -102,18 +103,18 @@ func uninstallPlugin(p plugin.Plugin) error {
// Remove tarball file // Remove tarball file
tarballPath := filepath.Join(pluginsDir, versionedBasename) tarballPath := filepath.Join(pluginsDir, versionedBasename)
if _, err := os.Stat(tarballPath); err == nil { if _, err := os.Stat(tarballPath); err == nil {
slog.Debug("removing versioned tarball", "path", tarballPath) logger.Debug("removing versioned tarball", "path", tarballPath)
if err := os.Remove(tarballPath); err != nil { if err := os.Remove(tarballPath); err != nil {
slog.Debug("failed to remove tarball file", "path", tarballPath, "error", err) logger.Debug("failed to remove tarball file", "path", tarballPath, "error", err)
} }
} }
// Remove provenance file // Remove provenance file
provPath := filepath.Join(pluginsDir, versionedBasename+".prov") provPath := filepath.Join(pluginsDir, versionedBasename+".prov")
if _, err := os.Stat(provPath); err == nil { if _, err := os.Stat(provPath); err == nil {
slog.Debug("removing versioned provenance", "path", provPath) logger.Debug("removing versioned provenance", "path", provPath)
if err := os.Remove(provPath); err != nil { if err := os.Remove(provPath); err != nil {
slog.Debug("failed to remove provenance file", "path", provPath, "error", err) logger.Debug("failed to remove provenance file", "path", provPath, "error", err)
} }
} }
} }

@ -30,10 +30,11 @@ import (
type pluginUpdateOptions struct { type pluginUpdateOptions struct {
names []string names []string
logger *slog.Logger
} }
func newPluginUpdateCmd(out io.Writer) *cobra.Command { func newPluginUpdateCmd(out io.Writer, logger *slog.Logger) *cobra.Command {
o := &pluginUpdateOptions{} o := &pluginUpdateOptions{logger: logger}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "update <plugin>...", Use: "update <plugin>...",
@ -61,7 +62,7 @@ func (o *pluginUpdateOptions) complete(args []string) error {
} }
func (o *pluginUpdateOptions) run(out io.Writer) error { func (o *pluginUpdateOptions) run(out io.Writer) error {
slog.Debug("loading installed plugins", "path", settings.PluginsDirectory) o.logger.Debug("loading installed plugins", "path", settings.PluginsDirectory)
plugins, err := plugin.LoadAllDir(settings.PluginsDirectory, plugin.LogIgnorePluginLoadErrorFilterFunc) plugins, err := plugin.LoadAllDir(settings.PluginsDirectory, plugin.LogIgnorePluginLoadErrorFilterFunc)
if err != nil { if err != nil {
return err return err
@ -70,7 +71,7 @@ func (o *pluginUpdateOptions) run(out io.Writer) error {
for _, name := range o.names { for _, name := range o.names {
if found := findPlugin(plugins, name); found != nil { if found := findPlugin(plugins, name); found != nil {
if err := updatePlugin(found); err != nil { if err := updatePlugin(found, o.logger); err != nil {
errorPlugins = append(errorPlugins, fmt.Errorf("failed to update plugin %s, got error (%v)", name, err)) errorPlugins = append(errorPlugins, fmt.Errorf("failed to update plugin %s, got error (%v)", name, err))
} else { } else {
fmt.Fprintf(out, "Updated plugin: %s\n", name) fmt.Fprintf(out, "Updated plugin: %s\n", name)
@ -85,7 +86,7 @@ func (o *pluginUpdateOptions) run(out io.Writer) error {
return nil return nil
} }
func updatePlugin(p plugin.Plugin) error { func updatePlugin(p plugin.Plugin, logger *slog.Logger) error {
exactLocation, err := filepath.EvalSymlinks(p.Dir()) exactLocation, err := filepath.EvalSymlinks(p.Dir())
if err != nil { if err != nil {
return err return err
@ -103,7 +104,7 @@ func updatePlugin(p plugin.Plugin) error {
return err return err
} }
slog.Debug("loading plugin", "path", i.Path()) logger.Debug("loading plugin", "path", i.Path())
updatedPlugin, err := plugin.LoadDir(i.Path()) updatedPlugin, err := plugin.LoadDir(i.Path())
if err != nil { if err != nil {
return err return err

@ -20,7 +20,6 @@ import (
"fmt" "fmt"
"io" "io"
"log" "log"
"log/slog"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -61,7 +60,7 @@ func newPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
RunE: func(_ *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
client.Settings = settings client.Settings = settings
if client.Version == "" && client.Devel { if client.Version == "" && client.Devel {
slog.Debug("setting version to >0.0.0-0") cfg.Logger().Debug("setting version to >0.0.0-0")
client.Version = ">0.0.0-0" client.Version = ">0.0.0-0"
} }

@ -63,7 +63,7 @@ func newRegistryLoginCmd(cfg *action.Configuration, out io.Writer) *cobra.Comman
RunE: func(_ *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
hostname := args[0] hostname := args[0]
username, password, err := getUsernamePassword(o.username, o.password, o.passwordFromStdinOpt) username, password, err := getUsernamePassword(o.username, o.password, o.passwordFromStdinOpt, cfg.Logger())
if err != nil { if err != nil {
return err return err
} }
@ -91,7 +91,7 @@ func newRegistryLoginCmd(cfg *action.Configuration, out io.Writer) *cobra.Comman
} }
// Adapted from https://github.com/oras-project/oras // Adapted from https://github.com/oras-project/oras
func getUsernamePassword(usernameOpt string, passwordOpt string, passwordFromStdinOpt bool) (string, string, error) { func getUsernamePassword(usernameOpt string, passwordOpt string, passwordFromStdinOpt bool, logger *slog.Logger) (string, string, error) {
var err error var err error
username := usernameOpt username := usernameOpt
password := passwordOpt password := passwordOpt
@ -127,7 +127,7 @@ func getUsernamePassword(usernameOpt string, passwordOpt string, passwordFromStd
} }
} }
} else { } else {
slog.Warn("using --password via the CLI is insecure. Use --password-stdin") logger.Warn("using --password via the CLI is insecure. Use --password-stdin")
} }
return username, password, nil return username, password, nil

@ -66,7 +66,7 @@ func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client.Version = ver client.Version = ver
} }
dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, false) dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, false, cfg.Logger())
if err != nil { if err != nil {
return err return err
} }
@ -93,7 +93,7 @@ func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f.BoolVar(&client.CleanupOnFail, "cleanup-on-fail", false, "allow deletion of new resources created in this rollback when rollback fails") f.BoolVar(&client.CleanupOnFail, "cleanup-on-fail", false, "allow deletion of new resources created in this rollback when rollback fails")
f.IntVar(&client.MaxHistory, "history-max", settings.MaxHistory, "limit the maximum number of revisions saved per release. Use 0 for no limit") f.IntVar(&client.MaxHistory, "history-max", settings.MaxHistory, "limit the maximum number of revisions saved per release. Use 0 for no limit")
addDryRunFlag(cmd) addDryRunFlag(cmd)
AddWaitFlag(cmd, &client.WaitStrategy) AddWaitFlag(cmd, &client.WaitStrategy, cfg.Logger())
cmd.MarkFlagsMutuallyExclusive("force-replace", "force-conflicts") cmd.MarkFlagsMutuallyExclusive("force-replace", "force-conflicts")
cmd.MarkFlagsMutuallyExclusive("force", "force-conflicts") cmd.MarkFlagsMutuallyExclusive("force", "force-conflicts")

@ -102,7 +102,7 @@ By default, the default directories depend on the Operating System. The defaults
var settings = cli.New() var settings = cli.New()
func NewRootCmd(out io.Writer, args []string, logSetup func(bool)) (*cobra.Command, error) { func NewRootCmd(out io.Writer, args []string, logSetup func(bool) *slog.Logger) (*cobra.Command, error) {
actionConfig := action.NewConfiguration() actionConfig := action.NewConfiguration()
cmd, err := newRootCmdWithConfig(actionConfig, out, args, logSetup) cmd, err := newRootCmdWithConfig(actionConfig, out, args, logSetup)
if err != nil { if err != nil {
@ -121,16 +121,15 @@ func NewRootCmd(out io.Writer, args []string, logSetup func(bool)) (*cobra.Comma
return cmd, nil return cmd, nil
} }
// SetupLogging sets up Helm logging used by the Helm client. // SetupLogging creates the Helm logger used by the Helm client.
// This function is passed to the NewRootCmd function to enable logging. Any other // This function is passed to the NewRootCmd function to enable logging. Any other
// application that uses the NewRootCmd function to setup all the Helm commands may // application that uses the NewRootCmd function to setup all the Helm commands may
// use this function to setup logging or their own. Using a custom logging setup function // use this function to setup logging or their own. Using a custom logging setup function
// enables applications using Helm commands to integrate with their existing logging // enables applications using Helm commands to integrate with their existing logging
// system. // system.
// The debug argument is the value if Helm is set for debugging (i.e. --debug flag) // The debug argument is the value if Helm is set for debugging (i.e. --debug flag)
func SetupLogging(debug bool) { func SetupLogging(debug bool) *slog.Logger {
logger := logging.NewLogger(func() bool { return debug }) return logging.NewLogger(func() bool { return debug })
slog.SetDefault(logger)
} }
// configureColorOutput configures the color output based on the ColorMode setting // configureColorOutput configures the color output based on the ColorMode setting
@ -147,7 +146,7 @@ func configureColorOutput(settings *cli.EnvSettings) {
} }
} }
func newRootCmdWithConfig(actionConfig *action.Configuration, out io.Writer, args []string, logSetup func(bool)) (*cobra.Command, error) { func newRootCmdWithConfig(actionConfig *action.Configuration, out io.Writer, args []string, logSetup func(bool) *slog.Logger) (*cobra.Command, error) {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "helm", Use: "helm",
Short: "The Helm package manager for Kubernetes.", Short: "The Helm package manager for Kubernetes.",
@ -177,17 +176,8 @@ func newRootCmdWithConfig(actionConfig *action.Configuration, out io.Writer, arg
flags.ParseErrorsAllowlist.UnknownFlags = true flags.ParseErrorsAllowlist.UnknownFlags = true
flags.Parse(args) flags.Parse(args)
logSetup(settings.Debug) logger := logSetup(settings.Debug)
actionConfig.SetLogger(logger.Handler())
// newRootCmdWithConfig is only called from NewRootCmd. NewRootCmd sets up
// NewConfiguration without a custom logger. So, the slog default is used. logSetup
// can change the default logger to the one in the logger package. This happens for
// the Helm client. This means the actionConfig logger is different from the slog
// default logger. If they are different we sync the actionConfig logger to the slog
// current default one.
if actionConfig.Logger() != slog.Default() {
actionConfig.SetLogger(slog.Default().Handler())
}
// Validate color mode setting // Validate color mode setting
switch settings.ColorMode { switch settings.ColorMode {
@ -273,7 +263,7 @@ func newRootCmdWithConfig(actionConfig *action.Configuration, out io.Writer, arg
newLintCmd(out), newLintCmd(out),
newPackageCmd(out), newPackageCmd(out),
newRepoCmd(out), newRepoCmd(out),
newSearchCmd(out), newSearchCmd(out, logger),
newVerifyCmd(out), newVerifyCmd(out),
// release commands // release commands
@ -290,7 +280,7 @@ func newRootCmdWithConfig(actionConfig *action.Configuration, out io.Writer, arg
newCompletionCmd(out), newCompletionCmd(out),
newEnvCmd(out), newEnvCmd(out),
newPluginCmd(out), newPluginCmd(out, logger),
newVersionCmd(out), newVersionCmd(out),
// Hidden documentation generator command: 'helm docs' // Hidden documentation generator command: 'helm docs'
@ -303,7 +293,7 @@ func newRootCmdWithConfig(actionConfig *action.Configuration, out io.Writer, arg
) )
// Find and add CLI plugins // Find and add CLI plugins
loadCLIPlugins(cmd, out) loadCLIPlugins(cmd, out, logger)
// Check for expired repositories // Check for expired repositories
checkForExpiredRepos(settings.RepositoryConfig) checkForExpiredRepos(settings.RepositoryConfig)

@ -142,10 +142,37 @@ func TestRootCmdLogger(t *testing.T) {
t.Errorf("expected no error, got: '%v'", err) t.Errorf("expected no error, got: '%v'", err)
} }
l1 := actionConfig.Logger() l := actionConfig.Logger()
l2 := slog.Default()
if l1.Handler() != l2.Handler() { // The actionConfig logger should be set (not the discard handler)
t.Error("expected actionConfig logger to be the slog default logger") if l.Handler() == slog.New(slog.DiscardHandler).Handler() {
t.Error("expected actionConfig logger to be set, got discard handler")
}
// The actionConfig logger should NOT be the global default (we no longer set slog.SetDefault)
if l.Handler() == slog.Default().Handler() {
t.Error("expected actionConfig logger to NOT be the slog default logger")
}
}
func TestRootCmdCustomLogger(t *testing.T) {
args := []string{}
buf := new(bytes.Buffer)
actionConfig := action.NewConfiguration()
// SDK users should be able to inject a custom logger
customHandler := slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug})
customLogSetup := func(_ bool) *slog.Logger {
return slog.New(customHandler)
}
_, err := newRootCmdWithConfig(actionConfig, buf, args, customLogSetup)
if err != nil {
t.Errorf("expected no error, got: '%v'", err)
}
// Verify the custom handler was injected into actionConfig
if actionConfig.Logger().Handler() != customHandler {
t.Error("expected actionConfig logger to use the custom handler")
} }
} }

@ -18,6 +18,7 @@ package cmd
import ( import (
"io" "io"
"log/slog"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -28,7 +29,7 @@ they can be stored including the Artifact Hub and repositories you have added.
Use search subcommands to search different locations for charts. Use search subcommands to search different locations for charts.
` `
func newSearchCmd(out io.Writer) *cobra.Command { func newSearchCmd(out io.Writer, logger *slog.Logger) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "search [keyword]", Use: "search [keyword]",
@ -36,8 +37,8 @@ func newSearchCmd(out io.Writer) *cobra.Command {
Long: searchDesc, Long: searchDesc,
} }
cmd.AddCommand(newSearchHubCmd(out)) cmd.AddCommand(newSearchHubCmd(out, logger))
cmd.AddCommand(newSearchRepoCmd(out)) cmd.AddCommand(newSearchRepoCmd(out, logger))
return cmd return cmd
} }

@ -56,10 +56,11 @@ type searchHubOptions struct {
outputFormat output.Format outputFormat output.Format
listRepoURL bool listRepoURL bool
failOnNoResult bool failOnNoResult bool
logger *slog.Logger
} }
func newSearchHubCmd(out io.Writer) *cobra.Command { func newSearchHubCmd(out io.Writer, logger *slog.Logger) *cobra.Command {
o := &searchHubOptions{} o := &searchHubOptions{logger: logger}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "hub [KEYWORD]", Use: "hub [KEYWORD]",
@ -90,7 +91,7 @@ func (o *searchHubOptions) run(out io.Writer, args []string) error {
q := strings.Join(args, " ") q := strings.Join(args, " ")
results, err := c.Search(q) results, err := c.Search(q)
if err != nil { if err != nil {
slog.Debug("search failed", slog.Any("error", err)) o.logger.Debug("search failed", slog.Any("error", err))
return fmt.Errorf("unable to perform search against %q", o.searchEndpoint) return fmt.Errorf("unable to perform search against %q", o.searchEndpoint)
} }

@ -73,10 +73,11 @@ type searchRepoOptions struct {
repoCacheDir string repoCacheDir string
outputFormat output.Format outputFormat output.Format
failOnNoResult bool failOnNoResult bool
logger *slog.Logger
} }
func newSearchRepoCmd(out io.Writer) *cobra.Command { func newSearchRepoCmd(out io.Writer, logger *slog.Logger) *cobra.Command {
o := &searchRepoOptions{} o := &searchRepoOptions{logger: logger}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "repo [keyword]", Use: "repo [keyword]",
@ -131,17 +132,17 @@ func (o *searchRepoOptions) run(out io.Writer, args []string) error {
} }
func (o *searchRepoOptions) setupSearchedVersion() { func (o *searchRepoOptions) setupSearchedVersion() {
slog.Debug("original chart version", "version", o.version) o.logger.Debug("original chart version", "version", o.version)
if o.version != "" { if o.version != "" {
return return
} }
if o.devel { // search for releases and prereleases (alpha, beta, and release candidate releases). if o.devel { // search for releases and prereleases (alpha, beta, and release candidate releases).
slog.Debug("setting version to >0.0.0-0") o.logger.Debug("setting version to >0.0.0-0")
o.version = ">0.0.0-0" o.version = ">0.0.0-0"
} else { // search only for stable releases, prerelease versions will be skipped } else { // search only for stable releases, prerelease versions will be skipped
slog.Debug("setting version to >0.0.0") o.logger.Debug("setting version to >0.0.0")
o.version = ">0.0.0" o.version = ">0.0.0"
} }
} }
@ -190,7 +191,7 @@ func (o *searchRepoOptions) buildIndex() (*search.Index, error) {
f := filepath.Join(o.repoCacheDir, helmpath.CacheIndexFile(n)) f := filepath.Join(o.repoCacheDir, helmpath.CacheIndexFile(n))
ind, err := repo.LoadIndexFile(f) ind, err := repo.LoadIndexFile(f)
if err != nil { if err != nil {
slog.Warn("repo is corrupt or missing", slog.String("repo", n), slog.Any("error", err)) o.logger.Warn("repo is corrupt or missing", slog.String("repo", n), slog.Any("error", err))
continue continue
} }

@ -88,7 +88,7 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
if err != nil { if err != nil {
return err return err
} }
output, err := runShow(args, client) output, err := runShow(args, client, cfg.Logger())
if err != nil { if err != nil {
return err return err
} }
@ -109,7 +109,7 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
if err != nil { if err != nil {
return err return err
} }
output, err := runShow(args, client) output, err := runShow(args, client, cfg.Logger())
if err != nil { if err != nil {
return err return err
} }
@ -130,7 +130,7 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
if err != nil { if err != nil {
return err return err
} }
output, err := runShow(args, client) output, err := runShow(args, client, cfg.Logger())
if err != nil { if err != nil {
return err return err
} }
@ -151,7 +151,7 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
if err != nil { if err != nil {
return err return err
} }
output, err := runShow(args, client) output, err := runShow(args, client, cfg.Logger())
if err != nil { if err != nil {
return err return err
} }
@ -172,7 +172,7 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
if err != nil { if err != nil {
return err return err
} }
output, err := runShow(args, client) output, err := runShow(args, client, cfg.Logger())
if err != nil { if err != nil {
return err return err
} }
@ -211,10 +211,10 @@ func addShowFlags(subCmd *cobra.Command, client *action.Show) {
} }
} }
func runShow(args []string, client *action.Show) (string, error) { func runShow(args []string, client *action.Show, logger *slog.Logger) (string, error) {
slog.Debug("original chart version", "version", client.Version) logger.Debug("original chart version", "version", client.Version)
if client.Version == "" && client.Devel { if client.Version == "" && client.Devel {
slog.Debug("setting version to >0.0.0-0") logger.Debug("setting version to >0.0.0-0")
client.Version = ">0.0.0-0" client.Version = ">0.0.0-0"
} }

@ -92,7 +92,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} }
client.SetRegistryClient(registryClient) client.SetRegistryClient(registryClient)
dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, true) dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, true, cfg.Logger())
if err != nil { if err != nil {
return err return err
} }
@ -105,7 +105,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client.Replace = true // Skip the name check client.Replace = true // Skip the name check
client.APIVersions = common.VersionSet(extraAPIs) client.APIVersions = common.VersionSet(extraAPIs)
client.IncludeCRDs = includeCrds client.IncludeCRDs = includeCrds
rel, err := runInstall(args, client, valueOpts, out) rel, err := runInstall(args, client, valueOpts, out, cfg.Logger())
if err != nil && !settings.Debug { if err != nil && !settings.Debug {
if rel != nil { if rel != nil {
@ -203,7 +203,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} }
f := cmd.Flags() f := cmd.Flags()
addInstallFlags(cmd, f, client, valueOpts) addInstallFlags(cmd, f, client, valueOpts, cfg.Logger())
f.StringArrayVarP(&showFiles, "show-only", "s", []string{}, "only show manifests rendered from the given templates") f.StringArrayVarP(&showFiles, "show-only", "s", []string{}, "only show manifests rendered from the given templates")
f.StringVar(&client.OutputDir, "output-dir", "", "writes the executed templates to files in output-dir instead of stdout") f.StringVar(&client.OutputDir, "output-dir", "", "writes the executed templates to files in output-dir instead of stdout")
f.BoolVar(&validate, "validate", false, "deprecated") f.BoolVar(&validate, "validate", false, "deprecated")

@ -82,7 +82,7 @@ func newUninstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f.StringVar(&client.DeletionPropagation, "cascade", "background", "Must be \"background\", \"orphan\", or \"foreground\". Selects the deletion cascading strategy for the dependents. Defaults to background. Use \"foreground\" with --wait to ensure resources with finalizers are fully deleted before returning.") f.StringVar(&client.DeletionPropagation, "cascade", "background", "Must be \"background\", \"orphan\", or \"foreground\". Selects the deletion cascading strategy for the dependents. Defaults to background. Use \"foreground\" with --wait to ensure resources with finalizers are fully deleted before returning.")
f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)")
f.StringVar(&client.Description, "description", "", "add a custom description") f.StringVar(&client.Description, "description", "", "add a custom description")
AddWaitFlag(cmd, &client.WaitStrategy) AddWaitFlag(cmd, &client.WaitStrategy, cfg.Logger())
return cmd return cmd
} }

@ -112,7 +112,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} }
client.SetRegistryClient(registryClient) client.SetRegistryClient(registryClient)
dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, false) dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, false, cfg.Logger())
if err != nil { if err != nil {
return err return err
} }
@ -125,7 +125,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
histClient := action.NewHistory(cfg) histClient := action.NewHistory(cfg)
histClient.Max = 1 histClient.Max = 1
versions, err := histClient.Run(args[0]) versions, err := histClient.Run(args[0])
if errors.Is(err, driver.ErrReleaseNotFound) || isReleaseUninstalled(versions) { if errors.Is(err, driver.ErrReleaseNotFound) || isReleaseUninstalled(versions, cfg.Logger()) {
// Only print this to stdout for table output // Only print this to stdout for table output
if outfmt == output.Table { if outfmt == output.Table {
fmt.Fprintf(out, "Release %q does not exist. Installing it now.\n", args[0]) fmt.Fprintf(out, "Release %q does not exist. Installing it now.\n", args[0])
@ -157,11 +157,11 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
instClient.ForceConflicts = client.ForceConflicts instClient.ForceConflicts = client.ForceConflicts
instClient.ServerSideApply = client.ServerSideApply != "false" instClient.ServerSideApply = client.ServerSideApply != "false"
if isReleaseUninstalled(versions) { if isReleaseUninstalled(versions, cfg.Logger()) {
instClient.Replace = true instClient.Replace = true
} }
rel, err := runInstall(args, instClient, valueOpts, out) rel, err := runInstall(args, instClient, valueOpts, out, cfg.Logger())
if err != nil { if err != nil {
return err return err
} }
@ -178,7 +178,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} }
if client.Version == "" && client.Devel { if client.Version == "" && client.Devel {
slog.Debug("setting version to >0.0.0-0") cfg.Logger().Debug("setting version to >0.0.0-0")
client.Version = ">0.0.0-0" client.Version = ">0.0.0-0"
} }
@ -232,7 +232,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} }
if ac.Deprecated() { if ac.Deprecated() {
slog.Warn("this chart is deprecated") cfg.Logger().Warn("this chart is deprecated")
} }
// Create context and prepare the handle of SIGTERM // Create context and prepare the handle of SIGTERM
@ -305,7 +305,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
addValueOptionsFlags(f, valueOpts) addValueOptionsFlags(f, valueOpts)
bindOutputFlag(cmd, &outfmt) bindOutputFlag(cmd, &outfmt)
bindPostRenderFlag(cmd, &client.PostRenderer, settings) bindPostRenderFlag(cmd, &client.PostRenderer, settings)
AddWaitFlag(cmd, &client.WaitStrategy) AddWaitFlag(cmd, &client.WaitStrategy, cfg.Logger())
cmd.MarkFlagsMutuallyExclusive("force-replace", "force-conflicts") cmd.MarkFlagsMutuallyExclusive("force-replace", "force-conflicts")
cmd.MarkFlagsMutuallyExclusive("force", "force-conflicts") cmd.MarkFlagsMutuallyExclusive("force", "force-conflicts")
@ -322,10 +322,10 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return cmd return cmd
} }
func isReleaseUninstalled(versionsi []ri.Releaser) bool { func isReleaseUninstalled(versionsi []ri.Releaser, logger *slog.Logger) bool {
versions, err := releaseListToV1List(versionsi) versions, err := releaseListToV1List(versionsi)
if err != nil { if err != nil {
slog.Error("cannot convert release list to v1 release list", "error", err) logger.Error("cannot convert release list to v1 release list", "error", err)
return false return false
} }
return len(versions) > 0 && versions[len(versions)-1].Info.Status == common.StatusUninstalled return len(versions) > 0 && versions[len(versions)-1].Info.Status == common.StatusUninstalled

@ -83,14 +83,14 @@ func (o *versionOptions) run(out io.Writer) error {
if err != nil { if err != nil {
return err return err
} }
return tt.Execute(out, version.Get()) return tt.Execute(out, version.Get(nil))
} }
fmt.Fprintln(out, formatVersion(o.short)) fmt.Fprintln(out, formatVersion(o.short))
return nil return nil
} }
func formatVersion(short bool) string { func formatVersion(short bool) string {
v := version.Get() v := version.Get(nil)
if short { if short {
if len(v.GitCommit) >= 7 { if len(v.GitCommit) >= 7 {
return fmt.Sprintf("%s+g%s", v.Version, v.GitCommit[:7]) return fmt.Sprintf("%s+g%s", v.Version, v.GitCommit[:7])

@ -50,6 +50,14 @@ var CacheProv = ".prov"
// DiskCache is a cache that stores data on disk. // DiskCache is a cache that stores data on disk.
type DiskCache struct { type DiskCache struct {
Root string Root string
Logger *slog.Logger
}
func (c *DiskCache) log() *slog.Logger {
if c.Logger != nil {
return c.Logger
}
return slog.New(slog.DiscardHandler)
} }
// Get returns a reader for the given key. // Get returns a reader for the given key.
@ -79,7 +87,7 @@ func (c *DiskCache) Put(key [sha256.Size]byte, data io.Reader, cacheType string)
// TODO: verify the key and digest of the key are the same. // TODO: verify the key and digest of the key are the same.
p := c.fileName(key, cacheType) p := c.fileName(key, cacheType)
if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil { if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil {
slog.Error("failed to create cache directory") c.log().Error("failed to create cache directory")
return p, err return p, err
} }
return p, fileutil.AtomicWriteFile(p, data, 0644) return p, fileutil.AtomicWriteFile(p, data, 0644)

@ -85,6 +85,16 @@ type ChartDownloader struct {
// Cache specifies the cache implementation to use. // Cache specifies the cache implementation to use.
Cache Cache Cache Cache
// Logger is the structured logger for this downloader instance.
Logger *slog.Logger
}
func (c *ChartDownloader) log() *slog.Logger {
if c.Logger != nil {
return c.Logger
}
return slog.New(slog.DiscardHandler)
} }
// DownloadTo retrieves a chart. Depending on the settings, it may also download a provenance file. // DownloadTo retrieves a chart. Depending on the settings, it may also download a provenance file.
@ -104,7 +114,7 @@ func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *proven
return "", nil, errors.New("content cache must be set") return "", nil, errors.New("content cache must be set")
} }
c.Cache = &DiskCache{Root: c.ContentCache} c.Cache = &DiskCache{Root: c.ContentCache}
slog.Debug("set up default downloader cache") c.log().Debug("set up default downloader cache")
} }
hash, u, err := c.ResolveChartVersion(ref, version) hash, u, err := c.ResolveChartVersion(ref, version)
if err != nil { if err != nil {
@ -140,7 +150,7 @@ func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *proven
if err == nil { if err == nil {
found = true found = true
data = bytes.NewBuffer(fdata) data = bytes.NewBuffer(fdata)
slog.Debug("found chart in cache", "id", hash) c.log().Debug("found chart in cache", "id", hash)
} }
} }
} }
@ -180,7 +190,7 @@ func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *proven
if err == nil { if err == nil {
found = true found = true
body = bytes.NewBuffer(fdata) body = bytes.NewBuffer(fdata)
slog.Debug("found provenance in cache", "id", hash) c.log().Debug("found provenance in cache", "id", hash)
} }
} }
} }
@ -220,7 +230,7 @@ func (c *ChartDownloader) DownloadToCache(ref, version string) (string, *provena
return "", nil, errors.New("content cache must be set") return "", nil, errors.New("content cache must be set")
} }
c.Cache = &DiskCache{Root: c.ContentCache} c.Cache = &DiskCache{Root: c.ContentCache}
slog.Debug("set up default downloader cache") c.log().Debug("set up default downloader cache")
} }
digestString, u, err := c.ResolveChartVersion(ref, version) digestString, u, err := c.ResolveChartVersion(ref, version)
@ -252,11 +262,11 @@ func (c *ChartDownloader) DownloadToCache(ref, version string) (string, *provena
if len(digest) > 0 { if len(digest) > 0 {
pth, err = c.Cache.Get(digest32, CacheChart) pth, err = c.Cache.Get(digest32, CacheChart)
if err == nil { if err == nil {
slog.Debug("found chart in cache", "id", digestString) c.log().Debug("found chart in cache", "id", digestString)
} }
} }
if len(digest) == 0 || err != nil { if len(digest) == 0 || err != nil {
slog.Debug("attempting to download chart", "ref", ref, "version", version) c.log().Debug("attempting to download chart", "ref", ref, "version", version)
if err != nil && !os.IsNotExist(err) { if err != nil && !os.IsNotExist(err) {
return "", nil, err return "", nil, err
} }
@ -276,7 +286,7 @@ func (c *ChartDownloader) DownloadToCache(ref, version string) (string, *provena
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }
slog.Debug("put downloaded chart in cache", "id", hex.EncodeToString(digest32[:])) c.log().Debug("put downloaded chart in cache", "id", hex.EncodeToString(digest32[:]))
} }
// If provenance is requested, verify it. // If provenance is requested, verify it.
@ -285,7 +295,7 @@ func (c *ChartDownloader) DownloadToCache(ref, version string) (string, *provena
ppth, err := c.Cache.Get(digest32, CacheProv) ppth, err := c.Cache.Get(digest32, CacheProv)
if err == nil { if err == nil {
slog.Debug("found provenance in cache", "id", digestString) c.log().Debug("found provenance in cache", "id", digestString)
} else { } else {
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
return pth, ver, err return pth, ver, err
@ -304,7 +314,7 @@ func (c *ChartDownloader) DownloadToCache(ref, version string) (string, *provena
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }
slog.Debug("put downloaded provenance file in cache", "id", hex.EncodeToString(digest32[:])) c.log().Debug("put downloaded provenance file in cache", "id", hex.EncodeToString(digest32[:]))
} }
if c.Verify != VerifyLater { if c.Verify != VerifyLater {

@ -47,6 +47,15 @@ type Engine struct {
EnableDNS bool EnableDNS bool
// CustomTemplateFuncs is defined by users to provide custom template funcs // CustomTemplateFuncs is defined by users to provide custom template funcs
CustomTemplateFuncs template.FuncMap CustomTemplateFuncs template.FuncMap
// logger is the structured logger for this engine instance
logger *slog.Logger
}
func (e Engine) log() *slog.Logger {
if e.logger != nil {
return e.logger
}
return slog.New(slog.DiscardHandler)
} }
// New creates a new instance of Engine using the passed in rest config. // New creates a new instance of Engine using the passed in rest config.
@ -77,7 +86,7 @@ func New(config *rest.Config) Engine {
// section contains a value named "bar", that value will be passed on to the // section contains a value named "bar", that value will be passed on to the
// bar chart during render time. // bar chart during render time.
func (e Engine) Render(chrt ci.Charter, values common.Values) (map[string]string, error) { func (e Engine) Render(chrt ci.Charter, values common.Values) (map[string]string, error) {
tmap := allTemplates(chrt, values) tmap := allTemplates(chrt, values, e.log())
return e.render(tmap) return e.render(tmap)
} }
@ -208,7 +217,7 @@ func (e Engine) initFunMap(t *template.Template) {
if val == nil { if val == nil {
if e.LintMode { if e.LintMode {
// Don't fail on missing required values when linting // Don't fail on missing required values when linting
slog.Warn("missing required value", "message", warn) e.log().Warn("missing required value", "message", warn)
return "", nil return "", nil
} }
return val, errors.New(warnWrap(warn)) return val, errors.New(warnWrap(warn))
@ -216,7 +225,7 @@ func (e Engine) initFunMap(t *template.Template) {
if val == "" { if val == "" {
if e.LintMode { if e.LintMode {
// Don't fail on missing required values when linting // Don't fail on missing required values when linting
slog.Warn("missing required values", "message", warn) e.log().Warn("missing required values", "message", warn)
return "", nil return "", nil
} }
return val, errors.New(warnWrap(warn)) return val, errors.New(warnWrap(warn))
@ -229,7 +238,7 @@ func (e Engine) initFunMap(t *template.Template) {
funcMap["fail"] = func(msg string) (string, error) { funcMap["fail"] = func(msg string) (string, error) {
if e.LintMode { if e.LintMode {
// Don't fail when linting // Don't fail when linting
slog.Info("funcMap fail", "message", msg) e.log().Info("funcMap fail", "message", msg)
return "", nil return "", nil
} }
return "", errors.New(warnWrap(msg)) return "", errors.New(warnWrap(msg))
@ -238,7 +247,7 @@ func (e Engine) initFunMap(t *template.Template) {
// If we are not linting and have a cluster connection, provide a Kubernetes-backed // If we are not linting and have a cluster connection, provide a Kubernetes-backed
// implementation. // implementation.
if !e.LintMode && e.clientProvider != nil { if !e.LintMode && e.clientProvider != nil {
funcMap["lookup"] = newLookupFunction(*e.clientProvider) funcMap["lookup"] = newLookupFunction(*e.clientProvider, e.log())
} }
// When DNS lookups are not enabled override the sprig function and return // When DNS lookups are not enabled override the sprig function and return
@ -521,9 +530,9 @@ func (p byPathLen) Less(i, j int) bool {
// allTemplates returns all templates for a chart and its dependencies. // allTemplates returns all templates for a chart and its dependencies.
// //
// As it goes, it also prepares the values in a scope-sensitive manner. // As it goes, it also prepares the values in a scope-sensitive manner.
func allTemplates(c ci.Charter, vals common.Values) map[string]renderable { func allTemplates(c ci.Charter, vals common.Values, logger *slog.Logger) map[string]renderable {
templates := make(map[string]renderable) templates := make(map[string]renderable)
recAllTpls(c, templates, vals) recAllTpls(c, templates, vals, logger)
return templates return templates
} }
@ -531,12 +540,12 @@ func allTemplates(c ci.Charter, vals common.Values) map[string]renderable {
// //
// As it recurses, it also sets the values to be appropriate for the template // As it recurses, it also sets the values to be appropriate for the template
// scope. // scope.
func recAllTpls(c ci.Charter, templates map[string]renderable, values common.Values) map[string]any { func recAllTpls(c ci.Charter, templates map[string]renderable, values common.Values, logger *slog.Logger) map[string]any {
vals := values.AsMap() vals := values.AsMap()
subCharts := make(map[string]any) subCharts := make(map[string]any)
accessor, err := ci.NewAccessor(c) accessor, err := ci.NewAccessor(c)
if err != nil { if err != nil {
slog.Error("error accessing chart", "error", err) logger.Error("error accessing chart", "error", err)
} }
chartMetaData := accessor.MetadataAsMap() chartMetaData := accessor.MetadataAsMap()
chartMetaData["IsRoot"] = accessor.IsRoot() chartMetaData["IsRoot"] = accessor.IsRoot()
@ -561,7 +570,7 @@ func recAllTpls(c ci.Charter, templates map[string]renderable, values common.Val
for _, child := range accessor.Dependencies() { for _, child := range accessor.Dependencies() {
// TODO: Handle error // TODO: Handle error
sub, _ := ci.NewAccessor(child) sub, _ := ci.NewAccessor(child)
subCharts[sub.Name()] = recAllTpls(child, templates, next) subCharts[sub.Name()] = recAllTpls(child, templates, next, logger)
} }
newParentID := accessor.ChartFullPath() newParentID := accessor.ChartFullPath()

@ -19,6 +19,7 @@ package engine
import ( import (
"errors" "errors"
"fmt" "fmt"
"log/slog"
"path" "path"
"strings" "strings"
"sync" "sync"
@ -590,7 +591,7 @@ func TestAllTemplates(t *testing.T) {
} }
dep1.AddDependency(dep2) dep1.AddDependency(dep2)
tpls := allTemplates(ch1, common.Values{}) tpls := allTemplates(ch1, common.Values{}, slog.New(slog.DiscardHandler))
if len(tpls) != 5 { if len(tpls) != 5 {
t.Errorf("Expected 5 charts, got %d", len(tpls)) t.Errorf("Expected 5 charts, got %d", len(tpls))
} }

@ -36,7 +36,7 @@ type lookupFunc = func(apiversion string, resource string, namespace string, nam
// //
// If the resource does not exist, no error is raised. // If the resource does not exist, no error is raised.
func NewLookupFunction(config *rest.Config) lookupFunc { //nolint:revive func NewLookupFunction(config *rest.Config) lookupFunc { //nolint:revive
return newLookupFunction(clientProviderFromConfig{config: config}) return newLookupFunction(clientProviderFromConfig{config: config}, slog.New(slog.DiscardHandler))
} }
type ClientProvider interface { type ClientProvider interface {
@ -51,10 +51,10 @@ type clientProviderFromConfig struct {
} }
func (c clientProviderFromConfig) GetClientFor(apiVersion, kind string) (dynamic.NamespaceableResourceInterface, bool, error) { func (c clientProviderFromConfig) GetClientFor(apiVersion, kind string) (dynamic.NamespaceableResourceInterface, bool, error) {
return getDynamicClientOnKind(apiVersion, kind, c.config) return getDynamicClientOnKind(apiVersion, kind, c.config, slog.New(slog.DiscardHandler))
} }
func newLookupFunction(clientProvider ClientProvider) lookupFunc { func newLookupFunction(clientProvider ClientProvider, logger *slog.Logger) lookupFunc {
return func(apiversion string, kind string, namespace string, name string) (map[string]any, error) { return func(apiversion string, kind string, namespace string, name string) (map[string]any, error) {
var client dynamic.ResourceInterface var client dynamic.ResourceInterface
c, namespaced, err := clientProvider.GetClientFor(apiversion, kind) c, namespaced, err := clientProvider.GetClientFor(apiversion, kind)
@ -94,11 +94,11 @@ func newLookupFunction(clientProvider ClientProvider) lookupFunc {
} }
// getDynamicClientOnKind returns a dynamic client on an Unstructured type. This client can be further namespaced. // getDynamicClientOnKind returns a dynamic client on an Unstructured type. This client can be further namespaced.
func getDynamicClientOnKind(apiversion string, kind string, config *rest.Config) (dynamic.NamespaceableResourceInterface, bool, error) { func getDynamicClientOnKind(apiversion string, kind string, config *rest.Config, logger *slog.Logger) (dynamic.NamespaceableResourceInterface, bool, error) {
gvk := schema.FromAPIVersionAndKind(apiversion, kind) gvk := schema.FromAPIVersionAndKind(apiversion, kind)
apiRes, err := getAPIResourceForGVK(gvk, config) apiRes, err := getAPIResourceForGVK(gvk, config, logger)
if err != nil { if err != nil {
slog.Error( logger.Error(
"unable to get apiresource", "unable to get apiresource",
slog.String("groupVersionKind", gvk.String()), slog.String("groupVersionKind", gvk.String()),
slog.Any("error", err), slog.Any("error", err),
@ -112,23 +112,23 @@ func getDynamicClientOnKind(apiversion string, kind string, config *rest.Config)
} }
intf, err := dynamic.NewForConfig(config) intf, err := dynamic.NewForConfig(config)
if err != nil { if err != nil {
slog.Error("unable to get dynamic client", slog.Any("error", err)) logger.Error("unable to get dynamic client", slog.Any("error", err))
return nil, false, err return nil, false, err
} }
res := intf.Resource(gvr) res := intf.Resource(gvr)
return res, apiRes.Namespaced, nil return res, apiRes.Namespaced, nil
} }
func getAPIResourceForGVK(gvk schema.GroupVersionKind, config *rest.Config) (metav1.APIResource, error) { func getAPIResourceForGVK(gvk schema.GroupVersionKind, config *rest.Config, logger *slog.Logger) (metav1.APIResource, error) {
res := metav1.APIResource{} res := metav1.APIResource{}
discoveryClient, err := discovery.NewDiscoveryClientForConfig(config) discoveryClient, err := discovery.NewDiscoveryClientForConfig(config)
if err != nil { if err != nil {
slog.Error("unable to create discovery client", slog.Any("error", err)) logger.Error("unable to create discovery client", slog.Any("error", err))
return res, err return res, err
} }
resList, err := discoveryClient.ServerResourcesForGroupVersion(gvk.GroupVersion().String()) resList, err := discoveryClient.ServerResourcesForGroupVersion(gvk.GroupVersion().String())
if err != nil { if err != nil {
slog.Error( logger.Error(
"unable to retrieve resource list", "unable to retrieve resource list",
slog.String("GroupVersion", gvk.GroupVersion().String()), slog.String("GroupVersion", gvk.GroupVersion().String()),
slog.Any("error", err), slog.Any("error", err),

@ -36,6 +36,14 @@ const HelmIgnore = ".helmignore"
// Empty() will create an immutable empty ruleset. // Empty() will create an immutable empty ruleset.
type Rules struct { type Rules struct {
patterns []*pattern patterns []*pattern
logger *slog.Logger
}
func (r *Rules) log() *slog.Logger {
if r.logger != nil {
return r.logger
}
return slog.New(slog.DiscardHandler)
} }
// Empty builds an empty ruleset. // Empty builds an empty ruleset.
@ -101,7 +109,7 @@ func (r *Rules) Ignore(path string, fi os.FileInfo) bool {
} }
for _, p := range r.patterns { for _, p := range r.patterns {
if p.match == nil { if p.match == nil {
slog.Info("this will be ignored no matcher supplied", "patterns", p.raw) r.log().Info("this will be ignored no matcher supplied", "patterns", p.raw)
return false return false
} }
@ -176,7 +184,7 @@ func (r *Rules) parseRule(rule string) error {
rule = after rule = after
ok, err := filepath.Match(rule, n) ok, err := filepath.Match(rule, n)
if err != nil { if err != nil {
slog.Error("failed to compile", slog.String("rule", rule), slog.Any("error", err)) r.log().Error("failed to compile", slog.String("rule", rule), slog.Any("error", err))
return false return false
} }
return ok return ok
@ -186,7 +194,7 @@ func (r *Rules) parseRule(rule string) error {
p.match = func(n string, _ os.FileInfo) bool { p.match = func(n string, _ os.FileInfo) bool {
ok, err := filepath.Match(rule, n) ok, err := filepath.Match(rule, n)
if err != nil { if err != nil {
slog.Error( r.log().Error(
"failed to compile", "failed to compile",
slog.String("rule", rule), slog.String("rule", rule),
slog.Any("error", err), slog.Any("error", err),
@ -202,7 +210,7 @@ func (r *Rules) parseRule(rule string) error {
n = filepath.Base(n) n = filepath.Base(n)
ok, err := filepath.Match(rule, n) ok, err := filepath.Match(rule, n)
if err != nil { if err != nil {
slog.Error("failed to compile", slog.String("rule", rule), slog.Any("error", err)) r.log().Error("failed to compile", slog.String("rule", rule), slog.Any("error", err))
return false return false
} }
return ok return ok

@ -191,7 +191,7 @@ func (c *Client) GetWaiterWithOptions(strategy WaitStrategy, opts ...WaitOption)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &legacyWaiter{kubeClient: kc, ctx: c.WaitContext}, nil return &legacyWaiter{kubeClient: kc, ctx: c.WaitContext, logger: c.Logger()}, nil
case StatusWatcherStrategy: case StatusWatcherStrategy:
return c.newStatusWatcher(opts...) return c.newStatusWatcher(opts...)
case HookOnlyStrategy: case HookOnlyStrategy:
@ -226,11 +226,9 @@ func New(getter genericclioptions.RESTClientGetter) *Client {
getter = genericclioptions.NewConfigFlags(true) getter = genericclioptions.NewConfigFlags(true)
} }
factory := cmdutil.NewFactory(getter) factory := cmdutil.NewFactory(getter)
c := &Client{ return &Client{
Factory: factory, Factory: factory,
} }
c.SetLogger(slog.Default().Handler())
return c
} }
// getKubeClient get or create a new KubernetesClientSet // getKubeClient get or create a new KubernetesClientSet
@ -601,7 +599,7 @@ func (c *Client) update(originals, targets ResourceList, createApplyFunc CreateA
if original == nil { if original == nil {
kind := target.Mapping.GroupVersionKind.Kind kind := target.Mapping.GroupVersionKind.Kind
slog.Warn("resource exists on cluster but not in original release, using cluster state as baseline", c.Logger().Warn("resource exists on cluster but not in original release, using cluster state as baseline",
"namespace", target.Namespace, "name", target.Name, "kind", kind) "namespace", target.Namespace, "name", target.Name, "kind", kind)
currentObj, err := helper.Get(target.Namespace, target.Name) currentObj, err := helper.Get(target.Namespace, target.Name)
@ -890,7 +888,7 @@ func (c *Client) Update(originals, targets ResourceList, options ...ClientUpdate
c.Logger().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 func(original, target *resource.Info) error {
return patchResourceClientSide(original.Object, target, updateOptions.threeWayMergeForUnstructured) return patchResourceClientSide(original.Object, target, updateOptions.threeWayMergeForUnstructured, c.Logger())
} }
} }
@ -1120,7 +1118,7 @@ func replaceResource(target *resource.Info, fieldValidationDirective FieldValida
} }
func patchResourceClientSide(original runtime.Object, target *resource.Info, threeWayMergeForUnstructured bool) error { func patchResourceClientSide(original runtime.Object, target *resource.Info, threeWayMergeForUnstructured bool, logger *slog.Logger) error {
patch, patchType, err := createPatch(original, target, threeWayMergeForUnstructured) patch, patchType, err := createPatch(original, target, threeWayMergeForUnstructured)
if err != nil { if err != nil {
@ -1129,7 +1127,7 @@ func patchResourceClientSide(original runtime.Object, target *resource.Info, thr
kind := target.Mapping.GroupVersionKind.Kind kind := target.Mapping.GroupVersionKind.Kind
if patch == nil || string(patch) == "{}" { if patch == nil || string(patch) == "{}" {
slog.Debug("no changes detected", "kind", kind, "name", target.Name) logger.Debug("no changes detected", "kind", kind, "name", target.Name)
// This needs to happen to make sure that Helm has the latest info from the API // This needs to happen to make sure that Helm has the latest info from the API
// Otherwise there will be no labels and other functions that use labels will panic // Otherwise there will be no labels and other functions that use labels will panic
if err := target.Get(); err != nil { if err := target.Get(); err != nil {
@ -1139,7 +1137,7 @@ func patchResourceClientSide(original runtime.Object, target *resource.Info, thr
} }
// send patch to server // send patch to server
slog.Debug("patching resource", "kind", kind, "name", target.Name, "namespace", target.Namespace) logger.Debug("patching resource", "kind", kind, "name", target.Name, "namespace", target.Namespace)
helper := resource.NewHelper(target.Client, target.Mapping).WithFieldManager(getManagedFieldsManager()) helper := resource.NewHelper(target.Client, target.Mapping).WithFieldManager(getManagedFieldsManager())
obj, err := helper.Patch(target.Namespace, target.Name, patchType, patch, nil) obj, err := helper.Patch(target.Namespace, target.Name, patchType, patch, nil)
if err != nil { if err != nil {

@ -73,6 +73,22 @@ type ReadyChecker struct {
client kubernetes.Interface client kubernetes.Interface
checkJobs bool checkJobs bool
pausedAsReady bool pausedAsReady bool
logger *slog.Logger
}
// log returns the configured logger or a discard logger if none is set.
func (c *ReadyChecker) log() *slog.Logger {
if c.logger != nil {
return c.logger
}
return slog.New(slog.DiscardHandler)
}
// WithLogger returns a ReadyCheckerOption that sets the logger.
func WithLogger(logger *slog.Logger) ReadyCheckerOption {
return func(c *ReadyChecker) {
c.logger = logger
}
} }
// IsReady checks if v is ready. It supports checking readiness for pods, // IsReady checks if v is ready. It supports checking readiness for pods,
@ -226,21 +242,21 @@ func (c *ReadyChecker) isPodReady(pod *corev1.Pod) bool {
return true return true
} }
} }
slog.Debug("Pod is not ready", "namespace", pod.GetNamespace(), "name", pod.GetName()) c.log().Debug("Pod is not ready", "namespace", pod.GetNamespace(), "name", pod.GetName())
return false return false
} }
func (c *ReadyChecker) jobReady(job *batchv1.Job) (bool, error) { func (c *ReadyChecker) jobReady(job *batchv1.Job) (bool, error) {
if job.Status.Failed > *job.Spec.BackoffLimit { if job.Status.Failed > *job.Spec.BackoffLimit {
slog.Debug("Job is failed", "namespace", job.GetNamespace(), "name", job.GetName()) c.log().Debug("Job is failed", "namespace", job.GetNamespace(), "name", job.GetName())
// If a job is failed, it can't recover, so throw an error // If a job is failed, it can't recover, so throw an error
return false, fmt.Errorf("job is failed: %s/%s", job.GetNamespace(), job.GetName()) return false, fmt.Errorf("job is failed: %s/%s", job.GetNamespace(), job.GetName())
} }
if job.Spec.Completions != nil && job.Status.Succeeded < *job.Spec.Completions { if job.Spec.Completions != nil && job.Status.Succeeded < *job.Spec.Completions {
slog.Debug("Job is not completed", "namespace", job.GetNamespace(), "name", job.GetName()) c.log().Debug("Job is not completed", "namespace", job.GetNamespace(), "name", job.GetName())
return false, nil return false, nil
} }
slog.Debug("Job is completed", "namespace", job.GetNamespace(), "name", job.GetName()) c.log().Debug("Job is completed", "namespace", job.GetNamespace(), "name", job.GetName())
return true, nil return true, nil
} }
@ -252,7 +268,7 @@ func (c *ReadyChecker) serviceReady(s *corev1.Service) bool {
// Ensure that the service cluster IP is not empty // Ensure that the service cluster IP is not empty
if s.Spec.ClusterIP == "" { if s.Spec.ClusterIP == "" {
slog.Debug("Service does not have cluster IP address", "namespace", s.GetNamespace(), "name", s.GetName()) c.log().Debug("Service does not have cluster IP address", "namespace", s.GetNamespace(), "name", s.GetName())
return false return false
} }
@ -260,25 +276,25 @@ func (c *ReadyChecker) serviceReady(s *corev1.Service) bool {
if s.Spec.Type == corev1.ServiceTypeLoadBalancer { if s.Spec.Type == corev1.ServiceTypeLoadBalancer {
// do not wait when at least 1 external IP is set // do not wait when at least 1 external IP is set
if len(s.Spec.ExternalIPs) > 0 { if len(s.Spec.ExternalIPs) > 0 {
slog.Debug("Service has external IP addresses", "namespace", s.GetNamespace(), "name", s.GetName(), "externalIPs", s.Spec.ExternalIPs) c.log().Debug("Service has external IP addresses", "namespace", s.GetNamespace(), "name", s.GetName(), "externalIPs", s.Spec.ExternalIPs)
return true return true
} }
if s.Status.LoadBalancer.Ingress == nil { if s.Status.LoadBalancer.Ingress == nil {
slog.Debug("Service does not have load balancer ingress IP address", "namespace", s.GetNamespace(), "name", s.GetName()) c.log().Debug("Service does not have load balancer ingress IP address", "namespace", s.GetNamespace(), "name", s.GetName())
return false return false
} }
} }
slog.Debug("Service is ready", "namespace", s.GetNamespace(), "name", s.GetName(), "clusterIP", s.Spec.ClusterIP, "externalIPs", s.Spec.ExternalIPs) c.log().Debug("Service is ready", "namespace", s.GetNamespace(), "name", s.GetName(), "clusterIP", s.Spec.ClusterIP, "externalIPs", s.Spec.ExternalIPs)
return true return true
} }
func (c *ReadyChecker) volumeReady(v *corev1.PersistentVolumeClaim) bool { func (c *ReadyChecker) volumeReady(v *corev1.PersistentVolumeClaim) bool {
if v.Status.Phase != corev1.ClaimBound { if v.Status.Phase != corev1.ClaimBound {
slog.Debug("PersistentVolumeClaim is not bound", "namespace", v.GetNamespace(), "name", v.GetName()) c.log().Debug("PersistentVolumeClaim is not bound", "namespace", v.GetNamespace(), "name", v.GetName())
return false return false
} }
slog.Debug("PersistentVolumeClaim is bound", "namespace", v.GetNamespace(), "name", v.GetName(), "phase", v.Status.Phase) c.log().Debug("PersistentVolumeClaim is bound", "namespace", v.GetNamespace(), "name", v.GetName(), "phase", v.Status.Phase)
return true return true
} }
@ -289,23 +305,23 @@ func (c *ReadyChecker) deploymentReady(rs *appsv1.ReplicaSet, dep *appsv1.Deploy
} }
// Verify the generation observed by the deployment controller matches the spec generation // Verify the generation observed by the deployment controller matches the spec generation
if dep.Status.ObservedGeneration != dep.Generation { if dep.Status.ObservedGeneration != dep.Generation {
slog.Debug("Deployment is not ready, observedGeneration does not match spec generation", "namespace", dep.GetNamespace(), "name", dep.GetName(), "actualGeneration", dep.Status.ObservedGeneration, "expectedGeneration", dep.Generation) c.log().Debug("Deployment is not ready, observedGeneration does not match spec generation", "namespace", dep.GetNamespace(), "name", dep.GetName(), "actualGeneration", dep.Status.ObservedGeneration, "expectedGeneration", dep.Generation)
return false return false
} }
expectedReady := *dep.Spec.Replicas - deploymentutil.MaxUnavailable(*dep) expectedReady := *dep.Spec.Replicas - deploymentutil.MaxUnavailable(*dep)
if rs.Status.ReadyReplicas < expectedReady { if rs.Status.ReadyReplicas < expectedReady {
slog.Debug("Deployment does not have enough pods ready", "namespace", dep.GetNamespace(), "name", dep.GetName(), "readyPods", rs.Status.ReadyReplicas, "totalPods", expectedReady) c.log().Debug("Deployment does not have enough pods ready", "namespace", dep.GetNamespace(), "name", dep.GetName(), "readyPods", rs.Status.ReadyReplicas, "totalPods", expectedReady)
return false return false
} }
slog.Debug("Deployment is ready", "namespace", dep.GetNamespace(), "name", dep.GetName(), "readyPods", rs.Status.ReadyReplicas, "totalPods", expectedReady) c.log().Debug("Deployment is ready", "namespace", dep.GetNamespace(), "name", dep.GetName(), "readyPods", rs.Status.ReadyReplicas, "totalPods", expectedReady)
return true return true
} }
func (c *ReadyChecker) daemonSetReady(ds *appsv1.DaemonSet) bool { func (c *ReadyChecker) daemonSetReady(ds *appsv1.DaemonSet) bool {
// Verify the generation observed by the daemonSet controller matches the spec generation // Verify the generation observed by the daemonSet controller matches the spec generation
if ds.Status.ObservedGeneration != ds.Generation { if ds.Status.ObservedGeneration != ds.Generation {
slog.Debug("DaemonSet is not ready, observedGeneration does not match spec generation", "namespace", ds.GetNamespace(), "name", ds.GetName(), "observedGeneration", ds.Status.ObservedGeneration, "expectedGeneration", ds.Generation) c.log().Debug("DaemonSet is not ready, observedGeneration does not match spec generation", "namespace", ds.GetNamespace(), "name", ds.GetName(), "observedGeneration", ds.Status.ObservedGeneration, "expectedGeneration", ds.Generation)
return false return false
} }
@ -316,7 +332,7 @@ func (c *ReadyChecker) daemonSetReady(ds *appsv1.DaemonSet) bool {
// Make sure all the updated pods have been scheduled // Make sure all the updated pods have been scheduled
if ds.Status.UpdatedNumberScheduled != ds.Status.DesiredNumberScheduled { if ds.Status.UpdatedNumberScheduled != ds.Status.DesiredNumberScheduled {
slog.Debug("DaemonSet does not have enough Pods scheduled", "namespace", ds.GetNamespace(), "name", ds.GetName(), "scheduledPods", ds.Status.UpdatedNumberScheduled, "totalPods", ds.Status.DesiredNumberScheduled) c.log().Debug("DaemonSet does not have enough Pods scheduled", "namespace", ds.GetNamespace(), "name", ds.GetName(), "scheduledPods", ds.Status.UpdatedNumberScheduled, "totalPods", ds.Status.DesiredNumberScheduled)
return false return false
} }
maxUnavailable, err := intstr.GetScaledValueFromIntOrPercent(ds.Spec.UpdateStrategy.RollingUpdate.MaxUnavailable, int(ds.Status.DesiredNumberScheduled), true) maxUnavailable, err := intstr.GetScaledValueFromIntOrPercent(ds.Spec.UpdateStrategy.RollingUpdate.MaxUnavailable, int(ds.Status.DesiredNumberScheduled), true)
@ -329,10 +345,10 @@ func (c *ReadyChecker) daemonSetReady(ds *appsv1.DaemonSet) bool {
expectedReady := int(ds.Status.DesiredNumberScheduled) - maxUnavailable expectedReady := int(ds.Status.DesiredNumberScheduled) - maxUnavailable
if int(ds.Status.NumberReady) < expectedReady { if int(ds.Status.NumberReady) < expectedReady {
slog.Debug("DaemonSet does not have enough Pods ready", "namespace", ds.GetNamespace(), "name", ds.GetName(), "readyPods", ds.Status.NumberReady, "totalPods", expectedReady) c.log().Debug("DaemonSet does not have enough Pods ready", "namespace", ds.GetNamespace(), "name", ds.GetName(), "readyPods", ds.Status.NumberReady, "totalPods", expectedReady)
return false return false
} }
slog.Debug("DaemonSet is ready", "namespace", ds.GetNamespace(), "name", ds.GetName(), "readyPods", ds.Status.NumberReady, "totalPods", expectedReady) c.log().Debug("DaemonSet is ready", "namespace", ds.GetNamespace(), "name", ds.GetName(), "readyPods", ds.Status.NumberReady, "totalPods", expectedReady)
return true return true
} }
@ -386,13 +402,13 @@ func (c *ReadyChecker) crdReady(crd apiextv1.CustomResourceDefinition) bool {
func (c *ReadyChecker) statefulSetReady(sts *appsv1.StatefulSet) bool { func (c *ReadyChecker) statefulSetReady(sts *appsv1.StatefulSet) bool {
// Verify the generation observed by the statefulSet controller matches the spec generation // Verify the generation observed by the statefulSet controller matches the spec generation
if sts.Status.ObservedGeneration != sts.Generation { if sts.Status.ObservedGeneration != sts.Generation {
slog.Debug("StatefulSet is not ready, observedGeneration doest not match spec generation", "namespace", sts.GetNamespace(), "name", sts.GetName(), "actualGeneration", sts.Status.ObservedGeneration, "expectedGeneration", sts.Generation) c.log().Debug("StatefulSet is not ready, observedGeneration doest not match spec generation", "namespace", sts.GetNamespace(), "name", sts.GetName(), "actualGeneration", sts.Status.ObservedGeneration, "expectedGeneration", sts.Generation)
return false return false
} }
// If the update strategy is not a rolling update, there will be nothing to wait for // If the update strategy is not a rolling update, there will be nothing to wait for
if sts.Spec.UpdateStrategy.Type != appsv1.RollingUpdateStatefulSetStrategyType { if sts.Spec.UpdateStrategy.Type != appsv1.RollingUpdateStatefulSetStrategyType {
slog.Debug("StatefulSet skipped ready check", "namespace", sts.GetNamespace(), "name", sts.GetName(), "updateStrategy", sts.Spec.UpdateStrategy.Type) c.log().Debug("StatefulSet skipped ready check", "namespace", sts.GetNamespace(), "name", sts.GetName(), "updateStrategy", sts.Spec.UpdateStrategy.Type)
return true return true
} }
@ -418,29 +434,29 @@ func (c *ReadyChecker) statefulSetReady(sts *appsv1.StatefulSet) bool {
// Make sure all the updated pods have been scheduled // Make sure all the updated pods have been scheduled
if int(sts.Status.UpdatedReplicas) < expectedReplicas { if int(sts.Status.UpdatedReplicas) < expectedReplicas {
slog.Debug("StatefulSet does not have enough Pods scheduled", "namespace", sts.GetNamespace(), "name", sts.GetName(), "readyPods", sts.Status.UpdatedReplicas, "totalPods", expectedReplicas) c.log().Debug("StatefulSet does not have enough Pods scheduled", "namespace", sts.GetNamespace(), "name", sts.GetName(), "readyPods", sts.Status.UpdatedReplicas, "totalPods", expectedReplicas)
return false return false
} }
if int(sts.Status.ReadyReplicas) != replicas { if int(sts.Status.ReadyReplicas) != replicas {
slog.Debug("StatefulSet does not have enough Pods ready", "namespace", sts.GetNamespace(), "name", sts.GetName(), "readyPods", sts.Status.ReadyReplicas, "totalPods", replicas) c.log().Debug("StatefulSet does not have enough Pods ready", "namespace", sts.GetNamespace(), "name", sts.GetName(), "readyPods", sts.Status.ReadyReplicas, "totalPods", replicas)
return false return false
} }
// This check only makes sense when all partitions are being upgraded otherwise during a // This check only makes sense when all partitions are being upgraded otherwise during a
// partitioned rolling upgrade, this condition will never evaluate to true, leading to // partitioned rolling upgrade, this condition will never evaluate to true, leading to
// error. // error.
if partition == 0 && sts.Status.CurrentRevision != sts.Status.UpdateRevision { if partition == 0 && sts.Status.CurrentRevision != sts.Status.UpdateRevision {
slog.Debug("StatefulSet is not ready, currentRevision does not match updateRevision", "namespace", sts.GetNamespace(), "name", sts.GetName(), "currentRevision", sts.Status.CurrentRevision, "updateRevision", sts.Status.UpdateRevision) c.log().Debug("StatefulSet is not ready, currentRevision does not match updateRevision", "namespace", sts.GetNamespace(), "name", sts.GetName(), "currentRevision", sts.Status.CurrentRevision, "updateRevision", sts.Status.UpdateRevision)
return false return false
} }
slog.Debug("StatefulSet is ready", "namespace", sts.GetNamespace(), "name", sts.GetName(), "readyPods", sts.Status.ReadyReplicas, "totalPods", replicas) c.log().Debug("StatefulSet is ready", "namespace", sts.GetNamespace(), "name", sts.GetName(), "readyPods", sts.Status.ReadyReplicas, "totalPods", replicas)
return true return true
} }
func (c *ReadyChecker) replicationControllerReady(rc *corev1.ReplicationController) bool { func (c *ReadyChecker) replicationControllerReady(rc *corev1.ReplicationController) bool {
// Verify the generation observed by the replicationController controller matches the spec generation // Verify the generation observed by the replicationController controller matches the spec generation
if rc.Status.ObservedGeneration != rc.Generation { if rc.Status.ObservedGeneration != rc.Generation {
slog.Debug("ReplicationController is not ready, observedGeneration doest not match spec generation", "namespace", rc.GetNamespace(), "name", rc.GetName(), "actualGeneration", rc.Status.ObservedGeneration, "expectedGeneration", rc.Generation) c.log().Debug("ReplicationController is not ready, observedGeneration doest not match spec generation", "namespace", rc.GetNamespace(), "name", rc.GetName(), "actualGeneration", rc.Status.ObservedGeneration, "expectedGeneration", rc.Generation)
return false return false
} }
return true return true
@ -449,7 +465,7 @@ func (c *ReadyChecker) replicationControllerReady(rc *corev1.ReplicationControll
func (c *ReadyChecker) replicaSetReady(rs *appsv1.ReplicaSet) bool { func (c *ReadyChecker) replicaSetReady(rs *appsv1.ReplicaSet) bool {
// Verify the generation observed by the replicaSet controller matches the spec generation // Verify the generation observed by the replicaSet controller matches the spec generation
if rs.Status.ObservedGeneration != rs.Generation { if rs.Status.ObservedGeneration != rs.Generation {
slog.Debug("ReplicaSet is not ready, observedGeneration doest not match spec generation", "namespace", rs.GetNamespace(), "name", rs.GetName(), "actualGeneration", rs.Status.ObservedGeneration, "expectedGeneration", rs.Generation) c.log().Debug("ReplicaSet is not ready, observedGeneration doest not match spec generation", "namespace", rs.GetNamespace(), "name", rs.GetName(), "actualGeneration", rs.Status.ObservedGeneration, "expectedGeneration", rs.Generation)
return false return false
} }
return true return true

@ -51,22 +51,31 @@ type legacyWaiter struct {
c ReadyChecker c ReadyChecker
kubeClient *kubernetes.Clientset kubeClient *kubernetes.Clientset
ctx context.Context ctx context.Context
logger *slog.Logger
}
// log returns the configured logger or a discard logger if none is set.
func (hw *legacyWaiter) log() *slog.Logger {
if hw.logger != nil {
return hw.logger
}
return slog.New(slog.DiscardHandler)
} }
func (hw *legacyWaiter) Wait(resources ResourceList, timeout time.Duration) error { func (hw *legacyWaiter) Wait(resources ResourceList, timeout time.Duration) error {
hw.c = NewReadyChecker(hw.kubeClient, PausedAsReady(true)) hw.c = NewReadyChecker(hw.kubeClient, PausedAsReady(true), WithLogger(hw.log()))
return hw.waitForResources(resources, timeout) return hw.waitForResources(resources, timeout)
} }
func (hw *legacyWaiter) WaitWithJobs(resources ResourceList, timeout time.Duration) error { func (hw *legacyWaiter) WaitWithJobs(resources ResourceList, timeout time.Duration) error {
hw.c = NewReadyChecker(hw.kubeClient, PausedAsReady(true), CheckJobs(true)) hw.c = NewReadyChecker(hw.kubeClient, PausedAsReady(true), CheckJobs(true), WithLogger(hw.log()))
return hw.waitForResources(resources, timeout) return hw.waitForResources(resources, timeout)
} }
// waitForResources polls to get the current status of all pods, PVCs, Services and // waitForResources polls to get the current status of all pods, PVCs, Services and
// Jobs(optional) until all are ready or a timeout is reached // Jobs(optional) until all are ready or a timeout is reached
func (hw *legacyWaiter) waitForResources(created ResourceList, timeout time.Duration) error { func (hw *legacyWaiter) waitForResources(created ResourceList, timeout time.Duration) error {
slog.Debug("beginning wait for resources", "count", len(created), "timeout", timeout) hw.log().Debug("beginning wait for resources", "count", len(created), "timeout", timeout)
ctx, cancel := hw.contextWithTimeout(timeout) ctx, cancel := hw.contextWithTimeout(timeout)
defer cancel() defer cancel()
@ -84,10 +93,10 @@ func (hw *legacyWaiter) waitForResources(created ResourceList, timeout time.Dura
if waitRetries > 0 && hw.isRetryableError(err, v) { if waitRetries > 0 && hw.isRetryableError(err, v) {
numberOfErrors[i]++ numberOfErrors[i]++
if numberOfErrors[i] > waitRetries { if numberOfErrors[i] > waitRetries {
slog.Debug("max number of retries reached", "resource", v.Name, "retries", numberOfErrors[i]) hw.log().Debug("max number of retries reached", "resource", v.Name, "retries", numberOfErrors[i])
return false, err return false, err
} }
slog.Debug("retrying resource readiness", "resource", v.Name, "currentRetries", numberOfErrors[i]-1, "maxRetries", waitRetries) hw.log().Debug("retrying resource readiness", "resource", v.Name, "currentRetries", numberOfErrors[i]-1, "maxRetries", waitRetries)
return false, nil return false, nil
} }
numberOfErrors[i] = 0 numberOfErrors[i] = 0
@ -103,7 +112,7 @@ func (hw *legacyWaiter) isRetryableError(err error, resource *resource.Info) boo
if err == nil { if err == nil {
return false return false
} }
slog.Debug( hw.log().Debug(
"error received when checking resource status", "error received when checking resource status",
slog.String("resource", resource.Name), slog.String("resource", resource.Name),
slog.Any("error", err), slog.Any("error", err),
@ -112,7 +121,7 @@ func (hw *legacyWaiter) isRetryableError(err error, resource *resource.Info) boo
if errors.As(err, &ev) { if errors.As(err, &ev) {
statusCode := ev.Status().Code statusCode := ev.Status().Code
retryable := hw.isRetryableHTTPStatusCode(statusCode) retryable := hw.isRetryableHTTPStatusCode(statusCode)
slog.Debug( hw.log().Debug(
"status code received", "status code received",
slog.String("resource", resource.Name), slog.String("resource", resource.Name),
slog.Int("statusCode", int(statusCode)), slog.Int("statusCode", int(statusCode)),
@ -120,7 +129,7 @@ func (hw *legacyWaiter) isRetryableError(err error, resource *resource.Info) boo
) )
return retryable return retryable
} }
slog.Debug("retryable error assumed", "resource", resource.Name) hw.log().Debug("retryable error assumed", "resource", resource.Name)
return true return true
} }
@ -130,7 +139,7 @@ func (hw *legacyWaiter) isRetryableHTTPStatusCode(httpStatusCode int32) bool {
// WaitForDelete polls to check if all the resources are deleted or a timeout is reached // WaitForDelete polls to check if all the resources are deleted or a timeout is reached
func (hw *legacyWaiter) WaitForDelete(deleted ResourceList, timeout time.Duration) error { func (hw *legacyWaiter) WaitForDelete(deleted ResourceList, timeout time.Duration) error {
slog.Debug("beginning wait for resources to be deleted", "count", len(deleted), "timeout", timeout) hw.log().Debug("beginning wait for resources to be deleted", "count", len(deleted), "timeout", timeout)
startTime := time.Now() startTime := time.Now()
ctx, cancel := hw.contextWithTimeout(timeout) ctx, cancel := hw.contextWithTimeout(timeout)
@ -148,9 +157,9 @@ func (hw *legacyWaiter) WaitForDelete(deleted ResourceList, timeout time.Duratio
elapsed := time.Since(startTime).Round(time.Second) elapsed := time.Since(startTime).Round(time.Second)
if err != nil { if err != nil {
slog.Debug("wait for resources failed", slog.Duration("elapsed", elapsed), slog.Any("error", err)) hw.log().Debug("wait for resources failed", slog.Duration("elapsed", elapsed), slog.Any("error", err))
} else { } else {
slog.Debug("wait for resources succeeded", slog.Duration("elapsed", elapsed)) hw.log().Debug("wait for resources succeeded", slog.Duration("elapsed", elapsed))
} }
return err return err
@ -242,7 +251,7 @@ func (hw *legacyWaiter) watchUntilReady(timeout time.Duration, info *resource.In
return nil return nil
} }
slog.Debug("watching for resource changes", "kind", kind, "resource", info.Name, "timeout", timeout) hw.log().Debug("watching for resource changes", "kind", kind, "resource", info.Name, "timeout", timeout)
// Use a selector on the name of the resource. This should be unique for the // Use a selector on the name of the resource. This should be unique for the
// given version and kind // given version and kind
@ -270,7 +279,7 @@ func (hw *legacyWaiter) watchUntilReady(timeout time.Duration, info *resource.In
// we get. We care mostly about jobs, where what we want to see is // we get. We care mostly about jobs, where what we want to see is
// the status go into a good state. For other types, like ReplicaSet // the status go into a good state. For other types, like ReplicaSet
// we don't really do anything to support these as hooks. // we don't really do anything to support these as hooks.
slog.Debug("add/modify event received", "resource", info.Name, "eventType", e.Type) hw.log().Debug("add/modify event received", "resource", info.Name, "eventType", e.Type)
switch kind { switch kind {
case "Job": case "Job":
@ -280,11 +289,11 @@ func (hw *legacyWaiter) watchUntilReady(timeout time.Duration, info *resource.In
} }
return true, nil return true, nil
case watch.Deleted: case watch.Deleted:
slog.Debug("deleted event received", "resource", info.Name) hw.log().Debug("deleted event received", "resource", info.Name)
return true, nil return true, nil
case watch.Error: case watch.Error:
// Handle error and return with an error. // Handle error and return with an error.
slog.Error("error event received", "resource", info.Name) hw.log().Error("error event received", "resource", info.Name)
return true, fmt.Errorf("failed to deploy %s", info.Name) return true, fmt.Errorf("failed to deploy %s", info.Name)
default: default:
return false, nil return false, nil
@ -306,12 +315,12 @@ func (hw *legacyWaiter) waitForJob(obj runtime.Object, name string) (bool, error
if c.Type == batchv1.JobComplete && c.Status == "True" { if c.Type == batchv1.JobComplete && c.Status == "True" {
return true, nil return true, nil
} else if c.Type == batchv1.JobFailed && c.Status == "True" { } else if c.Type == batchv1.JobFailed && c.Status == "True" {
slog.Error("job failed", "job", name, "reason", c.Reason) hw.log().Error("job failed", "job", name, "reason", c.Reason)
return true, fmt.Errorf("job %s failed: %s", name, c.Reason) return true, fmt.Errorf("job %s failed: %s", name, c.Reason)
} }
} }
slog.Debug("job status update", "job", name, "active", o.Status.Active, "failed", o.Status.Failed, "succeeded", o.Status.Succeeded) hw.log().Debug("job status update", "job", name, "active", o.Status.Active, "failed", o.Status.Failed, "succeeded", o.Status.Succeeded)
return false, nil return false, nil
} }
@ -326,17 +335,17 @@ func (hw *legacyWaiter) waitForPodSuccess(obj runtime.Object, name string) (bool
switch o.Status.Phase { switch o.Status.Phase {
case corev1.PodSucceeded: case corev1.PodSucceeded:
slog.Debug("pod succeeded", "pod", o.Name) hw.log().Debug("pod succeeded", "pod", o.Name)
return true, nil return true, nil
case corev1.PodFailed: case corev1.PodFailed:
slog.Error("pod failed", "pod", o.Name) hw.log().Error("pod failed", "pod", o.Name)
return true, fmt.Errorf("pod %s failed", o.Name) return true, fmt.Errorf("pod %s failed", o.Name)
case corev1.PodPending: case corev1.PodPending:
slog.Debug("pod pending", "pod", o.Name) hw.log().Debug("pod pending", "pod", o.Name)
case corev1.PodRunning: case corev1.PodRunning:
slog.Debug("pod running", "pod", o.Name) hw.log().Debug("pod running", "pod", o.Name)
case corev1.PodUnknown: case corev1.PodUnknown:
slog.Debug("pod unknown", "pod", o.Name) hw.log().Debug("pod unknown", "pod", o.Name)
} }
return false, nil return false, nil

@ -76,6 +76,7 @@ type (
credentialsStore credentials.Store credentialsStore credentials.Store
httpClient *http.Client httpClient *http.Client
plainHTTP bool plainHTTP bool
logger *slog.Logger
} }
// ClientOption allows specifying various settings configurable by the user for overriding the defaults // ClientOption allows specifying various settings configurable by the user for overriding the defaults
@ -217,6 +218,20 @@ func ClientOptPlainHTTP() ClientOption {
} }
} }
// ClientOptLogger returns a function that sets the logger on client options set
func ClientOptLogger(logger *slog.Logger) ClientOption {
return func(client *Client) {
client.logger = logger
}
}
func (c *Client) log() *slog.Logger {
if c.logger != nil {
return c.logger
}
return slog.New(slog.DiscardHandler)
}
type ( type (
// LoginOption allows specifying various settings on login // LoginOption allows specifying various settings on login
LoginOption func(*loginOperation) LoginOption func(*loginOperation)
@ -229,10 +244,10 @@ type (
// warnIfHostHasPath checks if the host contains a repository path and logs a warning if it does. // warnIfHostHasPath checks if the host contains a repository path and logs a warning if it does.
// Returns true if the host contains a path component (i.e., contains a '/'). // Returns true if the host contains a path component (i.e., contains a '/').
func warnIfHostHasPath(host string) bool { func warnIfHostHasPath(host string, logger *slog.Logger) bool {
if strings.Contains(host, "/") { if strings.Contains(host, "/") {
registryHost := strings.Split(host, "/")[0] registryHost := strings.Split(host, "/")[0]
slog.Warn("registry login currently only supports registry hostname, not a repository path", "host", host, "suggested", registryHost) logger.Warn("registry login currently only supports registry hostname, not a repository path", "host", host, "suggested", registryHost)
return true return true
} }
return false return false
@ -244,7 +259,7 @@ func (c *Client) Login(host string, options ...LoginOption) error {
option(&loginOperation{host, c}) option(&loginOperation{host, c})
} }
warnIfHostHasPath(host) warnIfHostHasPath(host, c.log())
reg, err := remote.NewRegistry(host) reg, err := remote.NewRegistry(host)
if err != nil { if err != nil {

@ -18,6 +18,7 @@ package registry
import ( import (
"io" "io"
"log/slog"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"path/filepath" "path/filepath"
@ -121,6 +122,38 @@ func TestLogin_ResetsForceAttemptOAuth2_OnFailure(t *testing.T) {
} }
} }
// TestClientOptLogger verifies that the logger option properly injects a custom logger.
func TestClientOptLogger(t *testing.T) {
t.Parallel()
var buf strings.Builder
customHandler := slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelWarn})
logger := slog.New(customHandler)
credFile := filepath.Join(t.TempDir(), "config.json")
c, err := NewClient(
ClientOptWriter(io.Discard),
ClientOptCredentialsFile(credFile),
ClientOptLogger(logger),
)
require.NoError(t, err)
// The client's log() should return the custom logger
if c.log().Handler() != customHandler {
t.Error("expected client logger to use the custom handler")
}
// Without the option, log() should return a discard handler
c2, err := NewClient(
ClientOptWriter(io.Discard),
ClientOptCredentialsFile(credFile),
)
require.NoError(t, err)
// Verify logging to the nil-logger client doesn't panic
c2.log().Warn("should not panic")
}
// TestWarnIfHostHasPath verifies that warnIfHostHasPath correctly detects path components. // TestWarnIfHostHasPath verifies that warnIfHostHasPath correctly detects path components.
func TestWarnIfHostHasPath(t *testing.T) { func TestWarnIfHostHasPath(t *testing.T) {
t.Parallel() t.Parallel()
@ -159,7 +192,7 @@ func TestWarnIfHostHasPath(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got := warnIfHostHasPath(tt.host) got := warnIfHostHasPath(tt.host, slog.New(slog.DiscardHandler))
if got != tt.wantWarn { if got != tt.wantWarn {
t.Errorf("warnIfHostHasPath(%q) = %v, want %v", tt.host, got, tt.wantWarn) t.Errorf("warnIfHostHasPath(%q) = %v, want %v", tt.host, got, tt.wantWarn)
} }

@ -48,6 +48,14 @@ const payloadSizeLimit int64 = 16 * 1024 // 16 KiB
// request and add hooks to report HTTP tracing events. // request and add hooks to report HTTP tracing events.
type LoggingTransport struct { type LoggingTransport struct {
http.RoundTripper http.RoundTripper
Logger *slog.Logger
}
func (t *LoggingTransport) log() *slog.Logger {
if t.Logger != nil {
return t.Logger
}
return slog.New(slog.DiscardHandler)
} }
// NewTransport creates and returns a new instance of LoggingTransport // NewTransport creates and returns a new instance of LoggingTransport
@ -69,14 +77,14 @@ func NewTransport(debug bool) *retry.Transport {
func (t *LoggingTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { func (t *LoggingTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
id := requestCount.Add(1) - 1 id := requestCount.Add(1) - 1
slog.Debug(req.Method, "id", id, "url", req.URL, "header", logHeader(req.Header)) t.log().Debug(req.Method, "id", id, "url", req.URL, "header", logHeader(req.Header))
resp, err = t.RoundTripper.RoundTrip(req) resp, err = t.RoundTripper.RoundTrip(req)
if err != nil { if err != nil {
slog.Debug("Response"[:len(req.Method)], "id", id, "error", err) t.log().Debug("Response"[:len(req.Method)], "id", id, "error", err)
} else if resp != nil { } else if resp != nil {
slog.Debug("Response"[:len(req.Method)], "id", id, "status", resp.Status, "header", logHeader(resp.Header), "body", logResponseBody(resp)) t.log().Debug("Response"[:len(req.Method)], "id", id, "status", resp.Status, "header", logHeader(resp.Header), "body", logResponseBody(resp))
} else { } else {
slog.Debug("Response"[:len(req.Method)], "id", id, "response", "nil") t.log().Debug("Response"[:len(req.Method)], "id", id, "response", "nil")
} }
return resp, err return resp, err

@ -74,7 +74,10 @@ var events = map[string]release.HookEvent{
// //
// Files that do not parse into the expected format are simply placed into a map and // Files that do not parse into the expected format are simply placed into a map and
// returned. // returned.
func SortManifests(files map[string]string, _ common.VersionSet, ordering KindSortOrder) ([]*release.Hook, []Manifest, error) { func SortManifests(files map[string]string, _ common.VersionSet, ordering KindSortOrder, logger *slog.Logger) ([]*release.Hook, []Manifest, error) {
if logger == nil {
logger = slog.New(slog.DiscardHandler)
}
result := &result{} result := &result{}
var sortedFilePaths []string var sortedFilePaths []string
@ -101,7 +104,7 @@ func SortManifests(files map[string]string, _ common.VersionSet, ordering KindSo
path: filePath, path: filePath,
} }
if err := manifestFile.sort(result); err != nil { if err := manifestFile.sort(result, logger); err != nil {
return result.hooks, result.generic, err return result.hooks, result.generic, err
} }
} }
@ -136,7 +139,7 @@ func SortManifests(files map[string]string, _ common.VersionSet, ordering KindSo
// metadata: // metadata:
// annotations: // annotations:
// helm.sh/hook-output-log-policy: hook-succeeded,hook-failed // helm.sh/hook-output-log-policy: hook-succeeded,hook-failed
func (file *manifestFile) sort(result *result) error { func (file *manifestFile) sort(result *result, logger *slog.Logger) error {
// Go through manifests in order found in file (function `SplitManifests` creates integer-sortable keys) // Go through manifests in order found in file (function `SplitManifests` creates integer-sortable keys)
var sortedEntryKeys []string var sortedEntryKeys []string
for entryKey := range file.entries { for entryKey := range file.entries {
@ -196,7 +199,7 @@ func (file *manifestFile) sort(result *result) error {
} }
if isUnknownHook { if isUnknownHook {
slog.Info("skipping unknown hooks", "hookTypes", hookTypes) logger.Info("skipping unknown hooks", "hookTypes", hookTypes)
continue continue
} }

@ -138,7 +138,7 @@ metadata:
manifests[o.path] = o.manifest manifests[o.path] = o.manifest
} }
hs, generic, err := SortManifests(manifests, nil, InstallOrder) hs, generic, err := SortManifests(manifests, nil, InstallOrder, nil)
if err != nil { if err != nil {
t.Fatalf("Unexpected error: %s", err) t.Fatalf("Unexpected error: %s", err)
} }

@ -23,7 +23,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"log/slog"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
@ -269,7 +268,6 @@ func ResolveReferenceURL(baseURL, refURL string) (string, error) {
func (e *Entry) String() string { func (e *Entry) String() string {
buf, err := json.Marshal(e) buf, err := json.Marshal(e)
if err != nil { if err != nil {
slog.Error("failed to marshal entry", slog.Any("error", err))
panic(err) panic(err)
} }
return string(buf) return string(buf)

@ -90,6 +90,16 @@ type IndexFile struct {
// Annotations are additional mappings uninterpreted by Helm. They are made available for // Annotations are additional mappings uninterpreted by Helm. They are made available for
// other applications to add information to the index file. // other applications to add information to the index file.
Annotations map[string]string `json:"annotations,omitempty"` Annotations map[string]string `json:"annotations,omitempty"`
// Logger is the structured logger for this index file. It is not serialized.
Logger *slog.Logger `json:"-"`
}
func (i IndexFile) log() *slog.Logger {
if i.Logger != nil {
return i.Logger
}
return slog.Default()
} }
// NewIndexFile initializes an index. // NewIndexFile initializes an index.
@ -154,7 +164,7 @@ func (i IndexFile) MustAdd(md *chart.Metadata, filename, baseURL, digest string)
// Deprecated: Use index.MustAdd instead. // Deprecated: Use index.MustAdd instead.
func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) { func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) {
if err := i.MustAdd(md, filename, baseURL, digest); err != nil { if err := i.MustAdd(md, filename, baseURL, digest); err != nil {
slog.Error("skipping loading invalid entry for chart", "name", md.Name, "version", md.Version, "file", filename, "error", err) i.log().Error("skipping loading invalid entry for chart", "name", md.Name, "version", md.Version, "file", filename, "error", err)
} }
} }
@ -217,7 +227,7 @@ func (i IndexFile) Get(name, version string) (*ChartVersion, error) {
if constraint.Check(test) { if constraint.Check(test) {
if len(version) != 0 { if len(version) != 0 {
slog.Warn("unable to find exact version requested; falling back to closest available version", "chart", name, "requested", version, "selected", ver.Version) i.log().Warn("unable to find exact version requested; falling back to closest available version", "chart", name, "requested", version, "selected", ver.Version)
} }
return ver, nil return ver, nil
} }
@ -359,7 +369,7 @@ func loadIndex(data []byte, source string) (*IndexFile, error) {
for name, cvs := range i.Entries { for name, cvs := range i.Entries {
for idx, v := range slices.Backward(cvs) { for idx, v := range slices.Backward(cvs) {
if v == nil { if v == nil {
slog.Warn("skipping loading invalid entry for chart: empty entry", "name", name, "source", source) i.log().Warn("skipping loading invalid entry for chart: empty entry", "name", name, "source", source)
cvs = append(cvs[:idx], cvs[idx+1:]...) cvs = append(cvs[:idx], cvs[idx+1:]...)
continue continue
} }
@ -371,7 +381,7 @@ func loadIndex(data []byte, source string) (*IndexFile, error) {
v.APIVersion = chart.APIVersionV1 v.APIVersion = chart.APIVersionV1
} }
if err := v.Validate(); ignoreSkippableChartValidationError(err) != nil { if err := v.Validate(); ignoreSkippableChartValidationError(err) != nil {
slog.Warn("skipping loading invalid entry for chart", "name", name, "version", v.Version, "source", source, "error", err) i.log().Warn("skipping loading invalid entry for chart", "name", name, "version", v.Version, "source", source, "error", err)
cvs = append(cvs[:idx], cvs[idx+1:]...) cvs = append(cvs[:idx], cvs[idx+1:]...)
} }
} }

@ -53,11 +53,9 @@ type ConfigMaps struct {
// NewConfigMaps initializes a new ConfigMaps wrapping an implementation of // NewConfigMaps initializes a new ConfigMaps wrapping an implementation of
// the kubernetes ConfigMapsInterface. // the kubernetes ConfigMapsInterface.
func NewConfigMaps(impl corev1.ConfigMapInterface) *ConfigMaps { func NewConfigMaps(impl corev1.ConfigMapInterface) *ConfigMaps {
c := &ConfigMaps{ return &ConfigMaps{
impl: impl, impl: impl,
} }
c.SetLogger(slog.Default().Handler())
return c
} }
// Name returns the name of the driver. // Name returns the name of the driver.

@ -17,7 +17,6 @@ limitations under the License.
package driver package driver
import ( import (
"log/slog"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -50,9 +49,7 @@ type Memory struct {
// NewMemory initializes a new memory driver. // NewMemory initializes a new memory driver.
func NewMemory() *Memory { func NewMemory() *Memory {
m := &Memory{cache: map[string]memReleases{}, namespace: "default"} return &Memory{cache: map[string]memReleases{}, namespace: "default"}
m.SetLogger(slog.Default().Handler())
return m
} }
// SetNamespace sets a specific namespace in which releases will be accessed. // SetNamespace sets a specific namespace in which releases will be accessed.

@ -52,11 +52,9 @@ type Secrets struct {
// NewSecrets initializes a new Secrets wrapping an implementation of // NewSecrets initializes a new Secrets wrapping an implementation of
// the kubernetes SecretsInterface. // the kubernetes SecretsInterface.
func NewSecrets(impl corev1.SecretInterface) *Secrets { func NewSecrets(impl corev1.SecretInterface) *Secrets {
s := &Secrets{ return &Secrets{
impl: impl, impl: impl,
} }
s.SetLogger(slog.Default().Handler())
return s
} }
// Name returns the name of the driver. // Name returns the name of the driver.

@ -296,7 +296,6 @@ func NewSQL(connectionString string, namespace string) (*SQL, error) {
} }
driver.namespace = namespace driver.namespace = namespace
driver.SetLogger(slog.Default().Handler())
return driver, nil return driver, nil
} }

@ -339,14 +339,9 @@ func Init(d driver.Driver) *Storage {
Driver: d, Driver: d,
} }
var h slog.Handler
// Get logger from driver if it implements the LoggerSetterGetter interface // Get logger from driver if it implements the LoggerSetterGetter interface
if ls, ok := d.(logging.LoggerSetterGetter); ok { if ls, ok := d.(logging.LoggerSetterGetter); ok {
h = ls.Logger().Handler() s.SetLogger(ls.Logger().Handler())
} else {
// If the driver does not implement the LoggerSetterGetter interface, set the default logger
h = slog.Default().Handler()
} }
s.SetLogger(h)
return s return s
} }

Loading…
Cancel
Save