Migrate to a dedicated internal package for slog adapter + migrate more

Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr>
pull/30708/head
Benoit Tigeot 5 months ago
parent fae2345edf
commit 83cdffe4ae
No known key found for this signature in database
GPG Key ID: 8E6D4FC8AEBDA62C

@ -23,6 +23,7 @@ import (
// Import to initialize client auth plugins.
_ "k8s.io/client-go/plugin/pkg/client/auth"
logadapter "helm.sh/helm/v4/internal/log"
helmcmd "helm.sh/helm/v4/pkg/cmd"
"helm.sh/helm/v4/pkg/kube"
)
@ -37,10 +38,11 @@ func main() {
// another name (e.g., helm2 or helm3) does not change the name of the
// manager as picked up by the automated name detection.
kube.ManagedFieldsManager = "helm"
logger := logadapter.NewReadableTextLogger(os.Stderr, false)
cmd, err := helmcmd.NewRootCmd(os.Stdout, os.Args[1:])
if err != nil {
helmcmd.Warning("%+v", err)
logger.Warn("%+v", err)
os.Exit(1)
}

@ -14,10 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// Package kube provides Kubernetes client utilities for Helm.
package kube
import "log/slog"
package log
// Logger defines a minimal logging interface compatible with structured logging.
// It provides methods for different log levels with structured key-value pairs.
@ -27,6 +24,9 @@ type Logger interface {
// Warn logs a message at warning level with structured key-value pairs.
Warn(msg string, args ...any)
// Error logs a message at error level with structured key-value pairs.
Error(msg string, args ...any)
}
// NopLogger is a logger implementation that discards all log messages.
@ -38,29 +38,9 @@ func (NopLogger) Debug(_ string, args ...any) {}
// Warn implements Logger.Warn by doing nothing.
func (NopLogger) Warn(_ string, args ...any) {}
// Error implements Logger.Error by doing nothing.
func (NopLogger) Error(_ string, args ...any) {}
// DefaultLogger provides a no-op logger that discards all messages.
// It can be used as a default when no logger is provided.
var DefaultLogger Logger = NopLogger{}
// SlogAdapter adapts a standard library slog.Logger to the Logger interface.
type SlogAdapter struct {
logger *slog.Logger
}
// Debug implements Logger.Debug by forwarding to the underlying slog.Logger.
func (a SlogAdapter) Debug(msg string, args ...any) {
a.logger.Debug(msg, args...)
}
// Warn implements Logger.Warn by forwarding to the underlying slog.Logger.
func (a SlogAdapter) Warn(msg string, args ...any) {
a.logger.Warn(msg, args...)
}
// NewSlogAdapter creates a Logger that forwards log messages to a slog.Logger.
func NewSlogAdapter(logger *slog.Logger) Logger {
if logger == nil {
return DefaultLogger
}
return SlogAdapter{logger: logger}
}

@ -0,0 +1,71 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package log
import (
"io"
"log/slog"
)
// SlogAdapter adapts a standard library slog.Logger to the Logger interface.
type SlogAdapter struct {
logger *slog.Logger
}
// Debug implements Logger.Debug by forwarding to the underlying slog.Logger.
func (a SlogAdapter) Debug(msg string, args ...any) {
a.logger.Debug(msg, args...)
}
// Warn implements Logger.Warn by forwarding to the underlying slog.Logger.
func (a SlogAdapter) Warn(msg string, args ...any) {
a.logger.Warn(msg, args...)
}
// Error implements Logger.Error by forwarding to the underlying slog.Logger.
func (a SlogAdapter) Error(msg string, args ...any) {
// TODO: Handle error with `slog.Any`: slog.Info("something went wrong", slog.Any("err", err))
a.logger.Error(msg, args...)
}
// NewSlogAdapter creates a Logger that forwards log messages to a slog.Logger.
func NewSlogAdapter(logger *slog.Logger) Logger {
if logger == nil {
return DefaultLogger
}
return SlogAdapter{logger: logger}
}
// NewReadableTextLogger creates a Logger that outputs in a readable text format without timestamps
func NewReadableTextLogger(output io.Writer, debugEnabled bool) Logger {
level := slog.LevelInfo
if debugEnabled {
level = slog.LevelDebug
}
handler := slog.NewTextHandler(output, &slog.HandlerOptions{
Level: level,
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey {
return slog.Attr{}
}
return a
},
})
return NewSlogAdapter(slog.New(handler))
}

