Add apply configuration generator

This commit is contained in:
Joe Betz 2021-02-11 17:01:40 -05:00
parent 60a714058b
commit 09cc895c84
16 changed files with 1396 additions and 15 deletions

View File

@ -29,10 +29,12 @@ kube::golang::setup_env
go install k8s.io/kubernetes/vendor/k8s.io/code-generator/cmd/client-gen
go install k8s.io/kubernetes/vendor/k8s.io/code-generator/cmd/lister-gen
go install k8s.io/kubernetes/vendor/k8s.io/code-generator/cmd/informer-gen
go install k8s.io/kubernetes/vendor/k8s.io/code-generator/cmd/applyconfiguration-gen
clientgen=$(kube::util::find-binary "client-gen")
listergen=$(kube::util::find-binary "lister-gen")
informergen=$(kube::util::find-binary "informer-gen")
applyconfigurationgen=$(kube::util::find-binary "applyconfiguration-gen")
IFS=" " read -r -a GROUP_VERSIONS <<< "${KUBE_AVAILABLE_GROUP_VERSIONS}"
GV_DIRS=()
@ -54,6 +56,22 @@ done
# delimit by commas for the command
GV_DIRS_CSV=$(IFS=',';echo "${GV_DIRS[*]// /,}";IFS=$)
applyconfigurationgen_external_apis=()
# because client-gen doesn't do policy/v1alpha1, we have to skip it too
kube::util::read-array applyconfigurationgen_external_apis < <(
cd "${KUBE_ROOT}/staging/src"
find k8s.io/api -name types.go -print0 | xargs -0 -n1 dirname | sort | grep -v pkg.apis.policy.v1alpha1
)
applyconfigurationgen_external_apis+=("k8s.io/apimachinery/pkg/apis/meta/v1")
applyconfigurationgen_external_apis_csv=$(IFS=,; echo "${applyconfigurationgen_external_apis[*]}")
applyconfigurations_package="k8s.io/client-go/applyconfigurations"
${applyconfigurationgen} \
--output-base "${KUBE_ROOT}/vendor" \
--output-package "${applyconfigurations_package}" \
--input-dirs "${applyconfigurationgen_external_apis_csv}" \
--go-header-file "${KUBE_ROOT}/hack/boilerplate/boilerplate.generatego.txt" \
"$@"
# This can be called with one flag, --verify-only, so it works for both the
# update- and verify- scripts.
${clientgen} --output-base "${KUBE_ROOT}/vendor" --output-package="k8s.io/client-go" --clientset-name="kubernetes" --input-base="k8s.io/api" --input="${GV_DIRS_CSV}" --go-header-file "${KUBE_ROOT}/hack/boilerplate/boilerplate.generatego.txt" "$@"

View File

