mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-24 04:06:03 +00:00
Add apply configuration generator
This commit is contained in:
parent
60a714058b
commit
09cc895c84
@ -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" "$@"
|
||||
|
241
pkg/api/testing/applyconfiguration_test.go
Normal file
241
pkg/api/testing/applyconfiguration_test.go
Normal 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()
|
||||
}
|
@ -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
|
||||
}
|
@ -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${}
|
||||
}
|
||||
`
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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")
|
||||
)
|
@ -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
|
||||
}
|
||||
`
|
@ -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.")
|
||||
}
|
@ -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
|
||||
|
@ -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])
|
||||
}
|
||||
}
|
||||
|
@ -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:], "/")
|
||||
}
|
||||
}
|
@ -46,6 +46,8 @@ var SupportedVerbs = []string{
|
||||
"list",
|
||||
"watch",
|
||||
"patch",
|
||||
"apply",
|
||||
"applyStatus",
|
||||
}
|
||||
|
||||
// ReadonlyVerbs represents a list of read-only verbs.
|
||||
|
@ -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`},
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user