@ -33,6 +33,7 @@ import (
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
logadapter "helm.sh/helm/v4/internal/log"
chart "helm.sh/helm/v4/pkg/chart/v2"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/engine"
@ -95,7 +96,7 @@ type Configuration struct {
// Capabilities describes the capabilities of the Kubernetes cluster.
Capabilities *chartutil.Capabilities
Log kube.Logger
Log logadapter.Logger
// HookOutputFunc called with container name and returns and expects writer that will receive the log output.
HookOutputFunc func(namespace, pod, container string) io.Writer
@ -374,7 +375,7 @@ func (cfg *Configuration) recordRelease(r *release.Release) {
}
// Init initializes the action configuration
func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namespace, helmDriver string, log kube.Logger) error {
func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namespace, helmDriver string, log logadapter.Logger) error {
kc := kube.New(getter)
kc.Log = log

@ -24,6 +24,7 @@ import (
"github.com/stretchr/testify/assert"
fakeclientset "k8s.io/client-go/kubernetes/fake"
logadapter "helm.sh/helm/v4/internal/log"
chart "helm.sh/helm/v4/pkg/chart/v2"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
@ -49,12 +50,7 @@ func actionConfigFixture(t *testing.T) *Configuration {
KubeClient: &kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}},
Capabilities: chartutil.DefaultCapabilities,
RegistryClient: registryClient,
Log: func(format string, v ...interface{}) {
t.Helper()
if *verbose {
t.Logf(format, v...)
}
},
Log: logadapter.DefaultLogger,
}
}

@ -53,6 +53,6 @@ func (h *History) Run(name string) ([]*release.Release, error) {
return nil, errors.Errorf("release name is invalid: %s", name)
}
h.cfg.Log("getting history for release %s", name)
h.cfg.Log.Debug("getting history for release", "release", name)
return h.cfg.Releases.History(name)
}