@ -0,0 +1,241 @@
/*
Copyright 2021 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 testing
import (
"math/rand"
"reflect"
"testing"
"github.com/google/go-cmp/cmp"
fuzz "github.com/google/gofuzz"
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
apiequality "k8s.io/apimachinery/pkg/api/equality"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/json"
"k8s.io/client-go/applyconfigurations"
v1mf "k8s.io/client-go/applyconfigurations/core/v1"
"k8s.io/kubernetes/pkg/api/legacyscheme"
api "k8s.io/kubernetes/pkg/apis/core"
)
// TestUnstructuredRoundTripApplyConfigurations converts each known object type through unstructured
// to the apply configuration for that object type, then converts it back to the object type and
// verifies it is unchanged.
func TestUnstructuredRoundTripApplyConfigurations(t *testing.T) {
for gvk := range legacyscheme.Scheme.AllKnownTypes() {
if nonRoundTrippableTypes.Has(gvk.Kind) {
continue
}
if gvk.Version == runtime.APIVersionInternal {
continue
}
if builder := applyconfigurations.ForKind(gvk); builder == nil {
continue
}
t.Run(gvk.String(), func(t *testing.T) {
for i := 0; i < 50; i++ {
item := fuzzObject(t, gvk)
builder := applyconfigurations.ForKind(gvk)
unstructuredRoundTripApplyConfiguration(t, item, builder)
if t.Failed() {
break
}
}
})
}
}
// TestJsonRoundTripApplyConfigurations converts each known object type through JSON to the apply
// configuration for that object type, then converts it back to the object type and verifies it
// is unchanged.
func TestJsonRoundTripApplyConfigurations(t *testing.T) {
for gvk := range legacyscheme.Scheme.AllKnownTypes() {
if nonRoundTrippableTypes.Has(gvk.Kind) {
continue
}
if gvk.Version == runtime.APIVersionInternal {
continue
}
if builder := applyconfigurations.ForKind(gvk); builder == nil {
continue
}
t.Run(gvk.String(), func(t *testing.T) {
for i := 0; i < 50; i++ {
item := fuzzObject(t, gvk)
builder := applyconfigurations.ForKind(gvk)
jsonRoundTripApplyConfiguration(t, item, builder)
if t.Failed() {
break
}
}
})
}
}
func unstructuredRoundTripApplyConfiguration(t *testing.T, item runtime.Object, applyConfig interface{}) {
u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(item)
if err != nil {
t.Errorf("ToUnstructured failed: %v", err)
return
}
err = runtime.DefaultUnstructuredConverter.FromUnstructured(u, applyConfig)
if err != nil {
t.Errorf("FromUnstructured failed: %v", err)
return
}
rtObj := reflect.New(reflect.TypeOf(item).Elem()).Interface().(runtime.Object)
u, err = runtime.DefaultUnstructuredConverter.ToUnstructured(applyConfig)
if err != nil {
t.Errorf("ToUnstructured failed: %v", err)
return
}
err = runtime.DefaultUnstructuredConverter.FromUnstructured(u, rtObj)
if err != nil {
t.Errorf("FromUnstructured failed: %v", err)
return
}
if !apiequality.Semantic.DeepEqual(item, rtObj) {
t.Errorf("Object changed, diff: %v", cmp.Diff(item, rtObj))
}
}
func jsonRoundTripApplyConfiguration(t *testing.T, item runtime.Object, applyConfig interface{}) {
objData, err := json.Marshal(item)
if err != nil {
t.Errorf("json.Marshal failed: %v", err)
return
}
err = json.Unmarshal(objData, applyConfig)
if err != nil {
t.Errorf("applyConfig.UnmarshalJSON failed: %v", err)
return
}
rtObj := reflect.New(reflect.TypeOf(item).Elem()).Interface().(runtime.Object)
applyData, err := json.Marshal(applyConfig)
if err != nil {
t.Errorf("applyConfig.MarshalJSON failed: %v", err)
return
}
err = json.Unmarshal(applyData, rtObj)
if err != nil {
t.Errorf("json.Unmarshal failed: %v", err)
return
}
if !apiequality.Semantic.DeepEqual(item, rtObj) {
t.Errorf("Object changed, diff: %v", cmp.Diff(item, rtObj))
}
}
func fuzzObject(t *testing.T, gvk schema.GroupVersionKind) runtime.Object {
internalVersion := schema.GroupVersion{Group: gvk.Group, Version: runtime.APIVersionInternal}
externalVersion := gvk.GroupVersion()
kind := gvk.Kind
// We do fuzzing on the internal version of the object, and only then
// convert to the external version. This is because custom fuzzing
// function are only supported for internal objects.
internalObj, err := legacyscheme.Scheme.New(internalVersion.WithKind(kind))
if err != nil {
t.Fatalf("Couldn't create internal object %v: %v", kind, err)
}
seed := rand.Int63()
fuzzer.FuzzerFor(FuzzerFuncs, rand.NewSource(seed), legacyscheme.Codecs).
Funcs(
// Ensure that InitContainers and their statuses are not generated. This
// is because in this test we are simply doing json operations, in which
// those disappear.
func(s *api.PodSpec, c fuzz.Continue) {
c.FuzzNoCustom(s)
s.InitContainers = nil
},
func(s *api.PodStatus, c fuzz.Continue) {
c.FuzzNoCustom(s)
s.InitContainerStatuses = nil
},
// Apply configuration types do not have managed fields, so we exclude
// them in our fuzz test cases.
func(s *v1.ObjectMeta, c fuzz.Continue) {
c.FuzzNoCustom(s)
s.ManagedFields = nil
},
).Fuzz(internalObj)
item, err := legacyscheme.Scheme.New(externalVersion.WithKind(kind))
if err != nil {
t.Fatalf("Couldn't create external object %v: %v", kind, err)
}
if err := legacyscheme.Scheme.Convert(internalObj, item, nil); err != nil {
t.Fatalf("Conversion for %v failed: %v", kind, err)
}
return item
}
func BenchmarkApplyConfigurationsFromUnstructured(b *testing.B) {
items := benchmarkItems(b)
convertor := runtime.DefaultUnstructuredConverter
unstr := make([]map[string]interface{}, len(items))
for i := range items {
item, err := convertor.ToUnstructured(&items[i])
if err != nil || item == nil {
b.Fatalf("unexpected error: %v", err)
}
unstr = append(unstr, item)
}
size := len(items)
b.ResetTimer()
for i := 0; i < b.N; i++ {
builder := &v1mf.PodApplyConfiguration{}
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstr[i%size], builder); err != nil {
b.Fatalf("unexpected error: %v", err)
}
}
b.StopTimer()
}
func BenchmarkApplyConfigurationsToUnstructured(b *testing.B) {
items := benchmarkItems(b)
convertor := runtime.DefaultUnstructuredConverter
builders := make([]*v1mf.PodApplyConfiguration, len(items))
for i := range items {
item, err := convertor.ToUnstructured(&items[i])
if err != nil || item == nil {
b.Fatalf("unexpected error: %v", err)
}
builder := &v1mf.PodApplyConfiguration{}
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(item, builder); err != nil {
b.Fatalf("unexpected error: %v", err)
}
builders[i] = builder
}
b.ResetTimer()
size := len(items)
for i := 0; i < b.N; i++ {
builder := builders[i%size]
if _, err := runtime.DefaultUnstructuredConverter.ToUnstructured(builder); err != nil {
b.Fatalf("unexpected error: %v", err)
}
}
b.StopTimer()
}

View File

@ -0,0 +1,45 @@
/*
Copyright 2021 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 args
import (
"fmt"
"path"
"k8s.io/gengo/args"
codegenutil "k8s.io/code-generator/pkg/util"
)
// NewDefaults returns default arguments for the generator.
func NewDefaults() *args.GeneratorArgs {
genericArgs := args.Default().WithoutDefaultFlagParsing()
if pkg := codegenutil.CurrentPackage(); len(pkg) != 0 {
genericArgs.OutputPackagePath = path.Join(pkg, "pkg/client/applyconfigurations")
}
return genericArgs
}
// Validate checks the given arguments.
func Validate(genericArgs *args.GeneratorArgs) error {
if len(genericArgs.OutputPackagePath) == 0 {
return fmt.Errorf("output package cannot be empty")
}
return nil
}

View File

@ -0,0 +1,308 @@
/*
Copyright 2021 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 (
"io"
"k8s.io/gengo/generator"
"k8s.io/gengo/namer"
"k8s.io/gengo/types"
"k8s.io/klog/v2"
"k8s.io/code-generator/cmd/client-gen/generators/util"
clientgentypes "k8s.io/code-generator/cmd/client-gen/types"
)
// applyConfigurationGenerator produces apply configurations for a given GroupVersion and type.
type applyConfigurationGenerator struct {
generator.DefaultGen
outputPackage string
localPackage types.Name
groupVersion clientgentypes.GroupVersion
applyConfig applyConfig
imports namer.ImportTracker
refGraph refGraph
}
var _ generator.Generator = &applyConfigurationGenerator{}
func (g *applyConfigurationGenerator) Filter(_ *generator.Context, t *types.Type) bool {
return t == g.applyConfig.Type
}
func (g *applyConfigurationGenerator) Namers(*generator.Context) namer.NameSystems {
return namer.NameSystems{
"raw": namer.NewRawNamer(g.localPackage.Package, g.imports),
"singularKind": namer.NewPublicNamer(0),
}
}
func (g *applyConfigurationGenerator) Imports(*generator.Context) (imports []string) {
return g.imports.ImportLines()
}
// TypeParams provides a struct that an apply configuration
// is generated for as well as the apply configuration details
// and types referenced by the struct.
type TypeParams struct {
Struct *types.Type
ApplyConfig applyConfig
Tags util.Tags
APIVersion string
}
type memberParams struct {
TypeParams
Member types.Member
MemberType *types.Type
JSONTags JSONTags
ArgType *types.Type // only set for maps and slices
EmbeddedIn *memberParams // parent embedded member, if any
}
func (g *applyConfigurationGenerator) GenerateType(c *generator.Context, t *types.Type, w io.Writer) error {
sw := generator.NewSnippetWriter(w, c, "$", "$")
klog.V(5).Infof("processing type %v", t)
typeParams := TypeParams{
Struct: t,
ApplyConfig: g.applyConfig,
Tags: genclientTags(t),
APIVersion: g.groupVersion.ToAPIVersion(),
}
g.generateStruct(sw, typeParams)
if typeParams.Tags.GenerateClient {
if typeParams.Tags.NonNamespaced {
sw.Do(clientgenTypeConstructorNonNamespaced, typeParams)
} else {
sw.Do(clientgenTypeConstructorNamespaced, typeParams)
}
} else {
sw.Do(constructor, typeParams)
}
g.generateWithFuncs(t, typeParams, sw, nil)
return sw.Error()
}
func blocklisted(t *types.Type, member types.Member) bool {
if objectMeta.Name == t.Name && member.Name == "ManagedFields" {
return true
}
return false
}
func (g *applyConfigurationGenerator) generateWithFuncs(t *types.Type, typeParams TypeParams, sw *generator.SnippetWriter, embed *memberParams) {
for _, member := range t.Members {
if blocklisted(t, member) {
continue
}
memberType := g.refGraph.applyConfigForType(member.Type)
if g.refGraph.isApplyConfig(member.Type) {
memberType = &types.Type{Kind: types.Pointer, Elem: memberType}
}
if jsonTags, ok := lookupJSONTags(member); ok {
memberParams := memberParams{
TypeParams: typeParams,
Member: member,
MemberType: memberType,
JSONTags: jsonTags,
EmbeddedIn: embed,
}
if memberParams.Member.Embedded {
g.generateWithFuncs(member.Type, typeParams, sw, &memberParams)
if !jsonTags.inline {
// non-inlined embeds are nillable and need a "ensure exists" utility function
sw.Do(ensureEmbedExists, memberParams)
}
continue
}
// For slices where the items are generated apply configuration types, accept varargs of
// pointers of the type as "with" function arguments so the "with" function can be used like so:
// WithFoos(Foo().WithName("x"), Foo().WithName("y"))
if t := deref(member.Type); t.Kind == types.Slice && g.refGraph.isApplyConfig(t.Elem) {
memberParams.ArgType = &types.Type{Kind: types.Pointer, Elem: memberType.Elem}
g.generateMemberWithForSlice(sw, memberParams)
continue
}
// Note: There are no maps where the values are generated apply configurations (because
// associative lists are used instead). So if a type like this is ever introduced, the
// default "with" function generator will produce a working (but not entirely convenient "with" function)
// that would be used like so:
// WithMap(map[string]FooApplyConfiguration{*Foo().WithName("x")})
switch memberParams.Member.Type.Kind {
case types.Slice:
memberParams.ArgType = memberType.Elem
g.generateMemberWithForSlice(sw, memberParams)
case types.Map:
g.generateMemberWithForMap(sw, memberParams)
default:
g.generateMemberWith(sw, memberParams)
}
}
}
}
func (g *applyConfigurationGenerator) generateStruct(sw *generator.SnippetWriter, typeParams TypeParams) {
sw.Do("// $.ApplyConfig.ApplyConfiguration|public$ represents an declarative configuration of the $.ApplyConfig.Type|public$ type for use\n", typeParams)
sw.Do("// with apply.\n", typeParams)
sw.Do("type $.ApplyConfig.ApplyConfiguration|public$ struct {\n", typeParams)
for _, structMember := range typeParams.Struct.Members {
if blocklisted(typeParams.Struct, structMember) {
continue
}
if structMemberTags, ok := lookupJSONTags(structMember); ok {
if !structMemberTags.inline {
structMemberTags.omitempty = true
}
params := memberParams{
TypeParams: typeParams,
Member: structMember,
MemberType: g.refGraph.applyConfigForType(structMember.Type),
JSONTags: structMemberTags,
}
if structMember.Embedded {
if structMemberTags.inline {
sw.Do("$.MemberType|raw$ `json:\"$.JSONTags$\"`\n", params)
} else {
sw.Do("*$.MemberType|raw$ `json:\"$.JSONTags$\"`\n", params)
}
} else if isNillable(structMember.Type) {
sw.Do("$.Member.Name$ $.MemberType|raw$ `json:\"$.JSONTags$\"`\n", params)
} else {
sw.Do("$.Member.Name$ *$.MemberType|raw$ `json:\"$.JSONTags$\"`\n", params)
}
}
}
sw.Do("}\n", typeParams)
}
func deref(t *types.Type) *types.Type {
for t.Kind == types.Pointer {
t = t.Elem
}
return t
}
func isNillable(t *types.Type) bool {
return t.Kind == types.Slice || t.Kind == types.Map
}
func (g *applyConfigurationGenerator) generateMemberWith(sw *generator.SnippetWriter, memberParams memberParams) {
sw.Do("// With$.Member.Name$ sets the $.Member.Name$ field in the declarative configuration to the given value\n", memberParams)
sw.Do("// and returns the receiver, so that objects can be built by chaining \"With\" function invocations.\n", memberParams)
sw.Do("// If called multiple times, the $.Member.Name$ field is set to the value of the last call.\n", memberParams)
sw.Do("func (b *$.ApplyConfig.ApplyConfiguration|public$) With$.Member.Name$(value $.MemberType|raw$) *$.ApplyConfig.ApplyConfiguration|public$ {\n", memberParams)
g.ensureEnbedExistsIfApplicable(sw, memberParams)
if g.refGraph.isApplyConfig(memberParams.Member.Type) || isNillable(memberParams.Member.Type) {
sw.Do("b.$.Member.Name$ = value\n", memberParams)
} else {
sw.Do("b.$.Member.Name$ = &value\n", memberParams)
}
sw.Do(" return b\n", memberParams)
sw.Do("}\n", memberParams)
}
func (g *applyConfigurationGenerator) generateMemberWithForSlice(sw *generator.SnippetWriter, memberParams memberParams) {
sw.Do("// With$.Member.Name$ adds the given value to the $.Member.Name$ field in the declarative configuration\n", memberParams)
sw.Do("// and returns the receiver, so that objects can be build by chaining \"With\" function invocations.\n", memberParams)
sw.Do("// If called multiple times, values provided by each call will be appended to the $.Member.Name$ field.\n", memberParams)
sw.Do("func (b *$.ApplyConfig.ApplyConfiguration|public$) With$.Member.Name$(values ...$.ArgType|raw$) *$.ApplyConfig.ApplyConfiguration|public$ {\n", memberParams)
g.ensureEnbedExistsIfApplicable(sw, memberParams)
sw.Do(" for i := range values {\n", memberParams)
if memberParams.ArgType.Kind == types.Pointer {
sw.Do("if values[i] == nil {\n", memberParams)
sw.Do(" panic(\"nil value passed to With$.Member.Name$\")\n", memberParams)
sw.Do("}\n", memberParams)
sw.Do("b.$.Member.Name$ = append(b.$.Member.Name$, *values[i])\n", memberParams)
} else {
sw.Do("b.$.Member.Name$ = append(b.$.Member.Name$, values[i])\n", memberParams)
}
sw.Do(" }\n", memberParams)
sw.Do(" return b\n", memberParams)
sw.Do("}\n", memberParams)
}
func (g *applyConfigurationGenerator) generateMemberWithForMap(sw *generator.SnippetWriter, memberParams memberParams) {
sw.Do("// With$.Member.Name$ puts the entries into the $.Member.Name$ field in the declarative configuration\n", memberParams)
sw.Do("// and returns the receiver, so that objects can be build by chaining \"With\" function invocations.\n", memberParams)
sw.Do("// If called multiple times, the entries provided by each call will be put on the $.Member.Name$ field,\n", memberParams)
sw.Do("// overwriting an existing map entries in $.Member.Name$ field with the same key.\n", memberParams)
sw.Do("func (b *$.ApplyConfig.ApplyConfiguration|public$) With$.Member.Name$(entries $.MemberType|raw$) *$.ApplyConfig.ApplyConfiguration|public$ {\n", memberParams)
g.ensureEnbedExistsIfApplicable(sw, memberParams)
sw.Do(" if b.$.Member.Name$ == nil && len(entries) > 0 {\n", memberParams)
sw.Do(" b.$.Member.Name$ = make($.MemberType|raw$, len(entries))\n", memberParams)
sw.Do(" }\n", memberParams)
sw.Do(" for k, v := range entries {\n", memberParams)
sw.Do(" b.$.Member.Name$[k] = v\n", memberParams)
sw.Do(" }\n", memberParams)
sw.Do(" return b\n", memberParams)
sw.Do("}\n", memberParams)
}
func (g *applyConfigurationGenerator) ensureEnbedExistsIfApplicable(sw *generator.SnippetWriter, memberParams memberParams) {
// Embedded types that are not inlined must be nillable so they are not included in the apply configuration
// when all their fields are omitted.
if memberParams.EmbeddedIn != nil && !memberParams.EmbeddedIn.JSONTags.inline {
sw.Do("b.ensure$.MemberType.Elem|public$Exists()\n", memberParams.EmbeddedIn)
}
}
var ensureEmbedExists = `
func (b *$.ApplyConfig.ApplyConfiguration|public$) ensure$.MemberType.Elem|public$Exists() {
if b.$.MemberType.Elem|public$ == nil {
b.$.MemberType.Elem|public$ = &$.MemberType.Elem|raw${}
}
}
`
var clientgenTypeConstructorNamespaced = `
// $.ApplyConfig.Type|public$ constructs an declarative configuration of the $.ApplyConfig.Type|public$ type for use with
// apply.
func $.ApplyConfig.Type|public$(name, namespace string) *$.ApplyConfig.ApplyConfiguration|public$ {
b := &$.ApplyConfig.ApplyConfiguration|public${}
b.WithName(name)
b.WithNamespace(namespace)
b.WithKind("$.ApplyConfig.Type|singularKind$")
b.WithAPIVersion("$.APIVersion$")
return b
}
`
var clientgenTypeConstructorNonNamespaced = `
// $.ApplyConfig.Type|public$ constructs an declarative configuration of the $.ApplyConfig.Type|public$ type for use with
// apply.
func $.ApplyConfig.Type|public$(name string) *$.ApplyConfig.ApplyConfiguration|public$ {
b := &$.ApplyConfig.ApplyConfiguration|public${}
b.WithName(name)
b.WithKind("$.ApplyConfig.Type|singularKind$")
b.WithAPIVersion("$.APIVersion$")
return b
}
`
var constructor = `
// $.ApplyConfig.ApplyConfiguration|public$ constructs an declarative configuration of the $.ApplyConfig.Type|public$ type for use with
// apply.
func $.ApplyConfig.Type|public$() *$.ApplyConfig.ApplyConfiguration|public$ {
return &$.ApplyConfig.ApplyConfiguration|public${}
}
`

View File

@ -0,0 +1,99 @@
/*
Copyright 2021 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"
"strings"
"k8s.io/gengo/types"
)
// TODO: This implements the same functionality as https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apimachinery/pkg/runtime/converter.go#L236
// but is based on the highly efficient approach from https://golang.org/src/encoding/json/encode.go
// JSONTags represents a go json field tag.
type JSONTags struct {
name string
omit bool
inline bool
omitempty bool
}
func (t JSONTags) String() string {
var tag string
if !t.inline {
tag += t.name
}
if t.omitempty {
tag += ",omitempty"
}
if t.inline {
tag += ",inline"
}
return tag
}
func lookupJSONTags(m types.Member) (JSONTags, bool) {
tag := reflect.StructTag(m.Tags).Get("json")
if tag == "" || tag == "-" {
return JSONTags{}, false
}
name, opts := parseTag(tag)
if name == "" {
name = m.Name
}
return JSONTags{
name: name,
omit: false,
inline: opts.Contains("inline"),
omitempty: opts.Contains("omitempty"),
}, true
}
type tagOptions string
// parseTag splits a struct field's json tag into its name and
// comma-separated options.
func parseTag(tag string) (string, tagOptions) {
if idx := strings.Index(tag, ","); idx != -1 {
return tag[:idx], tagOptions(tag[idx+1:])
}
return tag, ""
}
// Contains reports whether a comma-separated listAlias of options
// contains a particular substr flag. substr must be surrounded by a
// string boundary or commas.
func (o tagOptions) Contains(optionName string) bool {
if len(o) == 0 {
return false
}
s := string(o)
for s != "" {
var next string
i := strings.Index(s, ",")
if i >= 0 {
s, next = s[:i], s[i+1:]
}
if s == optionName {
return true
}
s = next
}
return false
}

View File

@ -0,0 +1,215 @@
/*
Copyright 2021 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 (
"path"
"path/filepath"
"sort"
"strings"
"k8s.io/gengo/args"
"k8s.io/gengo/generator"
"k8s.io/gengo/namer"
"k8s.io/gengo/types"
"k8s.io/klog/v2"
clientgentypes "k8s.io/code-generator/cmd/client-gen/types"
)
const (
// ApplyConfigurationTypeSuffix is the suffix of generated apply configuration types.
ApplyConfigurationTypeSuffix = "ApplyConfiguration"
)
// NameSystems returns the name system used by the generators in this package.
func NameSystems() namer.NameSystems {
return namer.NameSystems{
"public": namer.NewPublicNamer(0),
"private": namer.NewPrivateNamer(0),
"raw": namer.NewRawNamer("", nil),
}
}
// DefaultNameSystem returns the default name system for ordering the types to be
// processed by the generators in this package.
func DefaultNameSystem() string {
return "public"
}
// Packages makes the client package definition.
func Packages(context *generator.Context, arguments *args.GeneratorArgs) generator.Packages {
boilerplate, err := arguments.LoadGoBoilerplate()
if err != nil {
klog.Fatalf("Failed loading boilerplate: %v", err)
}
pkgTypes := packageTypesForInputDirs(context, arguments.InputDirs, arguments.OutputPackagePath)
refs := refGraphForReachableTypes(pkgTypes)
groupVersions := make(map[string]clientgentypes.GroupVersions)
groupGoNames := make(map[string]string)
applyConfigsForGroupVersion := make(map[clientgentypes.GroupVersion][]applyConfig)
var packageList generator.Packages
for pkg, p := range pkgTypes {
gv := groupVersion(p)
pkgType := types.Name{Name: gv.Group.PackageName(), Package: pkg}
var toGenerate []applyConfig
for _, t := range p.Types {
if typePkg, ok := refs[t.Name]; ok {
toGenerate = append(toGenerate, applyConfig{
Type: t,
ApplyConfiguration: types.Ref(typePkg, t.Name.Name+ApplyConfigurationTypeSuffix),
})
}
}
if len(toGenerate) == 0 {
continue // Don't generate empty packages
}
sort.Sort(applyConfigSort(toGenerate))
// generate the apply configurations
packageList = append(packageList, generatorForApplyConfigurationsPackage(arguments.OutputPackagePath, boilerplate, pkgType, gv, toGenerate, refs))
// group all the generated apply configurations by gv so ForKind() can be generated
groupPackageName := gv.Group.NonEmpty()
groupVersionsEntry, ok := groupVersions[groupPackageName]
if !ok {
groupVersionsEntry = clientgentypes.GroupVersions{
PackageName: groupPackageName,
Group: gv.Group,
}
}
groupVersionsEntry.Versions = append(groupVersionsEntry.Versions, clientgentypes.PackageVersion{
Version: gv.Version,
Package: path.Clean(p.Path),
})
groupGoNames[groupPackageName] = goName(gv, p)
applyConfigsForGroupVersion[gv] = toGenerate
groupVersions[groupPackageName] = groupVersionsEntry
}
// generate ForKind() utility function
packageList = append(packageList, generatorForUtils(arguments.OutputPackagePath, boilerplate, groupVersions, applyConfigsForGroupVersion, groupGoNames))
return packageList
}
func generatorForApplyConfigurationsPackage(outputPackagePath string, boilerplate []byte, packageName types.Name, gv clientgentypes.GroupVersion, typesToGenerate []applyConfig, refs refGraph) *generator.DefaultPackage {
return &generator.DefaultPackage{
PackageName: gv.Version.PackageName(),
PackagePath: packageName.Package,
HeaderText: boilerplate,
GeneratorFunc: func(c *generator.Context) (generators []generator.Generator) {
for _, toGenerate := range typesToGenerate {
generators = append(generators, &applyConfigurationGenerator{
DefaultGen: generator.DefaultGen{
OptionalName: strings.ToLower(toGenerate.Type.Name.Name),
},
outputPackage: outputPackagePath,
localPackage: packageName,
groupVersion: gv,
applyConfig: toGenerate,
imports: generator.NewImportTracker(),
refGraph: refs,
})
}
return generators
},
}
}
func generatorForUtils(outPackagePath string, boilerplate []byte, groupVersions map[string]clientgentypes.GroupVersions, applyConfigsForGroupVersion map[clientgentypes.GroupVersion][]applyConfig, groupGoNames map[string]string) *generator.DefaultPackage {
return &generator.DefaultPackage{
PackageName: filepath.Base(outPackagePath),
PackagePath: outPackagePath,
HeaderText: boilerplate,
GeneratorFunc: func(c *generator.Context) (generators []generator.Generator) {
generators = append(generators, &utilGenerator{
DefaultGen: generator.DefaultGen{
OptionalName: "utils",
},
outputPackage: outPackagePath,
imports: generator.NewImportTracker(),
groupVersions: groupVersions,
typesForGroupVersion: applyConfigsForGroupVersion,
groupGoNames: groupGoNames,
})
return generators
},
}
}
func goName(gv clientgentypes.GroupVersion, p *types.Package) string {
goName := namer.IC(strings.Split(gv.Group.NonEmpty(), ".")[0])
if override := types.ExtractCommentTags("+", p.Comments)["groupGoName"]; override != nil {
goName = namer.IC(override[0])
}
return goName
}
func packageTypesForInputDirs(context *generator.Context, inputDirs []string, outputPath string) map[string]*types.Package {
pkgTypes := map[string]*types.Package{}
for _, inputDir := range inputDirs {
p := context.Universe.Package(inputDir)
internal := isInternalPackage(p)
if internal {
klog.Warningf("Skipping internal package: %s", p.Path)
continue
}
gv := groupVersion(p)
pkg := filepath.Join(outputPath, gv.Group.PackageName(), strings.ToLower(gv.Version.NonEmpty()))
pkgTypes[pkg] = p
}
return pkgTypes
}
func groupVersion(p *types.Package) (gv clientgentypes.GroupVersion) {
parts := strings.Split(p.Path, "/")
gv.Group = clientgentypes.Group(parts[len(parts)-2])
gv.Version = clientgentypes.Version(parts[len(parts)-1])
// 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.
if override := types.ExtractCommentTags("+", p.Comments)["groupName"]; override != nil {
gv.Group = clientgentypes.Group(override[0])
}
return gv
}
// isInternalPackage returns true if the package is an internal package
func isInternalPackage(p *types.Package) bool {
for _, t := range p.Types {
for _, member := range t.Members {
if member.Name == "ObjectMeta" {
return isInternal(member)
}
}
}
return false
}
// isInternal returns true if the tags for a member do not contain a json tag
func isInternal(m types.Member) bool {
_, ok := lookupJSONTags(m)
return !ok
}

View File

@ -0,0 +1,164 @@
/*
Copyright 2021 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 (
"k8s.io/gengo/types"
"k8s.io/code-generator/cmd/client-gen/generators/util"
)
// refGraph maps existing types to the package the corresponding applyConfig types will be generated in
// so that references between apply configurations can be correctly generated.
type refGraph map[types.Name]string
// refGraphForReachableTypes returns a refGraph that contains all reachable types from
// the root clientgen types of the provided packages.
func refGraphForReachableTypes(pkgTypes map[string]*types.Package) refGraph {
refs := refGraph{}
// Include only types that are reachable from the root clientgen types.
// We don't want to generate apply configurations for types that are not reachable from a root
// clientgen type.
reachableTypes := map[types.Name]*types.Type{}
for _, p := range pkgTypes {
for _, t := range p.Types {
tags := genclientTags(t)
hasApply := tags.HasVerb("apply") || tags.HasVerb("applyStatus")
if tags.GenerateClient && hasApply {
findReachableTypes(t, reachableTypes)
}
}
}
for pkg, p := range pkgTypes {
for _, t := range p.Types {
if _, ok := reachableTypes[t.Name]; !ok {
continue
}
if requiresApplyConfiguration(t) {
refs[t.Name] = pkg
}
}
}
return refs
}
// applyConfigForType find the type used in the generate apply configurations for a field.
// This may either be an existing type or one of the other generated applyConfig types.
func (t refGraph) applyConfigForType(field *types.Type) *types.Type {
switch field.Kind {
case types.Struct:
if pkg, ok := t[field.Name]; ok { // TODO(jpbetz): Refs to types defined in a separate system (e.g. TypeMeta if generating a 3rd party controller) end up referencing the go struct, not the apply configuration type
return types.Ref(pkg, field.Name.Name+ApplyConfigurationTypeSuffix)
}
return field
case types.Map:
if _, ok := t[field.Elem.Name]; ok {
return &types.Type{
Kind: types.Map,
Elem: t.applyConfigForType(field.Elem),
}
}
return field
case types.Slice:
if _, ok := t[field.Elem.Name]; ok {
return &types.Type{
Kind: types.Slice,
Elem: t.applyConfigForType(field.Elem),
}
}
return field
case types.Pointer:
return t.applyConfigForType(field.Elem)
default:
return field
}
}
func (t refGraph) isApplyConfig(field *types.Type) bool {
switch field.Kind {
case types.Struct:
_, ok := t[field.Name]
return ok
case types.Pointer:
return t.isApplyConfig(field.Elem)
}
return false
}
// genclientTags returns the genclient Tags for the given type.
func genclientTags(t *types.Type) util.Tags {
return util.MustParseClientGenTags(append(t.SecondClosestCommentLines, t.CommentLines...))
}
// findReachableTypes finds all types transitively reachable from a given root type, including
// the root type itself.
func findReachableTypes(t *types.Type, referencedTypes map[types.Name]*types.Type) {
if _, ok := referencedTypes[t.Name]; ok {
return
}
referencedTypes[t.Name] = t
if t.Elem != nil {
findReachableTypes(t.Elem, referencedTypes)
}
if t.Underlying != nil {
findReachableTypes(t.Underlying, referencedTypes)
}
if t.Key != nil {
findReachableTypes(t.Key, referencedTypes)
}
for _, m := range t.Members {
findReachableTypes(m.Type, referencedTypes)
}
}
// excludeTypes contains well known types that we do not generate apply configurations for.
// Hard coding because we only have two, very specific types that serve a special purpose
// in the type system here.
var excludeTypes = map[types.Name]struct{}{
rawExtension.Name: {},
unknown.Name: {},
// DO NOT ADD TO THIS LIST. If we need to exclude other types, we should consider allowing the
// go type declarations to be annotated as excluded from this generator.
}
// requiresApplyConfiguration returns true if a type applyConfig should be generated for the given type.
// types applyConfig are only generated for struct types that contain fields with json tags.
func requiresApplyConfiguration(t *types.Type) bool {
for t.Kind == types.Alias {
t = t.Underlying
}
if t.Kind != types.Struct {
return false
}
if _, ok := excludeTypes[t.Name]; ok {
return false
}
var hasJSONTaggedMembers bool
for _, member := range t.Members {
if _, ok := lookupJSONTags(member); ok {
hasJSONTaggedMembers = true
}
}
if !hasJSONTaggedMembers {
return false
}
return true
}

View File

@ -0,0 +1,27 @@
/*
Copyright 2021 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 "k8s.io/gengo/types"
var (
applyConfiguration = types.Ref("k8s.io/apimachinery/pkg/runtime", "ApplyConfiguration")
groupVersionKind = types.Ref("k8s.io/apimachinery/pkg/runtime/schema", "GroupVersionKind")
objectMeta = types.Ref("k8s.io/apimachinery/pkg/apis/meta/v1", "ObjectMeta")
rawExtension = types.Ref("k8s.io/apimachinery/pkg/runtime", "RawExtension")
unknown = types.Ref("k8s.io/apimachinery/pkg/runtime", "Unknown")
)

View File

@ -0,0 +1,163 @@
/*
Copyright 2021 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 (
"io"
"sort"
"strings"
clientgentypes "k8s.io/code-generator/cmd/client-gen/types"
"k8s.io/gengo/generator"
"k8s.io/gengo/namer"
"k8s.io/gengo/types"
)
// utilGenerator generates the ForKind() utility function.
type utilGenerator struct {
generator.DefaultGen
outputPackage string
imports namer.ImportTracker
groupVersions map[string]clientgentypes.GroupVersions
groupGoNames map[string]string
typesForGroupVersion map[clientgentypes.GroupVersion][]applyConfig
filtered bool
}
var _ generator.Generator = &utilGenerator{}
func (g *utilGenerator) Filter(*generator.Context, *types.Type) bool {
// generate file exactly once
if !g.filtered {
g.filtered = true
return true
}
return false
}
func (g *utilGenerator) Namers(*generator.Context) namer.NameSystems {
return namer.NameSystems{
"raw": namer.NewRawNamer(g.outputPackage, g.imports),
"singularKind": namer.NewPublicNamer(0),
}
}
func (g *utilGenerator) Imports(*generator.Context) (imports []string) {
return g.imports.ImportLines()
}
type group struct {
GroupGoName string
Name string
Versions []*version
}
type groupSort []group
func (g groupSort) Len() int { return len(g) }
func (g groupSort) Less(i, j int) bool {
return strings.ToLower(g[i].Name) < strings.ToLower(g[j].Name)
}
func (g groupSort) Swap(i, j int) { g[i], g[j] = g[j], g[i] }
type version struct {
Name string
GoName string
Resources []applyConfig
}
type versionSort []*version
func (v versionSort) Len() int { return len(v) }
func (v versionSort) Less(i, j int) bool {
return strings.ToLower(v[i].Name) < strings.ToLower(v[j].Name)
}
func (v versionSort) Swap(i, j int) { v[i], v[j] = v[j], v[i] }
type applyConfig struct {
Type *types.Type
ApplyConfiguration *types.Type
}
type applyConfigSort []applyConfig
func (v applyConfigSort) Len() int { return len(v) }
func (v applyConfigSort) Less(i, j int) bool {
return strings.ToLower(v[i].Type.Name.Name) < strings.ToLower(v[j].Type.Name.Name)
}
func (v applyConfigSort) Swap(i, j int) { v[i], v[j] = v[j], v[i] }
func (g *utilGenerator) GenerateType(c *generator.Context, _ *types.Type, w io.Writer) error {
sw := generator.NewSnippetWriter(w, c, "{{", "}}")
var groups []group
schemeGVs := make(map[*version]*types.Type)
for groupPackageName, groupVersions := range g.groupVersions {
group := group{
GroupGoName: g.groupGoNames[groupPackageName],
Name: groupVersions.Group.NonEmpty(),
Versions: []*version{},
}
for _, v := range groupVersions.Versions {
gv := clientgentypes.GroupVersion{Group: groupVersions.Group, Version: v.Version}
version := &version{
Name: v.Version.NonEmpty(),
GoName: namer.IC(v.Version.NonEmpty()),
Resources: g.typesForGroupVersion[gv],
}
schemeGVs[version] = c.Universe.Variable(types.Name{
Package: g.typesForGroupVersion[gv][0].Type.Name.Package,
Name: "SchemeGroupVersion",
})
group.Versions = append(group.Versions, version)
}
sort.Sort(versionSort(group.Versions))
groups = append(groups, group)
}
sort.Sort(groupSort(groups))
m := map[string]interface{}{
"groups": groups,
"schemeGVs": schemeGVs,
"schemaGroupVersionKind": groupVersionKind,
"applyConfiguration": applyConfiguration,
}
sw.Do(forKindFunc, m)
return sw.Error()
}
var forKindFunc = `
// ForKind returns an apply configuration type for the given GroupVersionKind, or nil if no
// apply configuration type exists for the given GroupVersionKind.
func ForKind(kind {{.schemaGroupVersionKind|raw}}) interface{} {
switch kind {
{{range $group := .groups -}}{{$GroupGoName := .GroupGoName -}}
{{range $version := .Versions -}}
// Group={{$group.Name}}, Version={{.Name}}
{{range .Resources -}}
case {{index $.schemeGVs $version|raw}}.WithKind("{{.Type|singularKind}}"):
return &{{.ApplyConfiguration|raw}}{}
{{end}}
{{end}}
{{end -}}
}
return nil
}
`

View File

@ -0,0 +1,57 @@
/*
Copyright 2021 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.
*/
// typebuilder-gen is a tool for auto-generating apply builder functions.
package main
import (
"flag"
"path/filepath"
"github.com/spf13/pflag"
"k8s.io/gengo/args"
"k8s.io/klog/v2"
generatorargs "k8s.io/code-generator/cmd/applyconfiguration-gen/args"
"k8s.io/code-generator/cmd/applyconfiguration-gen/generators"
"k8s.io/code-generator/pkg/util"
)
func main() {
klog.InitFlags(nil)
genericArgs := generatorargs.NewDefaults()
genericArgs.GoHeaderFilePath = filepath.Join(args.DefaultSourceTree(), util.BoilerplatePath())
genericArgs.AddFlags(pflag.CommandLine)
if err := flag.Set("logtostderr", "true"); err != nil {
klog.Fatalf("Error: %v", err)
}
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
pflag.Parse()
if err := generatorargs.Validate(genericArgs); err != nil {
klog.Fatalf("Error: %v", err)
}
// Run it.
if err := genericArgs.Execute(
generators.NameSystems(),
generators.DefaultNameSystem(),
generators.Packages,
); err != nil {
klog.Fatalf("Error: %v", err)
}
klog.V(2).Info("Completed successfully.")
}

