Skip to content

Commit

Permalink
Stub out generator
Browse files Browse the repository at this point in the history
  • Loading branch information
jpbetz committed Nov 9, 2020
1 parent 6fa696d commit bb53d0b
Show file tree
Hide file tree
Showing 6 changed files with 689 additions and 0 deletions.
2 changes: 2 additions & 0 deletions cmd/controller-gen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (

"github.com/spf13/cobra"

"sigs.k8s.io/controller-tools/pkg/applyconfigurations"
"sigs.k8s.io/controller-tools/pkg/crd"
"sigs.k8s.io/controller-tools/pkg/deepcopy"
"sigs.k8s.io/controller-tools/pkg/genall"
Expand Down Expand Up @@ -51,6 +52,7 @@ var (
"crd": crd.Generator{},
"rbac": rbac.Generator{},
"object": deepcopy.Generator{},
"apply": applyconfigurations.Generator{},
"webhook": webhook.Generator{},
"schemapatch": schemapatcher.Generator{},
}
Expand Down
18 changes: 18 additions & 0 deletions pkg/applyconfigurations/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
Copyright 2019 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 applyconfigurations generates types for constructing declarative apply configurations.
package applyconfigurations
270 changes: 270 additions & 0 deletions pkg/applyconfigurations/gen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
/*
Copyright 2019 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 applyconfigurations

import (
"bytes"
"fmt"
"go/ast"
"go/format"
"io"
"sort"
"strings"

"sigs.k8s.io/controller-tools/pkg/genall"
"sigs.k8s.io/controller-tools/pkg/loader"
"sigs.k8s.io/controller-tools/pkg/markers"
)

// Based on deepcopy gen but with legacy marker support removed.

var (
enablePkgMarker = markers.Must(markers.MakeDefinition("kubebuilder:object:generate", markers.DescribesPackage, false))
enableTypeMarker = markers.Must(markers.MakeDefinition("kubebuilder:object:generate", markers.DescribesType, false))
isObjectMarker = markers.Must(markers.MakeDefinition("kubebuilder:object:root", markers.DescribesType, false))
)

// +controllertools:marker:generateHelp

// Generator generates code containing apply configuration type implementations.
type Generator struct {
// HeaderFile specifies the header text (e.g. license) to prepend to generated files.
HeaderFile string `marker:",optional"`
// Year specifies the year to substitute for " YEAR" in the header file.
Year string `marker:",optional"`
}

func (Generator) CheckFilter() loader.NodeFilter {
return func(node ast.Node) bool {
// ignore interfaces
_, isIface := node.(*ast.InterfaceType)
return !isIface
}
}

func (Generator) RegisterMarkers(into *markers.Registry) error {
if err := markers.RegisterAll(into,
enablePkgMarker, enableTypeMarker, isObjectMarker); err != nil {
return err
}
into.AddHelp(enablePkgMarker,
markers.SimpleHelp("apply", "enables or disables object apply configuration generation for this package"))
into.AddHelp(
enableTypeMarker, markers.SimpleHelp("apply", "overrides enabling or disabling apply configuration generation for this type"))
into.AddHelp(isObjectMarker,
markers.SimpleHelp("apply", "enables apply configuration generation for this type"))
return nil
}

func enabledOnPackage(col *markers.Collector, pkg *loader.Package) (bool, error) {
pkgMarkers, err := markers.PackageMarkers(col, pkg)
if err != nil {
return false, err
}
pkgMarker := pkgMarkers.Get(enablePkgMarker.Name)
if pkgMarker != nil {
return pkgMarker.(bool), nil
}

return false, nil
}

func enabledOnType(allTypes bool, info *markers.TypeInfo) bool {
if typeMarker := info.Markers.Get(enableTypeMarker.Name); typeMarker != nil {
return typeMarker.(bool)
}
return allTypes || genObjectInterface(info)
}

func genObjectInterface(info *markers.TypeInfo) bool {
objectEnabled := info.Markers.Get(isObjectMarker.Name)
if objectEnabled != nil {
return objectEnabled.(bool)
}
return false
}

func (d Generator) Generate(ctx *genall.GenerationContext) error {
var headerText string

if d.HeaderFile != "" {
headerBytes, err := ctx.ReadFile(d.HeaderFile)
if err != nil {
return err
}
headerText = string(headerBytes)
}
headerText = strings.ReplaceAll(headerText, " YEAR", " "+d.Year)

objGenCtx := ObjectGenCtx{
Collector: ctx.Collector,
Checker: ctx.Checker,
HeaderText: headerText,
}

for _, root := range ctx.Roots {
outContents := objGenCtx.generateForPackage(root)
if outContents == nil {
continue
}

writeOut(ctx, root, outContents)
}

return nil
}

// ObjectGenCtx contains the common info for generating apply configuration implementations.
// It mostly exists so that generating for a package can be easily tested without
// requiring a full set of output rules, etc.
type ObjectGenCtx struct {
Collector *markers.Collector
Checker *loader.TypeChecker
HeaderText string
}

// writeHeader writes out the build tag, package declaration, and imports
func writeHeader(pkg *loader.Package, out io.Writer, packageName string, imports *importsList, headerText string) {
// NB(directxman12): blank line after build tags to distinguish them from comments
_, err := fmt.Fprintf(out, `// +build !ignore_autogenerated
%[3]s
// Code generated by controller-gen. DO NOT EDIT.
package %[1]s
import (
%[2]s
)
`, packageName, strings.Join(imports.ImportSpecs(), "\n"), headerText)
if err != nil {
pkg.AddError(err)
}

}

// generateForPackage generates apply configuration implementations for
// types in the given package, writing the formatted result to given writer.
// May return nil if source could not be generated.
func (ctx *ObjectGenCtx) generateForPackage(root *loader.Package) []byte {
allTypes, err := enabledOnPackage(ctx.Collector, root)
if err != nil {
root.AddError(err)
return nil
}

ctx.Checker.Check(root)

root.NeedTypesInfo()

byType := make(map[string][]byte)
imports := &importsList{
byPath: make(map[string]string),
byAlias: make(map[string]string),
pkg: root,
}
// avoid confusing aliases by "reserving" the root package's name as an alias
imports.byAlias[root.Name] = ""

if err := markers.EachType(ctx.Collector, root, func(info *markers.TypeInfo) {
outContent := new(bytes.Buffer)

if !enabledOnType(allTypes, info) {
//root.AddError(fmt.Errorf("skipping type: %v", info.Name)) // TODO(jpbetz): Remove
return
}

// not all types required a generate apply configuration. For example, no apply configuration
// type is needed for Quantity, IntOrString, RawExtension or Unknown.
if !shouldBeApplyConfiguration(root, info) {
//root.AddError(fmt.Errorf("skipping type: %v", info.Name)) // TODO(jpbetz): Remove
return
}

copyCtx := &applyConfigurationMaker{
pkg: root,
importsList: imports,
codeWriter: &codeWriter{out: outContent},
}

copyCtx.GenerateTypesFor(root, info)

outBytes := outContent.Bytes()
if len(outBytes) > 0 {
byType[info.Name] = outBytes
}
}); err != nil {
root.AddError(err)
return nil
}

if len(byType) == 0 {
return nil
}

outContent := new(bytes.Buffer)
writeHeader(root, outContent, root.Name, imports, ctx.HeaderText)
writeTypes(root, outContent, byType)

outBytes := outContent.Bytes()
formattedBytes, err := format.Source(outBytes)
if err != nil {
root.AddError(err)
// we still write the invalid source to disk to figure out what went wrong
} else {
outBytes = formattedBytes
}

return outBytes
}

// writeTypes writes each method to the file, sorted by type name.
func writeTypes(pkg *loader.Package, out io.Writer, byType map[string][]byte) {
sortedNames := make([]string, 0, len(byType))
for name := range byType {
sortedNames = append(sortedNames, name)
}
sort.Strings(sortedNames)

for _, name := range sortedNames {
_, err := out.Write(byType[name])
if err != nil {
pkg.AddError(err)
}
}
}

// writeFormatted outputs the given code, after gofmt-ing it. If we couldn't gofmt,
// we write the unformatted code for debugging purposes.
func writeOut(ctx *genall.GenerationContext, root *loader.Package, outBytes []byte) {
outputFile, err := ctx.Open(root, "zz_generated.applyconfigurations.go")
if err != nil {
root.AddError(err)
return
}
defer outputFile.Close()
n, err := outputFile.Write(outBytes)
if err != nil {
root.AddError(err)
return
}
if n < len(outBytes) {
root.AddError(io.ErrShortWrite)
}
}
Loading

0 comments on commit bb53d0b

Please sign in to comment.