From bd065151bb42d9327e37342156fa5e6b2a7e98d0 Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Thu, 7 May 2026 16:05:52 -0400 Subject: [PATCH] Use generator utilities in all API package definition generators --- .../cmd/applyconfiguration-gen/args/args.go | 7 + .../generators/targets.go | 27 ++- .../cmd/client-gen/args/args.go | 7 + .../client-gen/generators/client_generator.go | 27 ++- .../generators/generator_for_group.go | 14 +- .../cmd/conversion-gen/args/args.go | 7 + .../conversion-gen/generators/conversion.go | 75 +++--- .../cmd/deepcopy-gen/args/args.go | 8 + .../cmd/deepcopy-gen/generators/deepcopy.go | 26 +- .../generators/deepcopy_targets_test.go | 223 +++++++++++++++++ .../cmd/defaulter-gen/args/args.go | 7 + .../cmd/defaulter-gen/generators/defaulter.go | 53 ++-- .../cmd/informer-gen/args/args.go | 8 + .../cmd/informer-gen/generators/targets.go | 31 ++- .../cmd/lister-gen/args/args.go | 8 + .../cmd/lister-gen/generators/lister.go | 26 +- .../cmd/prerelease-lifecycle-gen/args/args.go | 9 +- .../prerelease-lifecycle-generators/status.go | 77 +++--- .../status_targets_test.go | 168 +++++++++++++ .../cmd/register-gen/args/args.go | 7 + .../cmd/register-gen/generators/targets.go | 42 +++- .../register-gen/generators/targets_test.go | 227 ++++++++++++++++++ .../output_tests/simpletype/v1/doc.go | 1 + .../code-generator/cmd/validation-gen/main.go | 7 + .../cmd/validation-gen/targets.go | 77 +++--- 25 files changed, 949 insertions(+), 220 deletions(-) create mode 100644 staging/src/k8s.io/code-generator/cmd/deepcopy-gen/generators/deepcopy_targets_test.go create mode 100644 staging/src/k8s.io/code-generator/cmd/prerelease-lifecycle-gen/prerelease-lifecycle-generators/status_targets_test.go create mode 100644 staging/src/k8s.io/code-generator/cmd/register-gen/generators/targets_test.go diff --git a/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/args/args.go b/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/args/args.go index 36aa7a3710f..487b936e6f3 100644 --- a/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/args/args.go +++ b/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/args/args.go @@ -20,6 +20,7 @@ import ( "fmt" "github.com/spf13/pflag" + "k8s.io/code-generator/pkg/apidefinitions" "k8s.io/gengo/v2/types" ) @@ -43,6 +44,8 @@ type Args struct { ExternalApplyConfigurations map[types.Name]string OpenAPISchemaFilePath string + + apidefinitions.LintArgs } // New returns default arguments for the generator. @@ -74,6 +77,7 @@ func (args *Args) AddFlags(fs *pflag.FlagSet, inputBase string) { "For example: k8s.io/api/apps/v1.Deployment:k8s.io/client-go/applyconfigurations/apps/v1") fs.StringVar(&args.OpenAPISchemaFilePath, "openapi-schema", "", "path to the openapi schema containing all the types that apply configurations will be generated for") + apidefinitions.AddFlags(&args.LintArgs, fs) } // Validate checks the given arguments. @@ -84,5 +88,8 @@ func (args *Args) Validate() error { if len(args.OutputPkg) == 0 { return fmt.Errorf("--output-pkg must be specified") } + if err := apidefinitions.ValidateFlags(args.LintRules); err != nil { + return err + } return nil } diff --git a/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/generators/targets.go b/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/generators/targets.go index e37d58a1bea..01a8a3f1966 100644 --- a/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/generators/targets.go +++ b/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/generators/targets.go @@ -17,6 +17,7 @@ limitations under the License. package generators import ( + "errors" "fmt" "path" "path/filepath" @@ -32,6 +33,7 @@ import ( "k8s.io/code-generator/cmd/applyconfiguration-gen/args" "k8s.io/code-generator/cmd/client-gen/generators/util" clientgentypes "k8s.io/code-generator/cmd/client-gen/types" + "k8s.io/code-generator/pkg/apidefinitions" genutil "k8s.io/code-generator/pkg/util" ) @@ -62,7 +64,12 @@ func GetTargets(context *generator.Context, args *args.Args) []generator.Target klog.Fatalf("Failed loading boilerplate: %v", err) } - pkgTypes := packageTypesForInputs(context, args.OutputPkg) + var idOpts []apidefinitions.Option + if len(args.LintRules) > 0 { + idOpts = append(idOpts, apidefinitions.WithLintRules(args.LintRules...)) + } + + pkgTypes := packageTypesForInputs(context, args.OutputPkg, idOpts) initialTypes := args.ExternalApplyConfigurations refs := refGraphForReachableTypes(context.Universe, pkgTypes, initialTypes) typeModels, err := newTypeModels(args.OpenAPISchemaFilePath, pkgTypes) @@ -252,10 +259,17 @@ func goName(gv clientgentypes.GroupVersion, p *types.Package) (string, error) { return goName, nil } -func packageTypesForInputs(context *generator.Context, outPkgBase string) map[string]*types.Package { +func packageTypesForInputs(context *generator.Context, outPkgBase string, idOpts []apidefinitions.Option) map[string]*types.Package { pkgTypes := map[string]*types.Package{} for _, inputDir := range context.Inputs { p := context.Universe.Package(inputDir) + info, err := apidefinitions.Identify(p, apidefinitions.ApplyConfiguration, idOpts...) + if err != nil { + klog.Fatal(err) + } + if !info.ShouldGenerate() { + continue + } internal := isInternalPackage(p) if internal { klog.Warningf("Skipping internal package: %s", p.Path) @@ -280,13 +294,12 @@ func groupVersion(p *types.Package) (gv clientgentypes.GroupVersion, err error) // If there's a comment of the form "// +groupName=somegroup" or // "// +groupName=somegroup.foo.bar.io", use the first field (somegroup) as the name of the // group when generating. - override, err := genutil.ExtractCommentTagsWithoutArguments("+", []string{"groupName"}, p.Comments) - - if err != nil { + override, err := apidefinitions.GroupNameForPackage(p.Comments) + if err != nil && !errors.Is(err, apidefinitions.ErrGroupUndeclared) { return gv, err } - if values, ok := override["groupName"]; ok { - gv.Group = clientgentypes.Group(values[0]) + if err == nil { + gv.Group = clientgentypes.Group(override) } return gv, nil diff --git a/staging/src/k8s.io/code-generator/cmd/client-gen/args/args.go b/staging/src/k8s.io/code-generator/cmd/client-gen/args/args.go index 2c6abfd3fcf..9ea1d0bf364 100644 --- a/staging/src/k8s.io/code-generator/cmd/client-gen/args/args.go +++ b/staging/src/k8s.io/code-generator/cmd/client-gen/args/args.go @@ -22,6 +22,7 @@ import ( "github.com/spf13/pflag" "k8s.io/code-generator/cmd/client-gen/types" + "k8s.io/code-generator/pkg/apidefinitions" ) type Args struct { @@ -64,6 +65,8 @@ type Args struct { // PrefersProtobuf determines if the generated clientset uses protobuf for API requests. PrefersProtobuf bool + + apidefinitions.LintArgs } func New() *Args { @@ -104,6 +107,7 @@ func (args *Args) AddFlags(fs *pflag.FlagSet, inputBase string) { "optional package of apply configurations, generated by applyconfiguration-gen, that are required to generate Apply functions for each type in the clientset. By default Apply functions are not generated.") fs.BoolVar(&args.PrefersProtobuf, "prefers-protobuf", args.PrefersProtobuf, "when set, client-gen will generate a clientset that uses protobuf for API requests") + apidefinitions.AddFlags(&args.LintArgs, fs) // support old flags fs.SetNormalizeFunc(mapFlagName("clientset-path", "output-pkg", fs.GetNormalizeFunc())) @@ -122,6 +126,9 @@ func (args *Args) Validate() error { if len(args.ClientsetAPIPath) == 0 { return fmt.Errorf("--clientset-api-path cannot be empty") } + if err := apidefinitions.ValidateFlags(args.LintRules); err != nil { + return err + } return nil } diff --git a/staging/src/k8s.io/code-generator/cmd/client-gen/generators/client_generator.go b/staging/src/k8s.io/code-generator/cmd/client-gen/generators/client_generator.go index c81e358db95..9bbcdee6bfe 100644 --- a/staging/src/k8s.io/code-generator/cmd/client-gen/generators/client_generator.go +++ b/staging/src/k8s.io/code-generator/cmd/client-gen/generators/client_generator.go @@ -18,6 +18,7 @@ limitations under the License. package generators import ( + "errors" "fmt" "path" "path/filepath" @@ -28,6 +29,7 @@ import ( "k8s.io/code-generator/cmd/client-gen/generators/scheme" "k8s.io/code-generator/cmd/client-gen/generators/util" clientgentypes "k8s.io/code-generator/cmd/client-gen/types" + "k8s.io/code-generator/pkg/apidefinitions" codegennamer "k8s.io/code-generator/pkg/namer" genutil "k8s.io/code-generator/pkg/util" "k8s.io/gengo/v2" @@ -274,20 +276,18 @@ NextGroup: // applyGroupOverrides applies group name overrides to each package, if applicable. If there is a // comment of the form "// +groupName=somegroup" or "// +groupName=somegroup.foo.bar.io", use the // first field (somegroup) as the name of the group in Go code, e.g. as the func name in a clientset. -// -// If the first field of the groupName is not unique within the clientset, use "// +groupName=unique func applyGroupOverrides(universe types.Universe, args *args.Args) error { // Create a map from "old GV" to "new GV" so we know what changes we need to make. changes := make(map[clientgentypes.GroupVersion]clientgentypes.GroupVersion) for gv, inputDir := range args.GroupVersionPackages() { p := universe.Package(inputDir) - override, err := genutil.ExtractCommentTagsWithoutArguments("+", []string{"groupName"}, p.Comments) - if err != nil { - return fmt.Errorf("cannot extract groupName tags: %w", err) + override, err := apidefinitions.GroupNameForPackage(p.Comments) + if err != nil && !errors.Is(err, apidefinitions.ErrGroupUndeclared) { + return err } - if override["groupName"] != nil { + if err == nil { newGV := clientgentypes.GroupVersion{ - Group: clientgentypes.Group(override["groupName"][0]), + Group: clientgentypes.Group(override), Version: gv.Version, } changes[gv] = newGV @@ -363,11 +363,24 @@ func GetTargets(context *generator.Context, args *args.Args) []generator.Target klog.Fatalf("cannot apply group overrides: %v", err) } + var idOpts []apidefinitions.Option + if len(args.LintRules) > 0 { + idOpts = append(idOpts, apidefinitions.WithLintRules(args.LintRules...)) + } + gvToTypes := map[clientgentypes.GroupVersion][]*types.Type{} groupGoNames := make(map[clientgentypes.GroupVersion]string) for gv, inputDir := range args.GroupVersionPackages() { p := context.Universe.Package(inputDir) + info, err := apidefinitions.Identify(p, apidefinitions.Client, idOpts...) + if err != nil { + klog.Fatal(err) + } + if !info.ShouldGenerate() { + continue + } + // If there's a comment of the form "// +groupGoName=SomeUniqueShortName", use that as // the Go group identifier in CamelCase. It defaults groupGoNames[gv] = namer.IC(strings.Split(gv.Group.NonEmpty(), ".")[0]) diff --git a/staging/src/k8s.io/code-generator/cmd/client-gen/generators/generator_for_group.go b/staging/src/k8s.io/code-generator/cmd/client-gen/generators/generator_for_group.go index 9f40a790eb0..c20e4ea9633 100644 --- a/staging/src/k8s.io/code-generator/cmd/client-gen/generators/generator_for_group.go +++ b/staging/src/k8s.io/code-generator/cmd/client-gen/generators/generator_for_group.go @@ -17,15 +17,15 @@ limitations under the License. package generators import ( + "errors" "io" "path" - genutil "k8s.io/code-generator/pkg/util" + "k8s.io/code-generator/cmd/client-gen/generators/util" + "k8s.io/code-generator/pkg/apidefinitions" "k8s.io/gengo/v2/generator" "k8s.io/gengo/v2/namer" "k8s.io/gengo/v2/types" - - "k8s.io/code-generator/cmd/client-gen/generators/util" ) // genGroup produces a file for a group client, e.g. ExtensionsClient for the extension group. @@ -73,12 +73,12 @@ func (g *genGroup) GenerateType(c *generator.Context, t *types.Type, w io.Writer // allow user to define a group name that's different from the one parsed from the directory. p := c.Universe.Package(g.inputPackage) groupName := g.group - override, err := genutil.ExtractCommentTagsWithoutArguments("+", []string{"groupName"}, p.Comments) - if err != nil { + override, err := apidefinitions.GroupNameForPackage(p.Comments) + if err != nil && !errors.Is(err, apidefinitions.ErrGroupUndeclared) { return err } - if values, ok := override["groupName"]; ok { - groupName = values[0] + if err == nil { + groupName = override } apiPath := `"` + g.apiPath + `"` diff --git a/staging/src/k8s.io/code-generator/cmd/conversion-gen/args/args.go b/staging/src/k8s.io/code-generator/cmd/conversion-gen/args/args.go index eaadaa1b93f..57278583262 100644 --- a/staging/src/k8s.io/code-generator/cmd/conversion-gen/args/args.go +++ b/staging/src/k8s.io/code-generator/cmd/conversion-gen/args/args.go @@ -21,6 +21,7 @@ import ( "github.com/spf13/pflag" + "k8s.io/code-generator/pkg/apidefinitions" "k8s.io/gengo/v2" ) @@ -60,6 +61,8 @@ type Args struct { // groups of generators (external API that depends on Kube generations) should // keep tags distinct as well. GeneratedBuildTag string + + apidefinitions.LintArgs } // New returns default arguments for the generator. @@ -84,6 +87,7 @@ func (args *Args) AddFlags(fs *pflag.FlagSet) { fs.StringVar(&args.GoHeaderFile, "go-header-file", "", "the path to a file containing boilerplate header text; the string \"YEAR\" will be replaced with the current 4-digit year") fs.StringVar(&args.GeneratedBuildTag, "build-tag", args.GeneratedBuildTag, "A Go build tag to use to identify files generated by this command. Should be unique.") + apidefinitions.AddFlags(&args.LintArgs, fs) } // Validate checks the given arguments. @@ -91,5 +95,8 @@ func (args *Args) Validate() error { if len(args.OutputFile) == 0 { return fmt.Errorf("--output-file must be specified") } + if err := apidefinitions.ValidateFlags(args.LintRules); err != nil { + return err + } return nil } diff --git a/staging/src/k8s.io/code-generator/cmd/conversion-gen/generators/conversion.go b/staging/src/k8s.io/code-generator/cmd/conversion-gen/generators/conversion.go index 8ce11392d6c..640fb0adab3 100644 --- a/staging/src/k8s.io/code-generator/cmd/conversion-gen/generators/conversion.go +++ b/staging/src/k8s.io/code-generator/cmd/conversion-gen/generators/conversion.go @@ -26,6 +26,7 @@ import ( "strings" "k8s.io/code-generator/cmd/conversion-gen/args" + "k8s.io/code-generator/pkg/apidefinitions" "k8s.io/gengo/v2" "k8s.io/gengo/v2/generator" "k8s.io/gengo/v2/namer" @@ -43,9 +44,6 @@ const ( // e.g. "+k8s:conversion-gen:explicit-from=net/url.Values" in the type comment // will result in generating conversion from net/url.Values. explicitFromTagName = "k8s:conversion-gen:explicit-from" - // e.g., "+k8s:conversion-gen-external-types=" in doc.go, where - // is the relative path to the package the types are defined in. - externalTypesTagName = "k8s:conversion-gen-external-types" ) func extractTagValues(tagName string, comments []string) ([]string, error) { @@ -72,10 +70,6 @@ func extractExplicitFromTag(comments []string) ([]string, error) { return extractTagValues(explicitFromTagName, comments) } -func extractExternalTypesTag(comments []string) ([]string, error) { - return extractTagValues(externalTypesTagName, comments) -} - func isCopyOnly(comments []string) (bool, error) { values, err := extractTagValues("k8s:conversion-fn", comments) if err != nil { @@ -223,7 +217,12 @@ func GetTargets(context *generator.Context, args *args.Args) []generator.Target klog.Fatalf("Failed loading boilerplate: %v", err) } - targets := []generator.Target{} + var idOpts []apidefinitions.Option + if len(args.LintRules) > 0 { + idOpts = append(idOpts, apidefinitions.WithLintRules(args.LintRules...)) + } + + targetList := []generator.Target{} // Accumulate pre-existing conversion functions. // TODO: This is too ad-hoc. We need a better way. @@ -243,54 +242,36 @@ func GetTargets(context *generator.Context, args *args.Args) []generator.Target otherPkgs := make([]string, 0, len(context.Inputs)) pkgToPeers := map[string][]string{} pkgToExternal := map[string]string{} - for _, i := range context.Inputs { - klog.V(3).Infof("pre-processing pkg %q", i) + for _, i := range context.Inputs { + klog.V(3).Infof("considering pkg %q", i) pkg := context.Universe[i] - // Only generate conversions for packages which explicitly request it - // by specifying one or more "+k8s:conversion-gen=" - // in their doc.go file. - peerPkgs, err := extractTag(pkg.Comments) - if peerPkgs == nil { + info, err := apidefinitions.Identify(pkg, apidefinitions.Conversion, idOpts...) + if err != nil { + klog.Fatal(err) + } + if !info.ShouldGenerate() { klog.V(3).Infof(" no tag") continue } - if err != nil { - klog.Errorf("failed to extract tag %s", err) - continue - } - klog.V(3).Infof(" tags: %q", peerPkgs) - if len(peerPkgs) == 1 && peerPkgs[0] == "false" { - // If a single +k8s:conversion-gen=false tag is defined, we still want - // the generator to fire for this package for explicit conversions, but - // we are clearing the peerPkgs to not generate any standard conversions. - peerPkgs = nil - } else { - // Save peers for each input - pkgToPeers[i] = peerPkgs - } - otherPkgs = append(otherPkgs, peerPkgs...) - // Keep this one for further processing. filteredInputs = append(filteredInputs, i) - // if the external types are not in the same package where the - // conversion functions to be generated - externalTypesValues, err := extractExternalTypesTag(pkg.Comments) - if err != nil { - klog.Fatalf("Failed to extract external types tag for package %q: %v", i, err) + // Sole +k8s:conversion-gen=false: emit only the package's + // hand-written conversions, no peer-driven standard conversions. + if !info.IsExplicitOnly() { + peerPkgs := info.PeerPackages() + klog.V(3).Infof(" peers: %q", peerPkgs) + pkgToPeers[i] = peerPkgs + otherPkgs = append(otherPkgs, peerPkgs...) } - if externalTypesValues != nil { - if len(externalTypesValues) != 1 { - klog.Fatalf(" expect only one value for %q tag, got: %q", externalTypesTagName, externalTypesValues) - } - externalTypes := externalTypesValues[0] - klog.V(3).Infof(" external types tags: %q", externalTypes) + + externalTypes := info.ExternalTypes() + if externalTypes != i { + klog.V(3).Infof(" external types: %q", externalTypes) otherPkgs = append(otherPkgs, externalTypes) - pkgToExternal[i] = externalTypes - } else { - pkgToExternal[i] = i } + pkgToExternal[i] = externalTypes } // Make sure explicit peer-packages are added. @@ -346,7 +327,7 @@ func GetTargets(context *generator.Context, args *args.Args) []generator.Target unsafeEquality = noEquality{} } - targets = append(targets, + targetList = append(targetList, &generator.SimpleTarget{ PkgName: path.Base(pkg.Path), PkgPath: pkg.Path, @@ -377,7 +358,7 @@ func GetTargets(context *generator.Context, args *args.Args) []generator.Target memoryEquivalentTypes.Skip(k.inType, k.outType) } - return targets + return targetList } type equalMemoryTypes map[conversionPair]bool diff --git a/staging/src/k8s.io/code-generator/cmd/deepcopy-gen/args/args.go b/staging/src/k8s.io/code-generator/cmd/deepcopy-gen/args/args.go index a437189b586..90080c391d6 100644 --- a/staging/src/k8s.io/code-generator/cmd/deepcopy-gen/args/args.go +++ b/staging/src/k8s.io/code-generator/cmd/deepcopy-gen/args/args.go @@ -20,11 +20,15 @@ import ( "fmt" "github.com/spf13/pflag" + + "k8s.io/code-generator/pkg/apidefinitions" ) type Args struct { OutputFile string GoHeaderFile string + + apidefinitions.LintArgs } // New returns default arguments for the generator. @@ -38,6 +42,7 @@ func (args *Args) AddFlags(fs *pflag.FlagSet) { "the name of the file to be generated") fs.StringVar(&args.GoHeaderFile, "go-header-file", "", "the path to a file containing boilerplate header text; the string \"YEAR\" will be replaced with the current 4-digit year") + apidefinitions.AddFlags(&args.LintArgs, fs) } // Validate checks the given arguments. @@ -45,5 +50,8 @@ func (args *Args) Validate() error { if len(args.OutputFile) == 0 { return fmt.Errorf("--output-file must be specified") } + if err := apidefinitions.ValidateFlags(args.LintRules); err != nil { + return err + } return nil } diff --git a/staging/src/k8s.io/code-generator/cmd/deepcopy-gen/generators/deepcopy.go b/staging/src/k8s.io/code-generator/cmd/deepcopy-gen/generators/deepcopy.go index 4b1a78cd7b6..e1ed4f3c0dc 100644 --- a/staging/src/k8s.io/code-generator/cmd/deepcopy-gen/generators/deepcopy.go +++ b/staging/src/k8s.io/code-generator/cmd/deepcopy-gen/generators/deepcopy.go @@ -24,6 +24,7 @@ import ( "strings" "k8s.io/code-generator/cmd/deepcopy-gen/args" + "k8s.io/code-generator/pkg/apidefinitions" genutil "k8s.io/code-generator/pkg/util" "k8s.io/gengo/v2" "k8s.io/gengo/v2/generator" @@ -128,13 +129,28 @@ func GetTargets(context *generator.Context, args *args.Args) []generator.Target klog.Fatalf("Failed loading boilerplate: %v", err) } - targets := []generator.Target{} + var idOpts []apidefinitions.Option + if len(args.LintRules) > 0 { + idOpts = append(idOpts, apidefinitions.WithLintRules(args.LintRules...)) + } + + targetList := []generator.Target{} for _, i := range context.Inputs { - klog.V(3).Infof("Considering pkg %q", i) - + klog.V(3).Infof("considering pkg %q", i) pkg := context.Universe[i] + info, err := apidefinitions.Identify(pkg, apidefinitions.Deepcopy, idOpts...) + if err != nil { + klog.Fatal(err) + } + if !info.ShouldGenerate() { + klog.V(3).Infof(" inactive (no +k8s:deepcopy-gen, no type-level opt-in, or =false)") + continue + } + + // extractEnabledTag also parses the comma-separated subparams + // (e.g. ",register=true"), which Target.Values does not. ptag := extractEnabledTag(pkg.Comments) ptagValue := "" ptagRegister := false @@ -189,7 +205,7 @@ func GetTargets(context *generator.Context, args *args.Args) []generator.Target if pkgNeedsGeneration { klog.V(3).Infof("Package %q needs generation", i) - targets = append(targets, + targetList = append(targetList, &generator.SimpleTarget{ PkgName: strings.Split(path.Base(pkg.Path), ".")[0], PkgPath: pkg.Path, @@ -206,7 +222,7 @@ func GetTargets(context *generator.Context, args *args.Args) []generator.Target }) } } - return targets + return targetList } // genDeepCopy produces a file with autogenerated deep-copy functions. diff --git a/staging/src/k8s.io/code-generator/cmd/deepcopy-gen/generators/deepcopy_targets_test.go b/staging/src/k8s.io/code-generator/cmd/deepcopy-gen/generators/deepcopy_targets_test.go new file mode 100644 index 00000000000..c6d5024eab8 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/deepcopy-gen/generators/deepcopy_targets_test.go @@ -0,0 +1,223 @@ +/* +Copyright 2025 The Kubernetes 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 generators + +import ( + "reflect" + "sort" + "testing" + + "k8s.io/code-generator/cmd/deepcopy-gen/args" + "k8s.io/gengo/v2/generator" + "k8s.io/gengo/v2/types" +) + +// copyableStruct returns a struct type with a single primitive int32 field. +// copyableType in deepcopy.go accepts a non-private struct kind. +func copyableStruct(pkgPath, name string, comments []string) *types.Type { + return &types.Type{ + Name: types.Name{Package: pkgPath, Name: name}, + Kind: types.Struct, + CommentLines: comments, + Members: []types.Member{ + {Name: "X", Type: types.Int32}, + }, + } +} + +func TestGetTargets(t *testing.T) { + type pkgSpec struct { + path string + comments []string + // types maps type-name to its CommentLines (a copyable struct is + // synthesized for each entry). nil means no types in the package. + types map[string][]string + } + + cases := []struct { + name string + pkgs []pkgSpec + wantPkgs []string + // wantAllTypes maps PkgPath -> expected genDeepCopy.allTypes value. + wantAllTypes map[string]bool + // wantRegister maps PkgPath -> expected genDeepCopy.registerTypes value. + wantRegister map[string]bool + }{ + { + name: "package tag with copyable struct activates", + pkgs: []pkgSpec{ + { + path: "example.com/pkg/a", + comments: []string{"+k8s:deepcopy-gen=package"}, + types: map[string][]string{"T": nil}, + }, + }, + wantPkgs: []string{"example.com/pkg/a"}, + wantAllTypes: map[string]bool{"example.com/pkg/a": true}, + wantRegister: map[string]bool{"example.com/pkg/a": false}, + }, + { + name: "package tag with register=false activates with register=false", + pkgs: []pkgSpec{ + { + path: "example.com/pkg/b", + comments: []string{"+k8s:deepcopy-gen=package,register=false"}, + types: map[string][]string{"T": nil}, + }, + }, + wantPkgs: []string{"example.com/pkg/b"}, + wantAllTypes: map[string]bool{"example.com/pkg/b": true}, + wantRegister: map[string]bool{"example.com/pkg/b": false}, + }, + { + name: "package tag with register=true activates with register=true", + pkgs: []pkgSpec{ + { + path: "example.com/pkg/c", + comments: []string{"+k8s:deepcopy-gen=package,register=true"}, + types: map[string][]string{"T": nil}, + }, + }, + wantPkgs: []string{"example.com/pkg/c"}, + wantAllTypes: map[string]bool{"example.com/pkg/c": true}, + wantRegister: map[string]bool{"example.com/pkg/c": true}, + }, + { + name: "package opted out is skipped", + pkgs: []pkgSpec{ + { + path: "example.com/pkg/d", + comments: []string{"+k8s:deepcopy-gen=false"}, + types: map[string][]string{"T": nil}, + }, + }, + wantPkgs: nil, + }, + { + name: "no package tag but type opts in activates", + pkgs: []pkgSpec{ + { + path: "example.com/pkg/e", + types: map[string][]string{ + "T": {"+k8s:deepcopy-gen=true"}, + }, + }, + }, + wantPkgs: []string{"example.com/pkg/e"}, + wantAllTypes: map[string]bool{"example.com/pkg/e": false}, + wantRegister: map[string]bool{"example.com/pkg/e": false}, + }, + { + name: "package tag but no copyable types is skipped", + pkgs: []pkgSpec{ + { + path: "example.com/pkg/f", + comments: []string{"+k8s:deepcopy-gen=package"}, + types: nil, + }, + }, + wantPkgs: nil, + }, + { + name: "no tag and no opt-in types is skipped", + pkgs: []pkgSpec{ + { + path: "example.com/pkg/g", + types: map[string][]string{"T": nil}, + }, + }, + wantPkgs: nil, + }, + // Ecosystem regression: a third-party generator's tag in the same + // doc.go must NOT cause deepcopy-gen to fail. The deepcopy-gen + // tag still activates as expected. + { + name: "foreign third-party generator tag is ignored", + pkgs: []pkgSpec{ + { + path: "example.com/pkg/h", + comments: []string{ + "+k8s:my-custom-gen=value", + "+k8s:deepcopy-gen=package", + }, + types: map[string][]string{"T": nil}, + }, + }, + wantPkgs: []string{"example.com/pkg/h"}, + wantAllTypes: map[string]bool{"example.com/pkg/h": true}, + wantRegister: map[string]bool{"example.com/pkg/h": false}, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + universe := types.Universe{} + var inputs []string + for _, ps := range tc.pkgs { + pkg := &types.Package{ + Path: ps.path, + Dir: ps.path, + Name: "pkg", + Comments: ps.comments, + Types: map[string]*types.Type{}, + } + for tname, tcomments := range ps.types { + pkg.Types[tname] = copyableStruct(ps.path, tname, tcomments) + } + universe[ps.path] = pkg + inputs = append(inputs, ps.path) + } + + ctx := &generator.Context{ + Universe: universe, + Inputs: inputs, + } + + result := GetTargets(ctx, args.New()) + + var gotPkgs []string + for _, tgt := range result { + gotPkgs = append(gotPkgs, tgt.Path()) + } + sort.Strings(gotPkgs) + want := append([]string(nil), tc.wantPkgs...) + sort.Strings(want) + if !reflect.DeepEqual(gotPkgs, want) { + t.Errorf("PkgPaths = %v, want %v", gotPkgs, want) + } + + for _, tgt := range result { + gens := tgt.Generators(ctx) + if len(gens) != 1 { + t.Errorf("pkg %q: got %d generators, want 1", tgt.Path(), len(gens)) + continue + } + gdc, ok := gens[0].(*genDeepCopy) + if !ok { + t.Errorf("pkg %q: generator type = %T, want *genDeepCopy", tgt.Path(), gens[0]) + continue + } + if want, ok := tc.wantAllTypes[tgt.Path()]; ok && gdc.allTypes != want { + t.Errorf("pkg %q: allTypes = %v, want %v", tgt.Path(), gdc.allTypes, want) + } + if want, ok := tc.wantRegister[tgt.Path()]; ok && gdc.registerTypes != want { + t.Errorf("pkg %q: registerTypes = %v, want %v", tgt.Path(), gdc.registerTypes, want) + } + } + }) + } +} diff --git a/staging/src/k8s.io/code-generator/cmd/defaulter-gen/args/args.go b/staging/src/k8s.io/code-generator/cmd/defaulter-gen/args/args.go index 8d8dfe97f42..1163a2bd28d 100644 --- a/staging/src/k8s.io/code-generator/cmd/defaulter-gen/args/args.go +++ b/staging/src/k8s.io/code-generator/cmd/defaulter-gen/args/args.go @@ -21,6 +21,7 @@ import ( "github.com/spf13/pflag" + "k8s.io/code-generator/pkg/apidefinitions" "k8s.io/gengo/v2" ) @@ -34,6 +35,8 @@ type Args struct { // groups of generators (external API that depends on Kube generations) should // keep tags distinct as well. GeneratedBuildTag string + + apidefinitions.LintArgs } // New returns default arguments for the generator. @@ -52,6 +55,7 @@ func (args *Args) AddFlags(fs *pflag.FlagSet) { fs.StringVar(&args.GoHeaderFile, "go-header-file", "", "the path to a file containing boilerplate header text; the string \"YEAR\" will be replaced with the current 4-digit year") fs.StringVar(&args.GeneratedBuildTag, "build-tag", args.GeneratedBuildTag, "A Go build tag to use to identify files generated by this command. Should be unique.") + apidefinitions.AddFlags(&args.LintArgs, fs) } // Validate checks the given arguments. @@ -59,6 +63,9 @@ func (args *Args) Validate() error { if len(args.OutputFile) == 0 { return fmt.Errorf("--output-file must be specified") } + if err := apidefinitions.ValidateFlags(args.LintRules); err != nil { + return err + } return nil } diff --git a/staging/src/k8s.io/code-generator/cmd/defaulter-gen/generators/defaulter.go b/staging/src/k8s.io/code-generator/cmd/defaulter-gen/generators/defaulter.go index 0ce7d17f7dc..488f88714ec 100644 --- a/staging/src/k8s.io/code-generator/cmd/defaulter-gen/generators/defaulter.go +++ b/staging/src/k8s.io/code-generator/cmd/defaulter-gen/generators/defaulter.go @@ -28,6 +28,7 @@ import ( "strings" "k8s.io/code-generator/cmd/defaulter-gen/args" + "k8s.io/code-generator/pkg/apidefinitions" genutil "k8s.io/code-generator/pkg/util" "k8s.io/gengo/v2" "k8s.io/gengo/v2/generator" @@ -62,7 +63,6 @@ var typeZeroValue = map[string]interface{}{ // These are the comment tags that carry parameters for defaulter generation. const tagName = "k8s:defaulter-gen" -const inputTagName = "k8s:defaulter-gen-input" const defaultTagName = "default" func extractDefaultTag(comments []string) ([]string, error) { @@ -87,12 +87,17 @@ func extractTag(comments []string) ([]string, bool) { return values, true } -func extractInputTag(comments []string) ([]string, error) { - tags, err := genutil.ExtractCommentTagsWithoutArguments("+", []string{inputTagName}, comments) +// defaulterMatchType returns the values to be defaulted for pkg, or false +// if defaulter-gen should not run. +func defaulterMatchType(pkg *types.Package, idOpts []apidefinitions.Option) ([]string, bool) { + info, err := apidefinitions.Identify(pkg, apidefinitions.Defaulter, idOpts...) if err != nil { - return nil, err + klog.Fatal(err) } - return tags[inputTagName], nil + if !info.ShouldGenerate() { + return nil, false + } + return info.TypeFilters(), true } func checkTag(comments []string, require ...string) (bool, error) { @@ -250,7 +255,12 @@ func GetTargets(context *generator.Context, args *args.Args) []generator.Target klog.Fatalf("Failed loading boilerplate: %v", err) } - targets := []generator.Target{} + var idOpts []apidefinitions.Option + if len(args.LintRules) > 0 { + idOpts = append(idOpts, apidefinitions.WithLintRules(args.LintRules...)) + } + + targetList := []generator.Target{} // Accumulate pre-existing default functions. // TODO: This is too ad-hoc. We need a better way. @@ -265,26 +275,20 @@ func GetTargets(context *generator.Context, args *args.Args) []generator.Target pkgToInput := map[string]string{} for _, i := range context.Inputs { klog.V(5).Infof("considering pkg %q", i) - pkg := context.Universe[i] - // if the types are not in the same package where the defaulter functions to be generated - inputTags, err := extractInputTag(pkg.Comments) + info, err := apidefinitions.Identify(pkg, apidefinitions.Defaulter, idOpts...) if err != nil { - panic(fmt.Sprintf("error extracting input tag: %v", err)) + klog.Fatal(err) } - if len(inputTags) > 1 { - panic(fmt.Sprintf("there may only be one input tag, got %#v", inputTags)) + if !info.ShouldGenerate() { + continue } - if len(inputTags) == 1 { - inputPath := inputTags[0] - if strings.HasPrefix(inputPath, "./") || strings.HasPrefix(inputPath, "../") { - // this is a relative dir, which will not work under gomodules. - // join with the local package path, but warn - klog.Warningf("relative path %s=%s will not work under gomodule mode; use full package path (as used by 'import') instead", inputTagName, inputPath) - inputPath = path.Join(pkg.Path, inputTags[0]) - } + // +k8s:defaulter-gen-input may direct the generator at types in + // a different package than the one where defaulters will be emitted. + inputPath := info.ExternalTypes() + if inputPath != pkg.Path { klog.V(5).Infof(" input pkg %v", inputPath) inputPkgs = append(inputPkgs, inputPath) pkgToInput[i] = inputPath @@ -336,7 +340,7 @@ func GetTargets(context *generator.Context, args *args.Args) []generator.Target getManualDefaultingFunctions(context, context.Universe[pp], existingDefaulters) } - typesWith, found := extractTag(pkg.Comments) + typesWith, found := defaulterMatchType(pkg, idOpts) if !found { klog.V(2).InfoS(" did not find required tag", "tag", tagName) continue @@ -443,9 +447,12 @@ func GetTargets(context *generator.Context, args *args.Args) []generator.Target if len(newDefaulters) == 0 { klog.V(5).Infof("no defaulters in package %s", pkg.Name) + if _, hasTag := extractTag(pkg.Comments); !hasTag { + continue + } } - targets = append(targets, + targetList = append(targetList, &generator.SimpleTarget{ PkgName: path.Base(pkg.Path), PkgPath: pkg.Path, @@ -463,7 +470,7 @@ func GetTargets(context *generator.Context, args *args.Args) []generator.Target }, }) } - return targets + return targetList } // callTreeForType contains fields necessary to build a tree for types. diff --git a/staging/src/k8s.io/code-generator/cmd/informer-gen/args/args.go b/staging/src/k8s.io/code-generator/cmd/informer-gen/args/args.go index 8052578c543..a3b6dd9079a 100644 --- a/staging/src/k8s.io/code-generator/cmd/informer-gen/args/args.go +++ b/staging/src/k8s.io/code-generator/cmd/informer-gen/args/args.go @@ -20,6 +20,8 @@ import ( "fmt" "github.com/spf13/pflag" + + "k8s.io/code-generator/pkg/apidefinitions" ) // Args is used by the gengo framework to pass args specific to this generator. @@ -35,6 +37,8 @@ type Args struct { // PluralExceptions define a list of pluralizer exceptions in Type:PluralType format. // The default list is "Endpoints:Endpoints" PluralExceptions []string + + apidefinitions.LintArgs } // New returns default arguments for the generator. @@ -62,6 +66,7 @@ func (args *Args) AddFlags(fs *pflag.FlagSet) { "if true, omit the intermediate \"internalversion\" and \"externalversions\" subdirectories") fs.StringSliceVar(&args.PluralExceptions, "plural-exceptions", args.PluralExceptions, "list of comma separated plural exception definitions in Type:PluralizedType format") + apidefinitions.AddFlags(&args.LintArgs, fs) } // Validate checks the given arguments. @@ -78,5 +83,8 @@ func (args *Args) Validate() error { if len(args.ListersPackage) == 0 { return fmt.Errorf("--listers-package must be specified") } + if err := apidefinitions.ValidateFlags(args.LintRules); err != nil { + return err + } return nil } diff --git a/staging/src/k8s.io/code-generator/cmd/informer-gen/generators/targets.go b/staging/src/k8s.io/code-generator/cmd/informer-gen/generators/targets.go index cdba8e83563..62790012806 100644 --- a/staging/src/k8s.io/code-generator/cmd/informer-gen/generators/targets.go +++ b/staging/src/k8s.io/code-generator/cmd/informer-gen/generators/targets.go @@ -17,6 +17,7 @@ limitations under the License. package generators import ( + "errors" "fmt" "path" "path/filepath" @@ -25,6 +26,7 @@ import ( "k8s.io/code-generator/cmd/client-gen/generators/util" clientgentypes "k8s.io/code-generator/cmd/client-gen/types" "k8s.io/code-generator/cmd/informer-gen/args" + "k8s.io/code-generator/pkg/apidefinitions" genutil "k8s.io/code-generator/pkg/util" "k8s.io/gengo/v2" "k8s.io/gengo/v2/generator" @@ -104,6 +106,11 @@ func GetTargets(context *generator.Context, args *args.Args) []generator.Target externalVersionOutputPkg = path.Join(externalVersionOutputPkg, "externalversions") } + var idOpts []apidefinitions.Option + if len(args.LintRules) > 0 { + idOpts = append(idOpts, apidefinitions.WithLintRules(args.LintRules...)) + } + var targetList []generator.Target typesForGroupVersion := make(map[clientgentypes.GroupVersion][]*types.Type) @@ -113,6 +120,14 @@ func GetTargets(context *generator.Context, args *args.Args) []generator.Target for _, inputPkg := range context.Inputs { p := context.Universe.Package(inputPkg) + info, err := apidefinitions.Identify(p, apidefinitions.Informer, idOpts...) + if err != nil { + klog.Fatal(err) + } + if !info.ShouldGenerate() { + continue + } + objectMeta, internal, err := objectMetaForPackage(p) if err != nil { klog.Fatal(err) @@ -144,23 +159,23 @@ func GetTargets(context *generator.Context, args *args.Args) []generator.Target // If there's a comment of the form "// +groupName=somegroup" or // "// +groupName=somegroup.foo.bar.io", use the first field (somegroup) as the name of the // group when generating. - override, err := genutil.ExtractCommentTagsWithoutArguments("+", []string{"groupName"}, p.Comments) - if err != nil { - klog.Fatalf("error extracting groupName tags: %v", err) + override, err := apidefinitions.GroupNameForPackage(p.Comments) + if err != nil && !errors.Is(err, apidefinitions.ErrGroupUndeclared) { + klog.Fatalf("error resolving group name: %v", err) } - if override["groupName"] != nil { - gv.Group = clientgentypes.Group(override["groupName"][0]) + if err == nil { + gv.Group = clientgentypes.Group(override) } // If there's a comment of the form "// +groupGoName=SomeUniqueShortName", use that as // the Go group identifier in CamelCase. It defaults groupGoNames[groupPackageName] = namer.IC(strings.Split(gv.Group.NonEmpty(), ".")[0]) - override, err = genutil.ExtractCommentTagsWithoutArguments("+", []string{"groupGoName"}, p.Comments) + goName, err := genutil.ExtractCommentTagsWithoutArguments("+", []string{"groupGoName"}, p.Comments) if err != nil { klog.Fatalf("error extracting groupGoName tags: %v", err) } - if override["groupGoName"] != nil { - groupGoNames[groupPackageName] = namer.IC(override["groupGoName"][0]) + if goName["groupGoName"] != nil { + groupGoNames[groupPackageName] = namer.IC(goName["groupGoName"][0]) } var typesToGenerate []*types.Type diff --git a/staging/src/k8s.io/code-generator/cmd/lister-gen/args/args.go b/staging/src/k8s.io/code-generator/cmd/lister-gen/args/args.go index e6b9e00ab92..899ea6c664f 100644 --- a/staging/src/k8s.io/code-generator/cmd/lister-gen/args/args.go +++ b/staging/src/k8s.io/code-generator/cmd/lister-gen/args/args.go @@ -20,6 +20,8 @@ import ( "fmt" "github.com/spf13/pflag" + + "k8s.io/code-generator/pkg/apidefinitions" ) // Args is used by the gengo framework to pass args specific to this generator. @@ -31,6 +33,8 @@ type Args struct { // PluralExceptions specify list of exceptions used when pluralizing certain types. // For example 'Endpoints:Endpoints', otherwise the pluralizer will generate 'Endpointes'. PluralExceptions []string + + apidefinitions.LintArgs } // New returns default arguments for the generator. @@ -48,6 +52,7 @@ func (args *Args) AddFlags(fs *pflag.FlagSet) { "list of comma separated plural exception definitions in Type:PluralizedType format") fs.StringVar(&args.GoHeaderFile, "go-header-file", "", "the path to a file containing boilerplate header text; the string \"YEAR\" will be replaced with the current 4-digit year") + apidefinitions.AddFlags(&args.LintArgs, fs) } // Validate checks the given arguments. @@ -58,5 +63,8 @@ func (args *Args) Validate() error { if len(args.OutputPkg) == 0 { return fmt.Errorf("--output-pkg must be specified") } + if err := apidefinitions.ValidateFlags(args.LintRules); err != nil { + return err + } return nil } diff --git a/staging/src/k8s.io/code-generator/cmd/lister-gen/generators/lister.go b/staging/src/k8s.io/code-generator/cmd/lister-gen/generators/lister.go index 3a28115fa5b..fb8785d2c99 100644 --- a/staging/src/k8s.io/code-generator/cmd/lister-gen/generators/lister.go +++ b/staging/src/k8s.io/code-generator/cmd/lister-gen/generators/lister.go @@ -17,6 +17,7 @@ limitations under the License. package generators import ( + "errors" "fmt" "io" "path" @@ -26,7 +27,7 @@ import ( "k8s.io/code-generator/cmd/client-gen/generators/util" clientgentypes "k8s.io/code-generator/cmd/client-gen/types" "k8s.io/code-generator/cmd/lister-gen/args" - genutil "k8s.io/code-generator/pkg/util" + "k8s.io/code-generator/pkg/apidefinitions" "k8s.io/gengo/v2" "k8s.io/gengo/v2/generator" "k8s.io/gengo/v2/namer" @@ -67,10 +68,23 @@ func GetTargets(context *generator.Context, args *args.Args) []generator.Target klog.Fatalf("Failed loading boilerplate: %v", err) } + var idOpts []apidefinitions.Option + if len(args.LintRules) > 0 { + idOpts = append(idOpts, apidefinitions.WithLintRules(args.LintRules...)) + } + var targetList []generator.Target for _, inputPkg := range context.Inputs { p := context.Universe.Package(inputPkg) + info, err := apidefinitions.Identify(p, apidefinitions.Lister, idOpts...) + if err != nil { + klog.Fatal(err) + } + if !info.ShouldGenerate() { + continue + } + objectMeta, internal, err := objectMetaForPackage(p) if err != nil { klog.Fatal(err) @@ -102,12 +116,12 @@ func GetTargets(context *generator.Context, args *args.Args) []generator.Target // If there's a comment of the form "// +groupName=somegroup" or // "// +groupName=somegroup.foo.bar.io", use the first field (somegroup) as the name of the // group when generating. - override, err := genutil.ExtractCommentTagsWithoutArguments("+", []string{"groupName"}, p.Comments) - if err != nil { - klog.Fatalf("error extracting groupName tags: %v", err) + override, err := apidefinitions.GroupNameForPackage(p.Comments) + if err != nil && !errors.Is(err, apidefinitions.ErrGroupUndeclared) { + klog.Fatalf("error resolving group name: %v", err) } - if override["groupName"] != nil { - gv.Group = clientgentypes.Group(strings.SplitN(override["groupName"][0], ".", 2)[0]) + if err == nil { + gv.Group = clientgentypes.Group(strings.SplitN(override, ".", 2)[0]) } var typesToGenerate []*types.Type diff --git a/staging/src/k8s.io/code-generator/cmd/prerelease-lifecycle-gen/args/args.go b/staging/src/k8s.io/code-generator/cmd/prerelease-lifecycle-gen/args/args.go index 412dd534c5e..b4448303cc3 100644 --- a/staging/src/k8s.io/code-generator/cmd/prerelease-lifecycle-gen/args/args.go +++ b/staging/src/k8s.io/code-generator/cmd/prerelease-lifecycle-gen/args/args.go @@ -20,11 +20,15 @@ import ( "fmt" "github.com/spf13/pflag" + + "k8s.io/code-generator/pkg/apidefinitions" ) type Args struct { OutputFile string GoHeaderFile string + + apidefinitions.LintArgs } // New returns default arguments for the generator. @@ -38,6 +42,7 @@ func (args *Args) AddFlags(fs *pflag.FlagSet) { "the name of the file to be generated") fs.StringVar(&args.GoHeaderFile, "go-header-file", "", "the path to a file containing boilerplate header text; the string \"YEAR\" will be replaced with the current 4-digit year") + apidefinitions.AddFlags(&args.LintArgs, fs) } // Validate checks the given arguments. @@ -45,6 +50,8 @@ func (args *Args) Validate() error { if len(args.OutputFile) == 0 { return fmt.Errorf("--output-file must be specified") } - + if err := apidefinitions.ValidateFlags(args.LintRules); err != nil { + return err + } return nil } diff --git a/staging/src/k8s.io/code-generator/cmd/prerelease-lifecycle-gen/prerelease-lifecycle-generators/status.go b/staging/src/k8s.io/code-generator/cmd/prerelease-lifecycle-gen/prerelease-lifecycle-generators/status.go index 580bfc68c65..13f834dbf26 100644 --- a/staging/src/k8s.io/code-generator/cmd/prerelease-lifecycle-gen/prerelease-lifecycle-generators/status.go +++ b/staging/src/k8s.io/code-generator/cmd/prerelease-lifecycle-gen/prerelease-lifecycle-generators/status.go @@ -25,6 +25,7 @@ import ( "strings" "k8s.io/code-generator/cmd/prerelease-lifecycle-gen/args" + "k8s.io/code-generator/pkg/apidefinitions" genutil "k8s.io/code-generator/pkg/util" "k8s.io/gengo/v2" "k8s.io/gengo/v2/generator" @@ -192,64 +193,44 @@ func GetTargets(context *generator.Context, args *args.Args) []generator.Target klog.Fatalf("Failed loading boilerplate: %v", err) } - targets := []generator.Target{} + var idOpts []apidefinitions.Option + if len(args.LintRules) > 0 { + idOpts = append(idOpts, apidefinitions.WithLintRules(args.LintRules...)) + } + + targetList := []generator.Target{} for _, i := range context.Inputs { - klog.V(5).Infof("Considering pkg %q", i) - + klog.V(5).Infof("considering pkg %q", i) pkg := context.Universe[i] - ptag := extractTag(tagEnabledName, pkg.Comments) - pkgNeedsGeneration := false - if ptag != nil { - pkgNeedsGeneration, err = strconv.ParseBool(ptag.value) - if err != nil { - klog.Fatalf("Package %v: unsupported %s value: %q :%v", i, tagEnabledName, ptag.value, err) - } + info, err := apidefinitions.Identify(pkg, apidefinitions.PrereleaseLifecycle, idOpts...) + if err != nil { + klog.Fatal(err) } - if !pkgNeedsGeneration { - klog.V(5).Infof(" skipping package") + if !info.ShouldGenerate() { + klog.V(5).Infof(" not enabled") continue } - klog.V(3).Infof("Generating package %q", pkg.Path) + klog.V(3).Infof("generating package %q", pkg.Path) - // If the pkg-scoped tag says to generate, we can skip scanning types. - if !pkgNeedsGeneration { - // If the pkg-scoped tag did not exist, scan all types for one that - // explicitly wants generation. - for _, t := range pkg.Types { - klog.V(5).Infof(" considering type %q", t.Name.String()) - ttag := extractEnabledTypeTag(t) - if ttag != nil && ttag.value == "true" { - klog.V(5).Infof(" tag=true") - if !isAPIType(t) { - klog.Fatalf("Type %v requests prerelease generation but is not an API type", t) + targetList = append(targetList, + &generator.SimpleTarget{ + PkgName: strings.Split(path.Base(pkg.Path), ".")[0], + PkgPath: pkg.Path, + PkgDir: pkg.Dir, // output pkg is the same as the input + HeaderComment: boilerplate, + FilterFunc: func(c *generator.Context, t *types.Type) bool { + return t.Name.Package == pkg.Path + }, + GeneratorsFunc: func(c *generator.Context) (generators []generator.Generator) { + return []generator.Generator{ + NewPrereleaseLifecycleGen(args.OutputFile, pkg.Path), } - pkgNeedsGeneration = true - break - } - } - } - - if pkgNeedsGeneration { - targets = append(targets, - &generator.SimpleTarget{ - PkgName: strings.Split(path.Base(pkg.Path), ".")[0], - PkgPath: pkg.Path, - PkgDir: pkg.Dir, // output pkg is the same as the input - HeaderComment: boilerplate, - FilterFunc: func(c *generator.Context, t *types.Type) bool { - return t.Name.Package == pkg.Path - }, - GeneratorsFunc: func(c *generator.Context) (generators []generator.Generator) { - return []generator.Generator{ - NewPrereleaseLifecycleGen(args.OutputFile, pkg.Path), - } - }, - }) - } + }, + }) } - return targets + return targetList } // genDeepCopy produces a file with autogenerated deep-copy functions. diff --git a/staging/src/k8s.io/code-generator/cmd/prerelease-lifecycle-gen/prerelease-lifecycle-generators/status_targets_test.go b/staging/src/k8s.io/code-generator/cmd/prerelease-lifecycle-gen/prerelease-lifecycle-generators/status_targets_test.go new file mode 100644 index 00000000000..f811f04f8c9 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/prerelease-lifecycle-gen/prerelease-lifecycle-generators/status_targets_test.go @@ -0,0 +1,168 @@ +/* +Copyright The Kubernetes 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 prereleaselifecyclegenerators + +import ( + "sort" + "testing" + + "github.com/google/go-cmp/cmp" + + "k8s.io/code-generator/cmd/prerelease-lifecycle-gen/args" + "k8s.io/gengo/v2/generator" + "k8s.io/gengo/v2/types" +) + +func TestGetTargets(t *testing.T) { + cases := []struct { + name string + pkgs map[string]*types.Package + inputs []string + wantPkgs []string + }{ + { + name: "enabled package activates", + pkgs: map[string]*types.Package{ + "example.com/api/v1": { + Path: "example.com/api/v1", + Dir: "/tmp/example/api/v1", + Comments: []string{"+k8s:prerelease-lifecycle-gen=true"}, + }, + }, + inputs: []string{"example.com/api/v1"}, + wantPkgs: []string{"example.com/api/v1"}, + }, + { + name: "sole =false opts out", + pkgs: map[string]*types.Package{ + "example.com/api/v1": { + Path: "example.com/api/v1", + Dir: "/tmp/example/api/v1", + Comments: []string{"+k8s:prerelease-lifecycle-gen=false"}, + }, + }, + inputs: []string{"example.com/api/v1"}, + wantPkgs: nil, + }, + { + name: "no relevant tag is skipped", + pkgs: map[string]*types.Package{ + "example.com/api/v1": { + Path: "example.com/api/v1", + Dir: "/tmp/example/api/v1", + Comments: []string{"+groupName=example.com"}, + }, + }, + inputs: []string{"example.com/api/v1"}, + wantPkgs: nil, + }, + { + name: "enabled with introduced subtag activates", + pkgs: map[string]*types.Package{ + "example.com/api/v1beta1": { + Path: "example.com/api/v1beta1", + Dir: "/tmp/example/api/v1beta1", + Comments: []string{ + "+k8s:prerelease-lifecycle-gen=true", + "+k8s:prerelease-lifecycle-gen:introduced=1.30", + }, + }, + }, + inputs: []string{"example.com/api/v1beta1"}, + wantPkgs: []string{"example.com/api/v1beta1"}, + }, + // Ecosystem regression: a third-party generator's tag in the same + // doc.go must NOT cause prerelease-gen to fail or skip. + { + name: "foreign third-party generator tag is ignored", + pkgs: map[string]*types.Package{ + "example.com/api/v1": { + Path: "example.com/api/v1", + Dir: "/tmp/example/api/v1", + Comments: []string{ + "+k8s:my-custom-gen=value", + "+k8s:prerelease-lifecycle-gen=true", + }, + }, + }, + inputs: []string{"example.com/api/v1"}, + wantPkgs: []string{"example.com/api/v1"}, + }, + { + name: "mix of all cases", + pkgs: map[string]*types.Package{ + "example.com/enabled/v1": { + Path: "example.com/enabled/v1", + Dir: "/tmp/enabled/v1", + Comments: []string{"+k8s:prerelease-lifecycle-gen=true"}, + }, + "example.com/optedout/v1": { + Path: "example.com/optedout/v1", + Dir: "/tmp/optedout/v1", + Comments: []string{"+k8s:prerelease-lifecycle-gen=false"}, + }, + "example.com/untagged/v1": { + Path: "example.com/untagged/v1", + Dir: "/tmp/untagged/v1", + Comments: []string{"+groupName=example.com"}, + }, + "example.com/withsubtag/v1beta1": { + Path: "example.com/withsubtag/v1beta1", + Dir: "/tmp/withsubtag/v1beta1", + Comments: []string{ + "+k8s:prerelease-lifecycle-gen=true", + "+k8s:prerelease-lifecycle-gen:introduced=1.30", + }, + }, + }, + inputs: []string{ + "example.com/enabled/v1", + "example.com/optedout/v1", + "example.com/untagged/v1", + "example.com/withsubtag/v1beta1", + }, + wantPkgs: []string{ + "example.com/enabled/v1", + "example.com/withsubtag/v1beta1", + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ctx := &generator.Context{ + Universe: types.Universe(tc.pkgs), + Inputs: tc.inputs, + } + a := &args.Args{OutputFile: "zz_generated.prerelease_lifecycle.go"} + + got := GetTargets(ctx, a) + + var gotPkgs []string + for _, tgt := range got { + gotPkgs = append(gotPkgs, tgt.Path()) + } + sort.Strings(gotPkgs) + want := append([]string(nil), tc.wantPkgs...) + sort.Strings(want) + + if diff := cmp.Diff(want, gotPkgs); diff != "" { + t.Errorf("GetTargets package paths mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/staging/src/k8s.io/code-generator/cmd/register-gen/args/args.go b/staging/src/k8s.io/code-generator/cmd/register-gen/args/args.go index cc1fdafc83d..70e2b1d7390 100644 --- a/staging/src/k8s.io/code-generator/cmd/register-gen/args/args.go +++ b/staging/src/k8s.io/code-generator/cmd/register-gen/args/args.go @@ -20,11 +20,14 @@ import ( "fmt" "github.com/spf13/pflag" + "k8s.io/code-generator/pkg/apidefinitions" ) type Args struct { OutputFile string GoHeaderFile string + + apidefinitions.LintArgs } // New returns default arguments for the generator. @@ -38,6 +41,7 @@ func (args *Args) AddFlags(fs *pflag.FlagSet) { "the name of the file to be generated") fs.StringVar(&args.GoHeaderFile, "go-header-file", "", "the path to a file containing boilerplate header text; the string \"YEAR\" will be replaced with the current 4-digit year") + apidefinitions.AddFlags(&args.LintArgs, fs) } // Validate checks the given arguments. @@ -45,6 +49,9 @@ func (args *Args) Validate() error { if len(args.OutputFile) == 0 { return fmt.Errorf("output file base name cannot be empty") } + if err := apidefinitions.ValidateFlags(args.LintRules); err != nil { + return err + } return nil } diff --git a/staging/src/k8s.io/code-generator/cmd/register-gen/generators/targets.go b/staging/src/k8s.io/code-generator/cmd/register-gen/generators/targets.go index ac465f91bbc..ea8bdc4d8a9 100644 --- a/staging/src/k8s.io/code-generator/cmd/register-gen/generators/targets.go +++ b/staging/src/k8s.io/code-generator/cmd/register-gen/generators/targets.go @@ -17,6 +17,7 @@ limitations under the License. package generators import ( + "errors" "fmt" "os" "path" @@ -26,7 +27,7 @@ import ( clientgentypes "k8s.io/code-generator/cmd/client-gen/types" "k8s.io/code-generator/cmd/register-gen/args" - genutil "k8s.io/code-generator/pkg/util" + "k8s.io/code-generator/pkg/apidefinitions" "k8s.io/gengo/v2" "k8s.io/gengo/v2/generator" "k8s.io/gengo/v2/namer" @@ -51,9 +52,17 @@ func GetTargets(context *generator.Context, args *args.Args) []generator.Target klog.Fatalf("Failed loading boilerplate: %v", err) } - targets := []generator.Target{} + var idOpts []apidefinitions.Option + if len(args.LintRules) > 0 { + idOpts = append(idOpts, apidefinitions.WithLintRules(args.LintRules...)) + } + + targetList := []generator.Target{} for _, input := range context.Inputs { pkg := context.Universe.Package(input) + if !isRegisterGenTarget(pkg, idOpts) { + continue + } internal, err := isInternal(pkg) if err != nil { klog.V(5).Infof("skipping the generation of %s file, due to err %v", args.OutputFile, err) @@ -84,15 +93,13 @@ func GetTargets(context *generator.Context, args *args.Args) []generator.Target // if there is a comment of the form "// +groupName=somegroup" or "// +groupName=somegroup.foo.bar.io", // extract the fully qualified API group name from it and overwrite the group inferred from the package path - override, err := genutil.ExtractCommentTagsWithoutArguments("+", []string{"groupName"}, pkg.Comments) - if err != nil { - klog.Errorf("error extracting groupName tags: %v", err) - continue + override, err := apidefinitions.GroupNameForPackage(pkg.Comments) + if err != nil && !errors.Is(err, apidefinitions.ErrGroupUndeclared) { + klog.Fatalf("error resolving group name: %v", err) } - if override["groupName"] != nil { - groupName := override["groupName"][0] - klog.V(5).Infof("overriding the group name with = %s", groupName) - gv.Group = clientgentypes.Group(groupName) + if err == nil { + klog.V(5).Infof("overriding the group name with = %s", override) + gv.Group = clientgentypes.Group(override) } } @@ -106,7 +113,7 @@ func GetTargets(context *generator.Context, args *args.Args) []generator.Target } } - targets = append(targets, + targetList = append(targetList, &generator.SimpleTarget{ PkgName: pkg.Name, PkgPath: pkg.Path, // output to same pkg as input @@ -128,7 +135,18 @@ func GetTargets(context *generator.Context, args *args.Args) []generator.Target }) } - return targets + return targetList +} + +// isRegisterGenTarget reports whether pkg has opted in to register-gen. +// Activation rules are encoded in apidefinitions.Register's Spec +// (Boolean ActivationTag with a +groupName= fallback). +func isRegisterGenTarget(pkg *types.Package, idOpts []apidefinitions.Option) bool { + info, err := apidefinitions.Identify(pkg, apidefinitions.Register, idOpts...) + if err != nil { + klog.Fatal(err) + } + return info.ShouldGenerate() } // isInternal determines whether the given package diff --git a/staging/src/k8s.io/code-generator/cmd/register-gen/generators/targets_test.go b/staging/src/k8s.io/code-generator/cmd/register-gen/generators/targets_test.go new file mode 100644 index 00000000000..4c4f6a37ec6 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/register-gen/generators/targets_test.go @@ -0,0 +1,227 @@ +/* +Copyright 2025 The Kubernetes 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 generators + +import ( + "os" + "path/filepath" + "sort" + "testing" + + "k8s.io/code-generator/cmd/register-gen/args" + "k8s.io/gengo/v2/generator" + "k8s.io/gengo/v2/types" +) + +func TestGetTargets(t *testing.T) { + type pkgSpec struct { + path string + dir string // optional; will use t.TempDir() if needRegisterFile + comments []string + // typeMembers describes a single struct type in the package. nil means no types. + typeMembers []types.Member + // needRegisterFile, when true, creates dir/register.go before invoking GetTargets. + needRegisterFile bool + } + + externalTypeMeta := []types.Member{ + {Name: "TypeMeta", Embedded: true, Tags: `json:",inline"`}, + } + internalTypeMeta := []types.Member{ + {Name: "TypeMeta", Embedded: true}, + } + + cases := []struct { + name string + pkgs []pkgSpec + wantPaths []string // package paths expected in returned targets, sorted + wantGroups map[string]string // pkgPath -> expected group + wantHasType map[string]bool // pkgPath -> whether typesToGenerate is non-empty + }{ + { + name: "external pkg with +groupName activates", + pkgs: []pkgSpec{{ + path: "k8s.io/api/foo/v1", + comments: []string{"+groupName=foo.k8s.io"}, + typeMembers: externalTypeMeta, + }}, + wantPaths: []string{"k8s.io/api/foo/v1"}, + wantGroups: map[string]string{"k8s.io/api/foo/v1": "foo.k8s.io"}, + wantHasType: map[string]bool{"k8s.io/api/foo/v1": true}, + }, + { + name: "external pkg with +k8s:register-gen=false is opted out", + pkgs: []pkgSpec{{ + path: "k8s.io/api/foo/v1", + comments: []string{"+k8s:register-gen=false"}, + typeMembers: externalTypeMeta, + }}, + wantPaths: nil, + }, + { + name: "opt-out wins over +groupName", + pkgs: []pkgSpec{{ + path: "k8s.io/api/foo/v1", + comments: []string{"+groupName=foo.k8s.io", "+k8s:register-gen=false"}, + typeMembers: externalTypeMeta, + }}, + wantPaths: nil, + }, + { + name: "no +groupName and no +k8s:register-gen tag is skipped", + pkgs: []pkgSpec{{ + path: "k8s.io/api/foo/v1", + typeMembers: externalTypeMeta, + }}, + wantPaths: nil, + }, + { + // isInternal returns an error when the package has no TypeMeta- + // bearing types at all, and GetTargets treats that error as a skip. + name: "+groupName but no TypeMeta types is skipped (isInternal errors)", + pkgs: []pkgSpec{{ + path: "k8s.io/api/foo/v1", + comments: []string{"+groupName=foo.k8s.io"}, + }}, + wantPaths: nil, + }, + { + name: "internal pkg (TypeMeta without json tag) is skipped", + pkgs: []pkgSpec{{ + path: "k8s.io/api/foo/v1", + comments: []string{"+groupName=foo"}, + typeMembers: internalTypeMeta, + }}, + wantPaths: nil, + }, + { + name: "pkg with existing register.go is skipped", + pkgs: []pkgSpec{{ + path: "k8s.io/api/foo/v1", + comments: []string{"+groupName=foo.k8s.io"}, + typeMembers: externalTypeMeta, + needRegisterFile: true, + }}, + wantPaths: nil, + }, + // Ecosystem regression: a third-party generator's tag in the same + // doc.go must NOT cause register-gen to fail. The +groupName= still + // activates as expected. + { + name: "foreign third-party generator tag is ignored", + pkgs: []pkgSpec{{ + path: "k8s.io/api/foo/v1", + comments: []string{ + "+k8s:my-custom-gen=value", + "+groupName=foo.k8s.io", + }, + typeMembers: externalTypeMeta, + }}, + wantPaths: []string{"k8s.io/api/foo/v1"}, + wantGroups: map[string]string{"k8s.io/api/foo/v1": "foo.k8s.io"}, + wantHasType: map[string]bool{"k8s.io/api/foo/v1": true}, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + universe := types.Universe{} + ctx := &generator.Context{ + Universe: universe, + } + for i, ps := range tc.pkgs { + dir := ps.dir + if dir == "" { + dir = t.TempDir() + } + if ps.needRegisterFile { + f, err := os.Create(filepath.Join(dir, "register.go")) + if err != nil { + t.Fatalf("creating register.go for pkg[%d]: %v", i, err) + } + f.Close() + } + p := universe.Package(ps.path) + p.Name = filepath.Base(ps.path) + p.Dir = dir + p.Comments = ps.comments + if ps.typeMembers != nil { + p.Types["MyType"] = &types.Type{ + Name: types.Name{Package: ps.path, Name: "MyType"}, + Kind: types.Struct, + Members: ps.typeMembers, + } + } + ctx.Inputs = append(ctx.Inputs, ps.path) + } + + a := &args.Args{OutputFile: "zz_generated.register.go"} + got := GetTargets(ctx, a) + + gotPaths := make([]string, 0, len(got)) + for _, g := range got { + gotPaths = append(gotPaths, g.Path()) + } + sort.Strings(gotPaths) + wantPaths := append([]string(nil), tc.wantPaths...) + sort.Strings(wantPaths) + if !equalStrSlice(gotPaths, wantPaths) { + t.Fatalf("target paths: got %v, want %v", gotPaths, wantPaths) + } + + // Verify per-package group and typesToGenerate by exercising the + // generator function the SimpleTarget would run. + for _, tgt := range got { + st, ok := tgt.(*generator.SimpleTarget) + if !ok { + t.Fatalf("%s: target is %T, want *generator.SimpleTarget", tgt.Path(), tgt) + } + gens := st.GeneratorsFunc(ctx) + if len(gens) != 1 { + t.Fatalf("%s: got %d generators, want 1", tgt.Path(), len(gens)) + } + rg, ok := gens[0].(*registerExternalGenerator) + if !ok { + t.Fatalf("%s: generator is %T, want *registerExternalGenerator", tgt.Path(), gens[0]) + } + if want, ok := tc.wantGroups[tgt.Path()]; ok { + if string(rg.gv.Group) != want { + t.Errorf("%s: group = %q, want %q", tgt.Path(), rg.gv.Group, want) + } + } + if want, ok := tc.wantHasType[tgt.Path()]; ok { + hasType := len(rg.typesToGenerate) > 0 + if hasType != want { + t.Errorf("%s: hasType = %v, want %v (typesToGenerate=%v)", tgt.Path(), hasType, want, rg.typesToGenerate) + } + } + } + }) + } +} + +func equalStrSlice(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} diff --git a/staging/src/k8s.io/code-generator/cmd/register-gen/output_tests/simpletype/v1/doc.go b/staging/src/k8s.io/code-generator/cmd/register-gen/output_tests/simpletype/v1/doc.go index 729c3ae5e0d..6c1622a73b2 100644 --- a/staging/src/k8s.io/code-generator/cmd/register-gen/output_tests/simpletype/v1/doc.go +++ b/staging/src/k8s.io/code-generator/cmd/register-gen/output_tests/simpletype/v1/doc.go @@ -15,5 +15,6 @@ limitations under the License. */ // +k8s:register-gen=simpletype +// +k8s:deepcopy-gen=false package v1 diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/main.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/main.go index 594913b2656..50d2af3af32 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/main.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/main.go @@ -29,6 +29,7 @@ import ( "github.com/spf13/pflag" "k8s.io/code-generator/cmd/validation-gen/validators" + "k8s.io/code-generator/pkg/apidefinitions" "k8s.io/gengo/v2" "k8s.io/gengo/v2/generator" "k8s.io/gengo/v2/namer" @@ -96,6 +97,8 @@ type Args struct { // fields apiVersion, kind, path, errorType, origin (use "*" to wildcard // kind/path/errorType/origin) plus a required reason. TestAllowlist string + + apidefinitions.LintArgs } // AddFlags add the generator flags to the flag set. @@ -114,6 +117,7 @@ func (args *Args) AddFlags(fs *pflag.FlagSet) { "prefix prepended to every emitted test fixture filename; useful for marking files via a linguist-generated gitattributes pattern (e.g. \"zz_generated.\")") fs.StringVar(&args.TestAllowlist, "test-allowlist", "", "path to a YAML config file of rule-level filters to exclude from coverage fixture generation; only meaningful with --test-output-root") + apidefinitions.AddFlags(&args.LintArgs, fs) } // Validate checks the given arguments. @@ -128,6 +132,9 @@ func (args *Args) Validate() error { return fmt.Errorf("--test-output-file-prefix is only meaningful with --test-output-root") } + if err := apidefinitions.ValidateFlags(args.LintRules); err != nil { + return err + } return nil } diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/targets.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/targets.go index f25fb78b14a..35f5663958e 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/targets.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/targets.go @@ -26,6 +26,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/code-generator/cmd/validation-gen/validators" + "k8s.io/code-generator/pkg/apidefinitions" "k8s.io/gengo/v2" "k8s.io/gengo/v2/codetags" "k8s.io/gengo/v2/generator" @@ -37,7 +38,6 @@ import ( // These are the comment tags that carry parameters for validation generation. const ( mainTagName = "k8s:validation-gen" // defines which types to generate validation for - inputTagName = "k8s:validation-gen-input" // indicates that input types are in a different package schemeRegistryTagName = "k8s:validation-gen-scheme-registry" // defaults to k8s.io/apimachinery/pkg.runtime.Scheme testFixtureTagName = "k8s:validation-gen-test-fixture" // if set, generate go test files for test fixtures. Supported values: "validateFalse". @@ -74,40 +74,17 @@ func extractAndParseTag(tagName string, comments []string) ([]codetags.Tag, erro return tags, nil } -func extractMainTag(comments []string) ([]string, bool) { - // TODO: convert to extractAndParseTag() and update all callers to use quoted values - tags, err := gengo.ExtractFunctionStyleCommentTags("+", []string{mainTagName}, comments) +// validationTypeMatch returns the +k8s:validation-gen tag values for pkg, +// or false if validation-gen should not run. +func validationTypeMatch(pkg *types.Package, idOpts []apidefinitions.Option) ([]string, bool) { + info, err := apidefinitions.Identify(pkg, apidefinitions.Validation, idOpts...) if err != nil { - klog.Fatalf("Failed to extract tags: %v", err) + klog.Fatal(err) } - values, found := tags[mainTagName] - if !found || len(values) == 0 { + if !info.ShouldGenerate() { return nil, false } - - result := make([]string, len(values)) - for i, tag := range values { - result[i] = tag.Value - } - return result, true -} - -func extractInputTag(comments []string) []string { - // TODO: convert to extractAndParseTag() and update all callers to use quoted values - tags, err := gengo.ExtractFunctionStyleCommentTags("+", []string{inputTagName}, comments) - if err != nil { - klog.Fatalf("Failed to extract input tags: %v", err) - } - values, found := tags[inputTagName] - if !found { - return nil - } - - result := make([]string, len(values)) - for i, tag := range values { - result[i] = tag.Value - } - return result + return info.TypeFilters(), true } // TODO: this can just accept a single bool @@ -240,7 +217,12 @@ func GetTargets(context *generator.Context, args *Args) []generator.Target { klog.Fatalf("Failed loading boilerplate: %v", err) } - var targets []generator.Target + var idOpts []apidefinitions.Option + if len(args.LintRules) > 0 { + idOpts = append(idOpts, apidefinitions.WithLintRules(args.LintRules...)) + } + + var targetList []generator.Target // First load other "input" packages. We do this as a single call because // it is MUCH faster. @@ -248,23 +230,20 @@ func GetTargets(context *generator.Context, args *Args) []generator.Target { pkgToInput := map[string]string{} for _, input := range context.Inputs { klog.V(4).Infof("considering pkg %q", input) - pkg := context.Universe[input] - // if the types are not in the same package where the validation - // functions are to be emitted - inputTags := extractInputTag(pkg.Comments) - if len(inputTags) > 1 { - panic(fmt.Sprintf("there may only be one input tag, got %#v", inputTags)) + info, err := apidefinitions.Identify(pkg, apidefinitions.Validation, idOpts...) + if err != nil { + klog.Fatal(err) + } + if !info.ShouldGenerate() { + continue } - if len(inputTags) == 1 { - inputPath := inputTags[0] - if strings.HasPrefix(inputPath, "./") || strings.HasPrefix(inputPath, "../") { - // this is a relative dir, which will not work under gomodules. - // join with the local package path, but warn - klog.Fatalf("relative path (%s=%s) is not supported; use full package path (as used by 'import') instead", inputTagName, inputPath) - } + // +k8s:validation-gen-input may direct the generator at types in + // a different package than the one where validators will be emitted. + inputPath := info.ExternalTypes() + if inputPath != pkg.Path { klog.V(4).Infof(" input pkg %v", inputPath) inputPkgs = append(inputPkgs, inputPath) pkgToInput[input] = inputPath @@ -332,7 +311,7 @@ func GetTargets(context *generator.Context, args *Args) []generator.Target { schemeRegistry := schemeRegistryTag(pkg) - typesWith, found := extractMainTag(pkg.Comments) + typesWith, found := validationTypeMatch(pkg, idOpts) if !found { klog.V(2).InfoS(" did not find required tag", "tag", mainTagName) continue @@ -412,7 +391,7 @@ func GetTargets(context *generator.Context, args *Args) []generator.Target { } } - targets = append(targets, + targetList = append(targetList, &generator.SimpleTarget{ PkgName: pkg.Name, PkgPath: pkg.Path, @@ -450,7 +429,7 @@ func GetTargets(context *generator.Context, args *Args) []generator.Target { if err != nil { klog.Fatalf("loading allowlist: %v", err) } - targets = append(targets, testTargets(args.TestOutputRoot, args.TestOutputFilePrefix, groupKindReports, allowlist, boilerplate)...) + targetList = append(targetList, testTargets(args.TestOutputRoot, args.TestOutputFilePrefix, groupKindReports, allowlist, boilerplate)...) if len(linter.lintErrors) > 0 { buf := strings.Builder{} @@ -463,7 +442,7 @@ func GetTargets(context *generator.Context, args *Args) []generator.Target { } klog.Fatalf("lint failed:\n%s", buf.String()) } - return targets + return targetList } func isTypeWith(t *types.Type, typesWith []string) bool {