View File

@ -24,6 +24,7 @@ import (
"sort"
"strings"
"k8s.io/code-generator/cmd/client-gen/generators/util"
"k8s.io/code-generator/cmd/client-gen/types"
)
@ -120,7 +121,7 @@ func NewGroupVersionsBuilder(groups *[]types.GroupVersions) *groupVersionsBuilde
func (p *groupVersionsBuilder) update() error {
var seenGroups = make(map[types.Group]*types.GroupVersions)
for _, v := range p.groups {
pth, gvString := parsePathGroupVersion(v)
pth, gvString := util.ParsePathGroupVersion(v)
gv, err := types.ToGroupVersion(gvString)
if err != nil {
return err
@ -151,17 +152,6 @@ func (p *groupVersionsBuilder) update() error {
return nil
}
func parsePathGroupVersion(pgvString string) (gvPath string, gvString string) {
subs := strings.Split(pgvString, "/")
length := len(subs)
switch length {
case 0, 1, 2:
return "", pgvString
default:
return strings.Join(subs[:length-2], "/"), strings.Join(subs[length-2:], "/")
}
}
func readAsCSV(val string) ([]string, error) {
if val == "" {
return []string{}, nil

View File

@ -302,7 +302,7 @@ func generateInterface(tags util.Tags) string {
// need an ordered list here to guarantee order of generated methods.
out := []string{}
for _, m := range util.SupportedVerbs {
if tags.HasVerb(m) {
if tags.HasVerb(m) && len(defaultVerbTemplates[m]) > 0 {
out = append(out, defaultVerbTemplates[m])
}
}

View File

@ -0,0 +1,30 @@
/*
Copyright 2021 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 util
import "strings"
func ParsePathGroupVersion(pgvString string) (gvPath string, gvString string) {
subs := strings.Split(pgvString, "/")
length := len(subs)
switch length {
case 0, 1, 2:
return "", pgvString
default:
return strings.Join(subs[:length-2], "/"), strings.Join(subs[length-2:], "/")
}
}

View File

@ -46,6 +46,8 @@ var SupportedVerbs = []string{
"list",
"watch",
"patch",
"apply",
"applyStatus",
}
// ReadonlyVerbs represents a list of read-only verbs.

View File

@ -57,11 +57,11 @@ func TestParseTags(t *testing.T) {
},
"genclient:onlyVerbs": {
lines: []string{`+genclient`, `+genclient:onlyVerbs=create,delete`},
expectTags: Tags{GenerateClient: true, SkipVerbs: []string{"update", "updateStatus", "deleteCollection", "get", "list", "watch", "patch"}},
expectTags: Tags{GenerateClient: true, SkipVerbs: []string{"update", "updateStatus", "deleteCollection", "get", "list", "watch", "patch", "apply", "applyStatus"}},
},
"genclient:readonly": {
lines: []string{`+genclient`, `+genclient:readonly`},
expectTags: Tags{GenerateClient: true, SkipVerbs: []string{"create", "update", "updateStatus", "delete", "deleteCollection", "patch"}},
expectTags: Tags{GenerateClient: true, SkipVerbs: []string{"create", "update", "updateStatus", "delete", "deleteCollection", "patch", "apply", "applyStatus"}},
},
"genclient:conflict": {
lines: []string{`+genclient`, `+genclient:onlyVerbs=create`, `+genclient:skipVerbs=create`},

View File

@ -16,6 +16,8 @@ limitations under the License.
package types
import "strings"
type Version string
func (v Version) String() string {
@ -29,6 +31,10 @@ func (v Version) NonEmpty() string {
return v.String()
}
func (v Version) PackageName() string {
return strings.ToLower(v.NonEmpty())
}
type Group string
func (g Group) String() string {
@ -42,6 +48,14 @@ func (g Group) NonEmpty() string {
return string(g)
}
func (g Group) PackageName() string {
parts := strings.Split(g.NonEmpty(), ".")
if parts[0] == "internal" && len(parts) > 1 {
return strings.ToLower(parts[1] + parts[0])
}
return strings.ToLower(parts[0])
}
type PackageVersion struct {
Version
// The fully qualified package, e.g. k8s.io/kubernetes/pkg/apis/apps, where the types.go is found.
@ -53,6 +67,14 @@ type GroupVersion struct {
Version Version
}
func (gv GroupVersion) ToAPIVersion() string {
if len(gv.Group) > 0 && gv.Group.NonEmpty() != "core" {
return gv.Group.String() + "/" + gv.Version.String()
} else {
return gv.Version.String()
}
}
type GroupVersions struct {
// The name of the package for this group, e.g. apps.
PackageName string