diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index 869b5d202..66a28b2ab 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -17,8 +17,10 @@ limitations under the License. package engine import ( + "bytes" "errors" "fmt" + "log/slog" "path" "strings" "sync" @@ -393,6 +395,51 @@ func TestRenderWithClientProvider(t *testing.T) { } } +func TestLookupLogsDebugWhenObjectNotFound(t *testing.T) { + provider := &testClientProvider{ + t: t, + scheme: map[string]kindProps{ + "v1/Namespace": { + gvr: schema.GroupVersionResource{ + Version: "v1", + Resource: "namespaces", + }, + }, + }, + objects: []runtime.Object{ + makeUnstructured("v1", "Namespace", "default", ""), + }, + } + + originalLogger := slog.Default() + logBuffer := &bytes.Buffer{} + slog.SetDefault(slog.New(slog.NewTextHandler(logBuffer, &slog.HandlerOptions{Level: slog.LevelDebug}))) + t.Cleanup(func() { + slog.SetDefault(originalLogger) + }) + + lookup := newLookupFunction(provider) + + _, err := lookup("v1", "Namespace", "", "absent") + if err != nil { + t.Fatalf("expected no error when lookup object is missing: %v", err) + } + + logOutput := logBuffer.String() + if !strings.Contains(logOutput, "lookup returned no object") { + t.Fatalf("expected debug log for missing lookup object, got: %q", logOutput) + } + + logBuffer.Reset() + _, err = lookup("v1", "Namespace", "", "default") + if err != nil { + t.Fatalf("expected no error when lookup object exists: %v", err) + } + if strings.Contains(logBuffer.String(), "lookup returned no object") { + t.Fatalf("did not expect missing-object debug log for existing lookup object, got: %q", logBuffer.String()) + } +} + func TestRenderWithClientProvider_error(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{ diff --git a/pkg/engine/funcs.go b/pkg/engine/funcs.go index 431f82f63..e84c24d7a 100644 --- a/pkg/engine/funcs.go +++ b/pkg/engine/funcs.go @@ -21,6 +21,7 @@ import ( "encoding/json" "errors" "fmt" + "log/slog" "maps" "math" "reflect" @@ -89,7 +90,8 @@ func funcMap() template.FuncMap { "required": func(string, any) (any, error) { return "not implemented", nil }, // Provide a placeholder for the "lookup" function, which requires a kubernetes // connection. - "lookup": func(string, string, string, string) (map[string]any, error) { + "lookup": func(apiVersion, kind, namespace, name string) (map[string]any, error) { + slog.Debug("lookup skipped: no Kubernetes client available", "apiVersion", apiVersion, "kind", kind, "namespace", namespace, "name", name) return map[string]any{}, nil }, } diff --git a/pkg/engine/funcs_test.go b/pkg/engine/funcs_test.go index cf6a8d5c9..680d0ebe3 100644 --- a/pkg/engine/funcs_test.go +++ b/pkg/engine/funcs_test.go @@ -17,6 +17,8 @@ limitations under the License. package engine import ( + "bytes" + "log/slog" "math" "strings" "testing" @@ -195,6 +197,26 @@ keyInElement1 = "valueInElement1"`, } } +func TestLookupPlaceholderLogsDebug(t *testing.T) { + originalLogger := slog.Default() + logBuffer := &bytes.Buffer{} + slog.SetDefault(slog.New(slog.NewTextHandler(logBuffer, &slog.HandlerOptions{Level: slog.LevelDebug}))) + t.Cleanup(func() { + slog.SetDefault(originalLogger) + }) + + var output strings.Builder + err := template.Must(template.New("test").Funcs(funcMap()).Parse(`{{ lookup "v1" "Namespace" "" "missing" }}`)).Execute(&output, nil) + require.NoError(t, err) + assert.Equal(t, "map[]", output.String()) + + logOutput := logBuffer.String() + assert.Contains(t, logOutput, "lookup skipped: no Kubernetes client available") + assert.Contains(t, logOutput, "apiVersion=v1") + assert.Contains(t, logOutput, "kind=Namespace") + assert.Contains(t, logOutput, "name=missing") +} + func TestDurationHelpers(t *testing.T) { tests := []struct { name string diff --git a/pkg/engine/lookup_func.go b/pkg/engine/lookup_func.go index 52b6ffdaf..e1fb25d57 100644 --- a/pkg/engine/lookup_func.go +++ b/pkg/engine/lookup_func.go @@ -73,6 +73,7 @@ func newLookupFunction(clientProvider ClientProvider) lookupFunc { if apierrors.IsNotFound(err) { // Just return an empty interface when the object was not found. // That way, users can use `if not (lookup ...)` in their templates. + slog.Debug("lookup returned no object", "apiVersion", apiversion, "kind", kind, "namespace", namespace, "name", name) return map[string]any{}, nil } return map[string]any{}, err @@ -85,6 +86,7 @@ func newLookupFunction(clientProvider ClientProvider) lookupFunc { if apierrors.IsNotFound(err) { // Just return an empty interface when the object was not found. // That way, users can use `if not (lookup ...)` in their templates. + slog.Debug("lookup returned no objects", "apiVersion", apiversion, "kind", kind, "namespace", namespace) return map[string]any{}, nil } return map[string]any{}, err