Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions cmd/openapi-gen/args/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ type Args struct {
// Otherwise default value "-" will be used which indicates stdout.
ReportFilename string

// UseOpenAPIModelNames specifies the use of OpenAPI model names instead of
// Go '<package>.<type>' names for types in the OpenAPI spec.
UseOpenAPIModelNames bool
// OutputModelNameFile is the name of the file to be generated for OpenAPI schema name
// accessor functions. If empty, no model name accessor functions are generated.
// When this is specified, the OpenAPI spec generator will use the function names
// instead of Go type names for schema names.
OutputModelNameFile string
}

// New returns default arguments for the generator. Returning the arguments instead
Expand All @@ -58,11 +60,17 @@ func (args *Args) AddFlags(fs *pflag.FlagSet) {
"the base Go import-path under which to generate results")
fs.StringVar(&args.OutputFile, "output-file", "generated.openapi.go",
"the name of the file to be generated")
fs.StringVar(&args.OutputModelNameFile, "output-model-name-file", "",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not output to the same file?

EDIT: Oh I see. 1 Generator -> 1 file. Probably not worth changing, but IMO outputting to a single file would be cleaner, if we run across similar things in other generators. Unless there's a reason this is better that I am missing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is one file for the generated openapi definition, and there are N files for all the zz_geneated.model_name.go files, one for each package containing types that need this function added. Those files are very similar to deep copy generated files.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OH RIGHT. Nevermind me :)

`The filename for generated model name accessor functions.
If specified, a file with this name will be created in each package containing
a "+k8s:openapi-model-package" tag. The generated functions return fully qualified
model names, which are used in the OpenAPI spec as schema references instead of
Go type names. If empty, no model name accessor functions are generated and names
are inferred from Go type names.`)
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.StringVarP(&args.ReportFilename, "report-filename", "r", args.ReportFilename,
"Name of report file used by API linter to print API violations. Default \"-\" stands for standard output. NOTE that if valid filename other than \"-\" is specified, API linter won't return error on detected API violations. This allows further check of existing API violations without stopping the OpenAPI generation toolchain.")
fs.BoolVar(&args.UseOpenAPIModelNames, "use-openapi-model-names", false, "Use OpenAPI model names instead of Go '<package>.<type>' names for types in the OpenAPI spec.")
}

// Validate checks the given arguments.
Expand Down
27 changes: 24 additions & 3 deletions cmd/openapi-gen/openapi-gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"log"

"github.com/spf13/pflag"

"k8s.io/gengo/v2"
"k8s.io/gengo/v2/generator"
"k8s.io/klog/v2"
Expand All @@ -45,15 +46,35 @@ func main() {
log.Fatalf("Arguments validation error: %v", err)
}

