From d8780fb2163ab275877bdf7fc73e5414e94ccf20 Mon Sep 17 00:00:00 2001 From: Tamal Saha Date: Wed, 2 Sep 2026 20:54:12 +0600 Subject: [PATCH] gentype: add a create-only client for non-object request/response types Some aggregated-apiserver API types are pure request/response "action" payloads with no real persisted identity -- e.g. an editor-model resolver or a token-exchange endpoint that only ever accepts POST -- and so deliberately carry no metav1.ObjectMeta. k8s.io/client-go/gentype.Client[T] (which every client-gen-generated typed client has been built on since https://github.com/kubernetes/kubernetes/pull/121439) requires its type parameter to satisfy metav1.Object, even when the generated client only exposes Create: Client[T].Create never actually calls any metav1.Object method on T -- only Update/Get/Delete do (obj.GetName(), to build the REST path) -- but the constraint is declared on the whole generic struct rather than per-method, so every type needs it regardless of which verbs its generated client actually has. This adds a parallel Client[T runtime.Object]/FakeClient[T runtime.Object] pair, matching k8s.io/client-go/gentype's shape (NewClient, Option, PrefersProtobuf, GetClient, GetNamespace, Create) but scoped to exactly what a create-only client needs, for use by kmodules/code-generator's client-gen when it detects a +genclient:onlyVerbs=create type with no ObjectMeta (see the matching client-gen patch). Signed-off-by: Tamal Saha --- gentype/fake.go | 56 ++++++++++++++++++++++++ gentype/type.go | 113 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 169 insertions(+) create mode 100644 gentype/fake.go create mode 100644 gentype/type.go diff --git a/gentype/fake.go b/gentype/fake.go new file mode 100644 index 000000000..20051cb06 --- /dev/null +++ b/gentype/fake.go @@ -0,0 +1,56 @@ +/* +Copyright AppsCode Inc. and Contributors + +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 gentype + +import ( + "context" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/testing" +) + +// FakeClient represents a fake create-only client for a runtime.Object type +// with no real persisted identity, matching Client's shape. +type FakeClient[T runtime.Object] struct { + *testing.Fake + ns string + resource schema.GroupVersionResource + kind schema.GroupVersionKind + newObject func() T +} + +// NewFakeClient constructs a fake create-only client, namespaced or not. +// Non-namespaced clients are constructed by passing an empty namespace (""). +func NewFakeClient[T runtime.Object]( + fake *testing.Fake, namespace string, resource schema.GroupVersionResource, kind schema.GroupVersionKind, emptyObjectCreator func() T, +) *FakeClient[T] { + return &FakeClient[T]{fake, namespace, resource, kind, emptyObjectCreator} +} + +// Create takes the representation of a resource and creates it. Returns the +// server's representation of the resource, and an error, if there is any. +func (c *FakeClient[T]) Create(ctx context.Context, resource T, opts metav1.CreateOptions) (result T, err error) { + emptyResult := c.newObject() + obj, err := c.Fake. + Invokes(testing.NewCreateActionWithOptions(c.resource, c.ns, resource, opts), emptyResult) + if obj == nil { + return emptyResult, err + } + return obj.(T), err +} diff --git a/gentype/type.go b/gentype/type.go new file mode 100644 index 000000000..dbbe66648 --- /dev/null +++ b/gentype/type.go @@ -0,0 +1,113 @@ +/* +Copyright AppsCode Inc. and Contributors + +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 gentype provides a generic, create-only client-go typed client +// for API types that are not real stored Kubernetes objects -- request/ +// response "action" payloads (e.g. an aggregated-apiserver endpoint that +// only ever accepts POST) which have no metav1.ObjectMeta and thus no real +// identity, labels, owner references, etc. +// +// k8s.io/client-go/gentype's Client[T] (which every generated client built +// since https://github.com/kubernetes/kubernetes/pull/121439 embeds) +// requires its type parameter to implement metav1.Object, even when the +// generated client only ever exposes Create -- Client[T].Create doesn't +// call any metav1.Object method on T, but the constraint is declared on the +// whole generic struct rather than per-method, so every type needs it +// regardless of which verbs its generated client actually has. +// +// This package provides the same shape (Client, NewClient, Option, +// PrefersProtobuf) but constrained to plain runtime.Object, for use by +// kmodules/code-generator's client-gen when it detects a +genclient type +// that is both create-only (+genclient:onlyVerbs=create, no other verb) and +// has no metav1.ObjectMeta member -- see cmd/client-gen/generators/util. +// IsCreateOnly/HasObjectMeta in that fork. Get/List/Update/Delete/Watch/ +// Patch/Apply all need real object identity and have no equivalent here; +// a type needing any of those must have a real metav1.ObjectMeta and use +// k8s.io/client-go/gentype directly instead. +package gentype + +import ( + "context" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/rest" +) + +// Client represents a create-only client, optionally namespaced, for a +// runtime.Object type with no real persisted identity. +type Client[T runtime.Object] struct { + resource string + client rest.Interface + namespace string // "" for non-namespaced clients + newObject func() T + parameterCodec runtime.ParameterCodec + + prefersProtobuf bool +} + +// Option configures a Client. +type Option[T runtime.Object] func(*Client[T]) + +// PrefersProtobuf marks the client as preferring protobuf, matching +// k8s.io/client-go/gentype.PrefersProtobuf. +func PrefersProtobuf[T runtime.Object]() Option[T] { + return func(c *Client[T]) { c.prefersProtobuf = true } +} + +// NewClient constructs a create-only client, namespaced or not. Non- +// namespaced clients are constructed by passing an empty namespace (""). +func NewClient[T runtime.Object]( + resource string, client rest.Interface, parameterCodec runtime.ParameterCodec, namespace string, emptyObjectCreator func() T, + options ...Option[T], +) *Client[T] { + c := &Client[T]{ + resource: resource, + client: client, + parameterCodec: parameterCodec, + namespace: namespace, + newObject: emptyObjectCreator, + } + for _, option := range options { + option(c) + } + return c +} + +// GetClient returns the REST interface. +func (c *Client[T]) GetClient() rest.Interface { + return c.client +} + +// GetNamespace returns the client's namespace, if any. +func (c *Client[T]) GetNamespace() string { + return c.namespace +} + +// Create takes the representation of a resource and creates it. Returns the +// server's representation of the resource, and an error, if there is any. +func (c *Client[T]) Create(ctx context.Context, obj T, opts metav1.CreateOptions) (T, error) { + result := c.newObject() + err := c.client.Post(). + UseProtobufAsDefaultIfPreferred(c.prefersProtobuf). + NamespaceIfScoped(c.namespace, c.namespace != ""). + Resource(c.resource). + VersionedParams(&opts, c.parameterCodec). + Body(obj). + Do(ctx). + Into(result) + return result, err +}