From 960e43d4dff61b7f94372ae93d48c82652c2af1f Mon Sep 17 00:00:00 2001 From: wazery Date: Mon, 1 Sep 2025 23:19:00 +0200 Subject: [PATCH] feat: add verbose logging option to controller-gen --- pkg/crd/gen.go | 29 +++++++++++++++ pkg/genall/genall.go | 26 ++++++++++++++ pkg/genall/options.go | 52 ++++++++++++++++++++++++++- pkg/genall/zz_generated.markerhelp.go | 11 ++++++ 4 files changed, 117 insertions(+), 1 deletion(-) diff --git a/pkg/crd/gen.go b/pkg/crd/gen.go index 5fad65a71..de86bb851 100644 --- a/pkg/crd/gen.go +++ b/pkg/crd/gen.go @@ -20,6 +20,7 @@ import ( "fmt" "go/ast" "go/types" + "log/slog" "sort" "strings" @@ -124,6 +125,12 @@ func transformPreserveUnknownFields(value bool) func(map[string]interface{}) err } func (g Generator) Generate(ctx *genall.GenerationContext) error { + // Extract logger and use a discard logger if nil to avoid repeated nil checks + logger := ctx.Logger + if logger == nil { + logger = slog.New(slog.DiscardHandler) + } + parser := &Parser{ Collector: ctx.Collector, Checker: ctx.Checker, @@ -134,30 +141,44 @@ func (g Generator) Generate(ctx *genall.GenerationContext) error { GenerateEmbeddedObjectMeta: g.GenerateEmbeddedObjectMeta != nil && *g.GenerateEmbeddedObjectMeta, } + logger.Debug("starting CRD generation", "ignoreUnexported", parser.IgnoreUnexportedFields, "allowDangerous", parser.AllowDangerousTypes) + AddKnownTypes(parser) for _, root := range ctx.Roots { parser.NeedPackage(root) + logger.Debug("processing package", "package", root.PkgPath) } metav1Pkg := FindMetav1(ctx.Roots) if metav1Pkg == nil { // no objects in the roots, since nothing imported metav1 + logger.Debug("no metav1 package found in roots, no CRDs to generate") return nil } + logger.Debug("found metav1 package", "package", metav1Pkg.PkgPath) + // TODO: allow selecting a specific object kubeKinds := FindKubeKinds(parser, metav1Pkg) if len(kubeKinds) == 0 { // no objects in the roots + logger.Debug("no Kubernetes kinds found in packages") return nil } + logger.Info("found Kubernetes kinds for CRD generation", "count", len(kubeKinds)) + for _, kind := range kubeKinds { + logger.Debug("processing Kubernetes kind", "group", kind.Group, "kind", kind.Kind) + } + crdVersions := g.CRDVersions if len(crdVersions) == 0 { crdVersions = []string{defaultVersion} } + logger.Debug("using CRD versions", "versions", crdVersions) + var headerText string if g.HeaderFile != "" { @@ -166,6 +187,7 @@ func (g Generator) Generate(ctx *genall.GenerationContext) error { return err } headerText = string(headerBytes) + logger.Debug("loaded header file", "file", g.HeaderFile, "size", len(headerBytes)) } headerText = strings.ReplaceAll(headerText, " YEAR", " "+g.Year) @@ -178,6 +200,8 @@ func (g Generator) Generate(ctx *genall.GenerationContext) error { } for _, groupKind := range kubeKinds { + logger.Debug("generating CRD", "group", groupKind.Group, "kind", groupKind.Kind) + parser.NeedCRDFor(groupKind, g.MaxDescLen) crdRaw := parser.CustomResourceDefinitions[groupKind] addAttribution(&crdRaw) @@ -202,9 +226,14 @@ func (g Generator) Generate(ctx *genall.GenerationContext) error { } else { fileName = fmt.Sprintf("%s_%s.%s.yaml", crdRaw.Spec.Group, crdRaw.Spec.Names.Plural, crdVersions[i]) } + + logger.Debug("writing CRD file", "filename", fileName, "group", groupKind.Group, "kind", groupKind.Kind) + if err := ctx.WriteYAML(fileName, headerText, []interface{}{crd}, yamlOpts...); err != nil { return err } + + logger.Info("generated CRD", "filename", fileName, "group", groupKind.Group, "kind", groupKind.Kind) } } diff --git a/pkg/genall/genall.go b/pkg/genall/genall.go index b6f37409b..cd8c4bc23 100644 --- a/pkg/genall/genall.go +++ b/pkg/genall/genall.go @@ -20,6 +20,7 @@ import ( "encoding/json" "fmt" "io" + "log/slog" "os" "golang.org/x/tools/go/packages" @@ -101,6 +102,9 @@ type Runtime struct { OutputRules OutputRules // ErrorWriter defines where to write error messages. ErrorWriter io.Writer + // LogLevel sets the logging level for generator operations. + // Defaults to slog.LevelInfo if not specified. + LogLevel slog.Level } // GenerationContext defines the common information needed for each Generator @@ -117,6 +121,8 @@ type GenerationContext struct { // InputRule describes how to load associated boilerplate artifacts. // It should *not* be used to load source files. InputRule + // Logger is the logger for verbose output. If nil, logging is disabled. + Logger *slog.Logger } // WriteYAMLOptions implements the Options Pattern for WriteYAML. @@ -261,10 +267,28 @@ func (r *Runtime) Run() bool { return true } + // Set up logging based on log level setting + var logger *slog.Logger + + // Use the specified log level, defaulting to Info if not set + logLevel := r.LogLevel + if logLevel == 0 { + logLevel = slog.LevelInfo + } + + logger = slog.New(slog.NewTextHandler(r.ErrorWriter, &slog.HandlerOptions{ + Level: logLevel, + })) + + if logLevel <= slog.LevelDebug { + logger.Info("debug logging enabled") + } + hadErrs := false for _, gen := range r.Generators { ctx := r.GenerationContext // make a shallow copy ctx.OutputRule = r.OutputRules.ForGenerator(gen) + ctx.Logger = logger // don't pass a typechecker to generators that don't provide a filter // to avoid accidents @@ -272,6 +296,8 @@ func (r *Runtime) Run() bool { ctx.Checker = nil } + logger.Debug("running generator", "generator", fmt.Sprintf("%T", *gen)) + if err := (*gen).Generate(&ctx); err != nil { fmt.Fprintln(r.ErrorWriter, err) hadErrs = true diff --git a/pkg/genall/options.go b/pkg/genall/options.go index 192235b76..81e1254f3 100644 --- a/pkg/genall/options.go +++ b/pkg/genall/options.go @@ -18,6 +18,7 @@ package genall import ( "fmt" + "log/slog" "strings" "golang.org/x/tools/go/packages" @@ -26,6 +27,7 @@ import ( var ( InputPathsMarker = markers.Must(markers.MakeDefinition("paths", markers.DescribesPackage, InputPaths(nil))) + LogLevelMarker = markers.Must(markers.MakeDefinition("loglevel", markers.DescribesPackage, LogLevel(""))) ) // +controllertools:marker:generateHelp:category="" @@ -35,16 +37,52 @@ var ( // Multiple paths can be specified using "{path1, path2, path3}". type InputPaths []string +// +controllertools:marker:generateHelp:category="" + +// LogLevel sets the logging level for generator operations. +// Valid values are "debug", "info", "warn", "error". +// Defaults to "info" if not specified. +type LogLevel string + +const ( + LogLevelDebug LogLevel = "debug" + LogLevelInfo LogLevel = "info" + LogLevelWarn LogLevel = "warn" + LogLevelError LogLevel = "error" +) + +// ToSlogLevel converts the LogLevel to slog.Level +func (l LogLevel) ToSlogLevel() slog.Level { + switch l { + case LogLevelDebug: + return slog.LevelDebug + case LogLevelInfo: + return slog.LevelInfo + case LogLevelWarn: + return slog.LevelWarn + case LogLevelError: + return slog.LevelError + default: + return slog.LevelInfo // default fallback + } +} + // RegisterOptionsMarkers registers "mandatory" options markers for FromOptions into the given registry. -// At this point, that's just InputPaths. +// At this point, that's just InputPaths and LogLevel. func RegisterOptionsMarkers(into *markers.Registry) error { if err := into.Register(InputPathsMarker); err != nil { return err } + if err := into.Register(LogLevelMarker); err != nil { + return err + } // NB(directxman12): we make this optional so we don't have a bootstrap problem with helpgen if helpGiver, hasHelp := ((interface{})(InputPaths(nil))).(HasHelp); hasHelp { into.AddHelp(InputPathsMarker, helpGiver.Help()) } + if helpGiver, hasHelp := ((interface{})(LogLevel(""))).(HasHelp); hasHelp { + into.AddHelp(LogLevelMarker, helpGiver.Help()) + } return nil } @@ -90,6 +128,13 @@ func FromOptionsWithConfig(cfg *packages.Config, optionsRegistry *markers.Regist return nil, err } + // Set log level from the parsed options + if protoRt.LogLevel != "" { + genRuntime.LogLevel = protoRt.LogLevel.ToSlogLevel() + } else { + genRuntime.LogLevel = slog.LevelInfo // default + } + // attempt to figure out what the user wants without a lot of verbose specificity: // if the user specifies a default rule, assume that they probably want to fall back // to that. Otherwise, assume that they just wanted to customize one option from the @@ -117,6 +162,7 @@ func protoFromOptions(optionsRegistry *markers.Registry, options []string) (prot ByGenerator: make(map[*Generator]OutputRule), } var paths []string + var logLevel LogLevel // collect the generators first, so that we can key the output on the actual // generator, which matters if there's settings in the gen object and it's not a pointer. @@ -156,6 +202,8 @@ func protoFromOptions(optionsRegistry *markers.Registry, options []string) (prot continue case InputPaths: paths = append(paths, val...) + case LogLevel: + logLevel = val default: return protoRuntime{}, fmt.Errorf("unknown option marker %q", defn.Name) } @@ -176,6 +224,7 @@ func protoFromOptions(optionsRegistry *markers.Registry, options []string) (prot Generators: gens, OutputRules: rules, GeneratorsByName: gensByName, + LogLevel: logLevel, }, nil } @@ -186,6 +235,7 @@ type protoRuntime struct { Generators Generators OutputRules OutputRules GeneratorsByName map[string]*Generator + LogLevel LogLevel } // splitOutputRuleOption splits a marker name of "output:rule:gen" or "output:rule" diff --git a/pkg/genall/zz_generated.markerhelp.go b/pkg/genall/zz_generated.markerhelp.go index b33c2392e..db4a988e4 100644 --- a/pkg/genall/zz_generated.markerhelp.go +++ b/pkg/genall/zz_generated.markerhelp.go @@ -35,6 +35,17 @@ func (InputPaths) Help() *markers.DefinitionHelp { } } +func (LogLevel) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "", + DetailedHelp: markers.DetailedHelp{ + Summary: "sets the logging level for generator operations.", + Details: "Valid values are \"debug\", \"info\", \"warn\", \"error\".\nDefaults to \"info\" if not specified.", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} + func (OutputArtifacts) Help() *markers.DefinitionHelp { return &markers.DefinitionHelp{ Category: "",