myTargets := func(context *generator.Context) []generator.Target {
return generators.GetTargets(context, args)
boilerplate, err := gengo.GoBoilerplate(args.GoHeaderFile, gengo.StdBuildTag, gengo.StdGeneratedBy)
if err != nil {
log.Fatalf("Failed loading boilerplate: %v", err)
}

// Generates the code for model name accessors.
if len(args.OutputModelNameFile) > 0 {
modelNameTargets := func(context *generator.Context) []generator.Target {
return generators.GetModelNameTargets(context, args, boilerplate)
}
if err := gengo.Execute(
generators.NameSystems(),
generators.DefaultNameSystem(),
modelNameTargets,
gengo.StdBuildTag,
pflag.Args(),
); err != nil {
log.Fatalf("Model name code generation error: %v", err)
}
}

// Generates the code for the OpenAPIDefinitions.
openAPITargets := func(context *generator.Context) []generator.Target {
return generators.GetOpenAPITargets(context, args, boilerplate)
}
if err := gengo.Execute(
generators.NameSystems(),
generators.DefaultNameSystem(),
myTargets,
openAPITargets,
gengo.StdBuildTag,
pflag.Args(),
); err != nil {
Expand Down
64 changes: 56 additions & 8 deletions pkg/generators/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package generators
import (
"path"

"k8s.io/gengo/v2"
"k8s.io/gengo/v2/generator"
"k8s.io/gengo/v2/namer"
"k8s.io/gengo/v2/types"
Expand Down Expand Up @@ -49,12 +48,8 @@ func DefaultNameSystem() string {
return "sorting_namer"
}

func GetTargets(context *generator.Context, args *args.Args) []generator.Target {
boilerplate, err := gengo.GoBoilerplate(args.GoHeaderFile, gengo.StdBuildTag, gengo.StdGeneratedBy)
if err != nil {
klog.Fatalf("Failed loading boilerplate: %v", err)
}

// GetOpenAPITargets returns the targets for OpenAPI definition generation.
func GetOpenAPITargets(context *generator.Context, args *args.Args, boilerplate []byte) []generator.Target {
reportPath := "-"
if args.ReportFilename != "" {
reportPath = args.ReportFilename
Expand All @@ -74,7 +69,7 @@ func GetTargets(context *generator.Context, args *args.Args) []generator.Target
newOpenAPIGen(
args.OutputFile,
args.OutputPkg,
args.UseOpenAPIModelNames,
len(args.OutputModelNameFile) > 0,
),
newAPIViolationGen(),
}
Expand All @@ -83,3 +78,56 @@ func GetTargets(context *generator.Context, args *args.Args) []generator.Target
},
}
}

// GetModelNameTargets returns the targets for model name generation.
func GetModelNameTargets(context *generator.Context, args *args.Args, boilerplate []byte) []generator.Target {
var targets []generator.Target
for _, i := range context.Inputs {
klog.V(5).Infof("Considering pkg %q", i)

pkg := context.Universe[i]

openAPISchemaNamePackage, err := extractOpenAPISchemaNamePackage(pkg.Comments)
if err != nil {
klog.Fatalf("Package %v: invalid %s:%v", i, tagModelPackage, err)
}
hasPackageTag := len(openAPISchemaNamePackage) > 0

hasCandidates := false
for _, t := range pkg.Types {
v, err := singularTag(tagModelPackage, t.CommentLines)
if err != nil {
klog.Fatalf("Type %v: invalid %s:%v", t.Name, tagModelPackage, err)
}
hasTag := hasPackageTag || v != nil
hasModel := isSchemaNameType(t)
if hasModel && hasTag {
hasCandidates = true
break
}
}
if !hasCandidates {
klog.V(5).Infof(" skipping package")
continue
}

klog.V(3).Infof("Generating package %q", pkg.Path)

targets = append(targets,
&generator.SimpleTarget{
PkgName: path.Base(pkg.Path),
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{
NewSchemaNameGen(args.OutputModelNameFile, pkg.Path, openAPISchemaNamePackage),
}
},
})
}
return targets
}
177 changes: 177 additions & 0 deletions pkg/generators/model_names.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/*
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 (
"fmt"
"io"
"strings"

"k8s.io/gengo/v2"
"k8s.io/gengo/v2/generator"
"k8s.io/gengo/v2/namer"
"k8s.io/gengo/v2/types"
"k8s.io/klog/v2"
)

const (
tagModelPackage = "k8s:openapi-model-package"
)

func extractOpenAPISchemaNamePackage(comments []string) (string, error) {
v, err := singularTag(tagModelPackage, comments)
if v == nil || err != nil {
return "", err
}
return v.Value, nil
}

func singularTag(tagName string, comments []string) (*gengo.Tag, error) {
tags, err := gengo.ExtractFunctionStyleCommentTags("+", []string{tagName}, comments)
if err != nil {
return nil, err
}
if len(tags) == 0 {
return nil, nil
}
if len(tags) > 1 {
return nil, fmt.Errorf("multiple %s tags found", tagName)
}
tag := tags[tagName]
if len(tag) == 0 {
return nil, nil
}
if len(tag) > 1 {
klog.V(5).Infof("multiple %s tags found, using the first one", tagName)
}
value := tag[0]
return &value, nil
}

// genSchemaName produces a file with autogenerated openapi schema name functions.
type genSchemaName struct {
generator.GoGenerator
targetPackage string
imports namer.ImportTracker
typesForInit []*types.Type
openAPISchemaNamePackage string
}

// NewSchemaNameGen creates a generator
func NewSchemaNameGen(outputFilename, targetPackage string, openAPISchemaNamePackage string) generator.Generator {
return &genSchemaName{
GoGenerator: generator.GoGenerator{
OutputFilename: outputFilename,
},
targetPackage: targetPackage,
imports: generator.NewImportTracker(),
typesForInit: make([]*types.Type, 0),
openAPISchemaNamePackage: openAPISchemaNamePackage,
}
}

func (g *genSchemaName) Namers(c *generator.Context) namer.NameSystems {
return namer.NameSystems{
"public": namer.NewPublicNamer(1),
"local": namer.NewPublicNamer(0),
"raw": namer.NewRawNamer("", nil),
}
}

func (g *genSchemaName) Filter(c *generator.Context, t *types.Type) bool {
// Filter out types not being processed or not copyable within the package.
if !isSchemaNameType(t) {
klog.V(2).Infof("Type %v is not a valid target for OpenAPI schema name", t)
return false
}
g.typesForInit = append(g.typesForInit, t)
return true
}

// isSchemaNameType indicates whether or not a type could be used to serve an API.
func isSchemaNameType(t *types.Type) bool {
// Filter out private types.
if namer.IsPrivateGoName(t.Name.Name) {
return false
}

for t.Kind == types.Alias {
t = t.Underlying
}

if t.Kind != types.Struct {
return false
}
return true
}

func (g *genSchemaName) isOtherPackage(pkg string) bool {
if pkg == g.targetPackage {
return false
}
if strings.HasSuffix(pkg, ""+g.targetPackage+"") {
return false
}
return true
}

func (g *genSchemaName) Imports(c *generator.Context) (imports []string) {
importLines := []string{}
for _, singleImport := range g.imports.ImportLines() {
if g.isOtherPackage(singleImport) {
importLines = append(importLines, singleImport)
}
}
return importLines
}

func (g *genSchemaName) Init(c *generator.Context, w io.Writer) error {
return nil
}

func (g *genSchemaName) GenerateType(c *generator.Context, t *types.Type, w io.Writer) error {
klog.V(3).Infof("Generating openapi schema name for type %v", t)

openAPISchemaNamePackage := g.openAPISchemaNamePackage
v, err := singularTag(tagModelPackage, t.CommentLines)
if err != nil {
return fmt.Errorf("type %v: invalid %s:%v", t.Name, tagModelPackage, err)
}
if v != nil && v.Value != "" {
openAPISchemaNamePackage = v.Value
}

if openAPISchemaNamePackage == "" {
return nil
}

schemaName := openAPISchemaNamePackage + "." + t.Name.Name

a := map[string]interface{}{
"type": t,
"schemaName": schemaName,
}

sw := generator.NewSnippetWriter(w, c, "$", "$")

sw.Do("// OpenAPIModelName returns the OpenAPI model name for this type.\n", a)
sw.Do("func (in $.type|local$) OpenAPIModelName() string {\n", a)
sw.Do("\treturn \"$.schemaName$\"\n", a)
sw.Do("}\n\n", nil)

return sw.Error()
}
Loading