diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index b4614898d99..7563e70d014 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -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" "$@" diff --git a/pkg/api/testing/applyconfiguration_test.go b/pkg/api/testing/applyconfiguration_test.go new file mode 100644 index 00000000000..16d67d4cb6f --- /dev/null +++ b/pkg/api/testing/applyconfiguration_test.go @@ -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() +} diff --git a/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/args/args.go b/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/args/args.go new file mode 100644 index 00000000000..5264f1a2a77 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/args/args.go @@ -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 +} diff --git a/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/generators/applyconfiguration.go b/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/generators/applyconfiguration.go new file mode 100644 index 00000000000..bed276b9b77 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/generators/applyconfiguration.go @@ -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${} +} +` diff --git a/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/generators/jsontagutil.go b/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/generators/jsontagutil.go new file mode 100644 index 00000000000..2a643290bb9 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/generators/jsontagutil.go @@ -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 +} diff --git a/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/generators/packages.go b/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/generators/packages.go new file mode 100644 index 00000000000..bb794f73c38 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/generators/packages.go @@ -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 +} diff --git a/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/generators/refgraph.go b/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/generators/refgraph.go new file mode 100644 index 00000000000..600012bd79c --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/generators/refgraph.go @@ -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 +} diff --git a/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/generators/types.go b/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/generators/types.go new file mode 100644 index 00000000000..28b4c3007e5 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/generators/types.go @@ -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") +) diff --git a/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/generators/util.go b/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/generators/util.go new file mode 100644 index 00000000000..258293afea5 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/generators/util.go @@ -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 +} +` diff --git a/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/main.go b/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/main.go new file mode 100644 index 00000000000..af8d5f23e67 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/applyconfiguration-gen/main.go @@ -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.") +} diff --git a/staging/src/k8s.io/code-generator/cmd/client-gen/args/gvpackages.go b/staging/src/k8s.io/code-generator/cmd/client-gen/args/gvpackages.go index 8da71d6f9bf..50d29a95be1 100644 --- a/staging/src/k8s.io/code-generator/cmd/client-gen/args/gvpackages.go +++ b/staging/src/k8s.io/code-generator/cmd/client-gen/args/gvpackages.go @@ -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 diff --git a/staging/src/k8s.io/code-generator/cmd/client-gen/generators/generator_for_type.go b/staging/src/k8s.io/code-generator/cmd/client-gen/generators/generator_for_type.go index 13664d0d924..87f663342ff 100644 --- a/staging/src/k8s.io/code-generator/cmd/client-gen/generators/generator_for_type.go +++ b/staging/src/k8s.io/code-generator/cmd/client-gen/generators/generator_for_type.go @@ -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]) } } diff --git a/staging/src/k8s.io/code-generator/cmd/client-gen/generators/util/gvpackages.go b/staging/src/k8s.io/code-generator/cmd/client-gen/generators/util/gvpackages.go new file mode 100644 index 00000000000..fcc1950909b --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/client-gen/generators/util/gvpackages.go @@ -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:], "/") + } +} diff --git a/staging/src/k8s.io/code-generator/cmd/client-gen/generators/util/tags.go b/staging/src/k8s.io/code-generator/cmd/client-gen/generators/util/tags.go index 426b3927311..71346061f0c 100644 --- a/staging/src/k8s.io/code-generator/cmd/client-gen/generators/util/tags.go +++ b/staging/src/k8s.io/code-generator/cmd/client-gen/generators/util/tags.go @@ -46,6 +46,8 @@ var SupportedVerbs = []string{ "list", "watch", "patch", + "apply", + "applyStatus", } // ReadonlyVerbs represents a list of read-only verbs. diff --git a/staging/src/k8s.io/code-generator/cmd/client-gen/generators/util/tags_test.go b/staging/src/k8s.io/code-generator/cmd/client-gen/generators/util/tags_test.go index a5c93875483..b9ffcfbbbae 100644 --- a/staging/src/k8s.io/code-generator/cmd/client-gen/generators/util/tags_test.go +++ b/staging/src/k8s.io/code-generator/cmd/client-gen/generators/util/tags_test.go @@ -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`}, diff --git a/staging/src/k8s.io/code-generator/cmd/client-gen/types/types.go b/staging/src/k8s.io/code-generator/cmd/client-gen/types/types.go index 7d1606c508a..a5cc37cf7fc 100644 --- a/staging/src/k8s.io/code-generator/cmd/client-gen/types/types.go +++ b/staging/src/k8s.io/code-generator/cmd/client-gen/types/types.go @@ -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