@ -172,7 +172,7 @@ func (i *Install) installCRDs(crds []chart.CRD) error {
// If the error is CRD already exists, continue.
if apierrors.IsAlreadyExists(err) {
crdName := res[0].Name
i.cfg.Log("CRD %s is already present. Skipping.", crdName)
i.cfg.Log.Debug("CRD is already present. Skipping", "crd", crdName)
continue
}
return errors.Wrapf(err, "failed to install CRD %s", obj.Name)
@ -200,7 +200,7 @@ func (i *Install) installCRDs(crds []chart.CRD) error {
return err
}
i.cfg.Log("Clearing discovery cache")
i.cfg.Log.Debug("clearing discovery cache")
discoveryClient.Invalidate()
_, _ = discoveryClient.ServerGroups()
@ -213,7 +213,7 @@ func (i *Install) installCRDs(crds []chart.CRD) error {
return err
}
if resettable, ok := restMapper.(meta.ResettableRESTMapper); ok {
i.cfg.Log("Clearing REST mapper cache")
i.cfg.Log.Debug("clearing REST mapper cache")
resettable.Reset()
}
}
@ -237,24 +237,24 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
// Check reachability of cluster unless in client-only mode (e.g. `helm template` without `--validate`)
if !i.ClientOnly {
if err := i.cfg.KubeClient.IsReachable(); err != nil {
i.cfg.Log(fmt.Sprintf("ERROR: Cluster reachability check failed: %v", err))
i.cfg.Log.Error(fmt.Sprintf("cluster reachability check failed: %v", err))
return nil, errors.Wrap(err, "cluster reachability check failed")
}
}
// HideSecret must be used with dry run. Otherwise, return an error.
if !i.isDryRun() && i.HideSecret {
i.cfg.Log("ERROR: Hiding Kubernetes secrets requires a dry-run mode")
i.cfg.Log.Error("hiding Kubernetes secrets requires a dry-run mode")
return nil, errors.New("Hiding Kubernetes secrets requires a dry-run mode")
}
if err := i.availableName(); err != nil {
i.cfg.Log(fmt.Sprintf("ERROR: Release name check failed: %v", err))
i.cfg.Log.Error("release name check failed", "error", err)
return nil, errors.Wrap(err, "release name check failed")
}
if err := chartutil.ProcessDependencies(chrt, vals); err != nil {
i.cfg.Log(fmt.Sprintf("ERROR: Processing chart dependencies failed: %v", err))
i.cfg.Log.Error("chart dependencies processing failed", "error", err)
return nil, errors.Wrap(err, "chart dependencies processing failed")
}
@ -268,7 +268,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
if crds := chrt.CRDObjects(); !i.ClientOnly && !i.SkipCRDs && len(crds) > 0 {
// On dry run, bail here
if i.isDryRun() {
i.cfg.Log("WARNING: This chart or one of its subcharts contains CRDs. Rendering may fail or contain inaccuracies.")
i.cfg.Log.Warn("This chart or one of its subcharts contains CRDs. Rendering may fail or contain inaccuracies.")
} else if err := i.installCRDs(crds); err != nil {
return nil, err
}
@ -288,7 +288,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
mem.SetNamespace(i.Namespace)
i.cfg.Releases = storage.Init(mem)
} else if !i.ClientOnly && len(i.APIVersions) > 0 {
i.cfg.Log("API Version list given outside of client only mode, this list will be ignored")
i.cfg.Log.Debug("API Version list given outside of client only mode, this list will be ignored")
}
// Make sure if Atomic is set, that wait is set as well. This makes it so
@ -505,7 +505,7 @@ func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.Resource
// One possible strategy would be to do a timed retry to see if we can get
// this stored in the future.
if err := i.recordRelease(rel); err != nil {
i.cfg.Log("failed to record the release: %s", err)
i.cfg.Log.Error("failed to record the release", "error", err)
}
return rel, nil
@ -514,7 +514,7 @@ func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.Resource
func (i *Install) failRelease(rel *release.Release, err error) (*release.Release, error) {
rel.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", i.ReleaseName, err.Error()))
if i.Atomic {
i.cfg.Log("Install failed and atomic is set, uninstalling release")
i.cfg.Log.Debug("install failed, uninstalling release", "release", i.ReleaseName)
uninstall := NewUninstall(i.cfg)
uninstall.DisableHooks = i.DisableHooks
uninstall.KeepHistory = false

@ -26,6 +26,7 @@ import (
shellwords "github.com/mattn/go-shellwords"
"github.com/spf13/cobra"
logadapter "helm.sh/helm/v4/internal/log"
"helm.sh/helm/v4/internal/test"
"helm.sh/helm/v4/pkg/action"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
@ -92,7 +93,7 @@ func executeActionCommandStdinC(store *storage.Storage, in *os.File, cmd string)
Releases: store,
KubeClient: &kubefake.PrintingKubeClient{Out: io.Discard},
Capabilities: chartutil.DefaultCapabilities,
Log: func(_ string, _ ...interface{}) {},
Log: logadapter.DefaultLogger,
}
root, err := newRootCmdWithConfig(actionConfig, buf, args)

@ -265,7 +265,7 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
}
if chartRequested.Metadata.Deprecated {
Warning("This chart is deprecated")
logger.Warn("this chart is deprecated")
}
if req := chartRequested.Metadata.Dependencies; req != nil {

@ -26,10 +26,10 @@ import (
"github.com/gosuri/uitable"
"github.com/spf13/cobra"
logadapter "helm.sh/helm/v4/internal/log"
"helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v4/pkg/cli/output"
"helm.sh/helm/v4/pkg/cmd/require"
"helm.sh/helm/v4/pkg/kube"
release "helm.sh/helm/v4/pkg/release/v1"
)
@ -64,7 +64,7 @@ func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client := action.NewList(cfg)
var outfmt output.Format
slogger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
adapter := kube.NewSlogAdapter(slogger)
adapter := logadapter.NewSlogAdapter(slogger)
cmd := &cobra.Command{
Use: "list",

@ -122,7 +122,7 @@ func getUsernamePassword(usernameOpt string, passwordOpt string, passwordFromStd
}
}
} else {
Warning("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

@ -31,6 +31,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/clientcmd"
logadapter "helm.sh/helm/v4/internal/log"
"helm.sh/helm/v4/internal/tlsutil"
"helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v4/pkg/cli"
@ -95,16 +96,7 @@ By default, the default directories depend on the Operating System. The defaults
`
var settings = cli.New()
func Debug(format string, v ...interface{}) {
if settings.Debug {
log.Output(2, fmt.Sprintf("[debug] "+format+"\n", v...))
}
}
func Warning(format string, v ...interface{}) {
fmt.Fprintf(os.Stderr, "WARNING: "+format+"\n", v...)
}
var logger = logadapter.NewReadableTextLogger(os.Stderr, settings.Debug)
func NewRootCmd(out io.Writer, args []string) (*cobra.Command, error) {
actionConfig := new(action.Configuration)
@ -114,7 +106,7 @@ func NewRootCmd(out io.Writer, args []string) (*cobra.Command, error) {
}
cobra.OnInitialize(func() {
helmDriver := os.Getenv("HELM_DRIVER")
if err := actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), helmDriver, Debug); err != nil {
if err := actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), helmDriver, logger); err != nil {
log.Fatal(err)
}
if helmDriver == "memory" {

@ -189,8 +189,7 @@ func (o *searchRepoOptions) buildIndex() (*search.Index, error) {
f := filepath.Join(o.repoCacheDir, helmpath.CacheIndexFile(n))
ind, err := repo.LoadIndexFile(f)
if err != nil {
Warning("Repo %q is corrupt or missing. Try 'helm repo update'.", n)
Warning("%s", err)
logger.Warn("repo is corrupt or missing", "repo", n, "error", err)
continue
}

@ -225,7 +225,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
}
if ch.Metadata.Deprecated {
Warning("This chart is deprecated")
logger.Warn("this chart is deprecated")
}
// Create context and prepare the handle of SIGTERM

@ -73,7 +73,7 @@ type Client struct {
// needs. The smaller surface area of the interface means there is a lower
// chance of it changing.
Factory Factory
Log Logger
Log logadapter.Logger
// Namespace allows to bypass the kubeconfig file for the choice of the namespace
Namespace string

@ -26,6 +26,7 @@ import (
"github.com/stretchr/testify/assert"
logadapter "helm.sh/helm/v4/internal/log"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
@ -107,7 +108,7 @@ func newTestClient(t *testing.T) *Client {
return &Client{
Factory: testFactory.WithNamespace("default"),
Log: DefaultLogger,
Log: logadapter.DefaultLogger,
}
}

@ -32,6 +32,7 @@ import (
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
logadapter "helm.sh/helm/v4/internal/log"
deploymentutil "helm.sh/helm/v4/internal/third_party/k8s.io/kubernetes/deployment/util"
)
@ -57,13 +58,13 @@ func CheckJobs(checkJobs bool) ReadyCheckerOption {
// NewReadyChecker creates a new checker. Passed ReadyCheckerOptions can
// be used to override defaults.
func NewReadyChecker(cl kubernetes.Interface, logger Logger, opts ...ReadyCheckerOption) ReadyChecker {
func NewReadyChecker(cl kubernetes.Interface, logger logadapter.Logger, opts ...ReadyCheckerOption) ReadyChecker {
c := ReadyChecker{
client: cl,
log: logger,
}
if c.log == nil {
c.log = DefaultLogger
c.log = logadapter.DefaultLogger
}
for _, opt := range opts {
opt(&c)
@ -74,7 +75,7 @@ func NewReadyChecker(cl kubernetes.Interface, logger Logger, opts ...ReadyChecke
// ReadyChecker is a type that can check core Kubernetes types for readiness.
type ReadyChecker struct {
client kubernetes.Interface
log Logger
log logadapter.Logger
checkJobs bool
pausedAsReady bool
}

@ -19,6 +19,7 @@ import (
"context"
"testing"
logadapter "helm.sh/helm/v4/internal/log"
appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
@ -37,7 +38,7 @@ const defaultNamespace = metav1.NamespaceDefault
func Test_ReadyChecker_IsReady_Pod(t *testing.T) {
type fields struct {
client kubernetes.Interface
log Logger
log logadapter.Logger
checkJobs bool
pausedAsReady bool
}
@ -661,7 +662,7 @@ func Test_ReadyChecker_IsReady_ReplicationController(t *testing.T) {
func Test_ReadyChecker_IsReady_ReplicaSet(t *testing.T) {
type fields struct {
client kubernetes.Interface
log Logger
log logadapter.Logger
checkJobs bool
pausedAsReady bool
}

@ -25,6 +25,7 @@ import (
multierror "github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
logadapter "helm.sh/helm/v4/internal/log"
appsv1 "k8s.io/api/apps/v1"
appsv1beta1 "k8s.io/api/apps/v1beta1"
appsv1beta2 "k8s.io/api/apps/v1beta2"

@ -31,7 +31,7 @@ import (
"k8s.io/apimachinery/pkg/util/validation"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
"helm.sh/helm/v4/pkg/kube"
logadapter "helm.sh/helm/v4/internal/log"
rspb "helm.sh/helm/v4/pkg/release/v1"
)
@ -44,7 +44,7 @@ const ConfigMapsDriverName = "ConfigMap"
// ConfigMapsInterface.
type ConfigMaps struct {
impl corev1.ConfigMapInterface
Log kube.Logger
Log logadapter.Logger
}
// NewConfigMaps initializes a new ConfigMaps wrapping an implementation of

@ -262,7 +262,6 @@ func newTestFixtureSQL(t *testing.T, _ ...*rspb.Release) (*SQL, sqlmock.Sqlmock)
sqlxDB := sqlx.NewDb(sqlDB, "sqlmock")
return &SQL{
db: sqlxDB,
Log: func(_ string, _ ...interface{}) {},
namespace: "default",
statementBuilder: sq.StatementBuilder.PlaceholderFormat(sq.Dollar),
}, mock

@ -31,7 +31,7 @@ import (
"k8s.io/apimachinery/pkg/util/validation"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
"helm.sh/helm/v4/pkg/kube"
logadapter "helm.sh/helm/v4/internal/log"
rspb "helm.sh/helm/v4/pkg/release/v1"
)
@ -44,7 +44,7 @@ const SecretsDriverName = "Secret"
// SecretsInterface.
type Secrets struct {
impl corev1.SecretInterface
Log kube.Logger
Log logadapter.Logger
}
// NewSecrets initializes a new Secrets wrapping an implementation of

@ -30,7 +30,7 @@ import (
// Import pq for postgres dialect
_ "github.com/lib/pq"
"helm.sh/helm/v4/pkg/kube"
logadapter "helm.sh/helm/v4/internal/log"
rspb "helm.sh/helm/v4/pkg/release/v1"
)
@ -88,7 +88,7 @@ type SQL struct {
namespace string
statementBuilder sq.StatementBuilderType
Log kube.Logger
Log logadapter.Logger
}
// Name returns the name of the driver.
@ -277,7 +277,7 @@ type SQLReleaseCustomLabelWrapper struct {
}
// NewSQL initializes a new sql driver.
func NewSQL(connectionString string, logger kube.Logger, namespace string) (*SQL, error) {
func NewSQL(connectionString string, logger logadapter.Logger, namespace string) (*SQL, error) {
db, err := sqlx.Connect(postgreSQLDialect, connectionString)
if err != nil {
return nil, err

Loading…
Cancel
Save