Use generator utilities in all API package definition generators

This commit is contained in:
Joe Betz 2026-05-07 16:05:52 -04:00
parent 05a1a7f2db
commit bd065151bb
No known key found for this signature in database
GPG key ID: 1E2BA7FEB91911CB
25 changed files with 949 additions and 220 deletions

View file

@ -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
}

View file

@ -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

View file

@ -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
}

View file

@ -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])

View file

@ -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 + `"`

View file

@ -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
}

View file

@ -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=<type-pkg>" in doc.go, where
// <type-pkg> 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=<peer-pkg>"
// 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

View file

@ -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
}

View file

@ -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.

View file

@ -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)
}
}
})
}
}

View file

@ -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
}

View file

@ -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.

View file

@ -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
}

View file

@ -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

View file

@ -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
}

View file

@ -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

View file

@ -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
}

View file

@ -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.

View file

@ -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)
}
})
}
}

View file

@ -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
}

View file

@ -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

View file

@ -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
}

View file

@ -15,5 +15,6 @@ limitations under the License.
*/
// +k8s:register-gen=simpletype
// +k8s:deepcopy-gen=false
package v1

View file

@ -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
}

View file

@ -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 {