From abc79fbea7a7e798d0f919df7c07893ca62ab014 Mon Sep 17 00:00:00 2001 From: David Desmarais-Michaud Date: Wed, 24 Jun 2026 20:39:30 -0400 Subject: [PATCH] pkg/k8s: make generic typed interface client namespace aware when dealing with input resources --- cmd/atc/flight_test.go | 16 ++++++++-------- cmd/atc/main_test.go | 22 +++++++++++----------- internal/atc/reconciler_flight.go | 2 +- internal/k8s/client.go | 18 +++++++++++++++--- internal/k8s/k8s.go | 4 ++-- pkg/k8s/client.go | 5 ++--- 6 files changed, 39 insertions(+), 28 deletions(-) diff --git a/cmd/atc/flight_test.go b/cmd/atc/flight_test.go index ffe9d6d2..41f86696 100644 --- a/cmd/atc/flight_test.go +++ b/cmd/atc/flight_test.go @@ -34,7 +34,7 @@ func TestFlightInstance(t *testing.T) { client, err := k8s.NewClientFromKubeConfig(home.Kubeconfig) require.NoError(t, err) - flightIntf := k8s.TypedInterface[v1alpha1.Flight](client.Dynamic, v1alpha1.FlightGVR()).Namespace("default") + flightIntf := k8s.TypedInterface[v1alpha1.Flight](client, v1alpha1.FlightGVR()).Namespace("default") toJSONString := func(t *testing.T, value any) string { var buffer bytes.Buffer @@ -132,7 +132,7 @@ func TestFlightCrossNamespace(t *testing.T) { client, err := k8s.NewClientFromKubeConfig(home.Kubeconfig) require.NoError(t, err) - flightIntf := k8s.TypedInterface[v1alpha1.Flight](client.Dynamic, v1alpha1.FlightGVR()).Namespace("default") + flightIntf := k8s.TypedInterface[v1alpha1.Flight](client, v1alpha1.FlightGVR()).Namespace("default") _, err = flightIntf.Create( context.Background(), @@ -144,7 +144,7 @@ func TestFlightCrossNamespace(t *testing.T) { ) require.ErrorContains(t, err, "Multiple namespaces detected (if desired enable multinamespace releases)") - clusterFlightIntf := k8s.TypedInterface[v1alpha1.ClusterFlight](client.Dynamic, v1alpha1.ClusterFlightGVR()) + clusterFlightIntf := k8s.TypedInterface[v1alpha1.ClusterFlight](client, v1alpha1.ClusterFlightGVR()) // Crossnamespace depends on "foo" and "bar" for _, ns := range []string{"foo", "bar"} { @@ -205,7 +205,7 @@ func TestFlightInputObject(t *testing.T) { client, err := k8s.NewClientFromKubeConfig(home.Kubeconfig) require.NoError(t, err) - flightIntf := k8s.TypedInterface[v1alpha1.Flight](client.Dynamic, v1alpha1.FlightGVR()).Namespace("default") + flightIntf := k8s.TypedInterface[v1alpha1.Flight](client, v1alpha1.FlightGVR()).Namespace("default") flight, err := flightIntf.Create( context.Background(), @@ -261,7 +261,7 @@ func TestFlightValidationWebhook(t *testing.T) { client, err := k8s.NewClientFromKubeConfig(home.Kubeconfig) require.NoError(t, err) - flightIntf := k8s.TypedInterface[v1alpha1.Flight](client.Dynamic, v1alpha1.FlightGVR()).Namespace("default") + flightIntf := k8s.TypedInterface[v1alpha1.Flight](client, v1alpha1.FlightGVR()).Namespace("default") _, err = flightIntf.Create( context.Background(), @@ -282,7 +282,7 @@ func TestNotAllowedFlightWasmURL(t *testing.T) { client, err := k8s.NewClientFromKubeConfig(home.Kubeconfig) require.NoError(t, err) - flightIntf := k8s.TypedInterface[v1alpha1.Flight](client.Dynamic, v1alpha1.FlightGVR()).Namespace("default") + flightIntf := k8s.TypedInterface[v1alpha1.Flight](client, v1alpha1.FlightGVR()).Namespace("default") _, err = flightIntf.Create( context.Background(), @@ -299,7 +299,7 @@ func TestFlightInvalidChecksum(t *testing.T) { client, err := k8s.NewClientFromKubeConfig(home.Kubeconfig) require.NoError(t, err) - flightIntf := k8s.TypedInterface[v1alpha1.Flight](client.Dynamic, v1alpha1.FlightGVR()).Namespace("default") + flightIntf := k8s.TypedInterface[v1alpha1.Flight](client, v1alpha1.FlightGVR()).Namespace("default") // Create the flight initially so that we can warm the cache. // Otherwise it'll behave different when we run this test alone versus with the rest of the test suite. @@ -405,7 +405,7 @@ func TestFlightCodeSigning(t *testing.T) { Insecure: true, })) - flightIntf := k8s.TypedInterface[v1alpha1.Flight](client.Dynamic, v1alpha1.FlightGVR()).Namespace("default") + flightIntf := k8s.TypedInterface[v1alpha1.Flight](client, v1alpha1.FlightGVR()).Namespace("default") _, err = flightIntf.Create( context.Background(), diff --git a/cmd/atc/main_test.go b/cmd/atc/main_test.go index 33eddbe9..db6869f0 100644 --- a/cmd/atc/main_test.go +++ b/cmd/atc/main_test.go @@ -1056,7 +1056,7 @@ func TestClusterScopeDynamicAirway(t *testing.T) { require.NoError(t, client.EnsureNamespace(context.Background(), ns)) } - testIntf := k8s.TypedInterface[EmptyCRD](client.Dynamic, schema.GroupVersionResource{ + testIntf := k8s.TypedInterface[EmptyCRD](client, schema.GroupVersionResource{ Group: "examples.com", Version: "v1", Resource: "tests", @@ -2219,7 +2219,7 @@ func TestDynamicWithExternalResource(t *testing.T) { configMapIntf := client.Clientset.CoreV1().ConfigMaps("default") backendIntf := k8s. - TypedInterface[EmptyCRD](client.Dynamic, schema.GroupVersionResource{ + TypedInterface[EmptyCRD](client, schema.GroupVersionResource{ Group: "examples.com", Version: "v1", Resource: "backends", @@ -2383,7 +2383,7 @@ func TestExternalDynamicCreateEvent(t *testing.T) { configmapIntf := client.Clientset.CoreV1().ConfigMaps("default") cloneIntf := k8s. - TypedInterface[CopyJob](client.Dynamic, schema.GroupVersionResource{ + TypedInterface[CopyJob](client, schema.GroupVersionResource{ Group: "examples.com", Version: "v1", Resource: "clones", @@ -2544,7 +2544,7 @@ func TestStatusUpdates(t *testing.T) { Resource: "backends", } - backendIntf := k8s.TypedInterface[CR](client.Dynamic, backendGVR).Namespace("default") + backendIntf := k8s.TypedInterface[CR](client, backendGVR).Namespace("default") cases := []struct { Name string @@ -2783,7 +2783,7 @@ func TestDeploymentStatus(t *testing.T) { Resource: "backends", } - backendIntf := k8s.TypedInterface[CR](client.Dynamic, backendGVR).Namespace("default") + backendIntf := k8s.TypedInterface[CR](client, backendGVR).Namespace("default") be := &CR{ TypeMeta: metav1.TypeMeta{ @@ -3311,7 +3311,7 @@ func TestTimeout(t *testing.T) { }() timeoutIntf := k8s. - TypedInterface[EmptyCRD](client.Dynamic, schema.GroupVersionResource{ + TypedInterface[EmptyCRD](client, schema.GroupVersionResource{ Group: "examples.com", Version: "v1", Resource: "timeouts", @@ -3403,7 +3403,7 @@ func TestSubscriptionMode(t *testing.T) { ) }() - subIntf := k8s.TypedInterface[EmptyCRD](client.Dynamic, schema.GroupVersionResource{ + subIntf := k8s.TypedInterface[EmptyCRD](client, schema.GroupVersionResource{ Group: "example.com", Version: "v1", Resource: "subscriptions", @@ -3568,7 +3568,7 @@ func TestValidationCycle(t *testing.T) { require.NoError(t, client.EnsureNamespace(context.Background(), "foo")) - backendIntf := k8s.TypedInterface[backendv1.Backend](client.Dynamic, schema.GroupVersionResource{ + backendIntf := k8s.TypedInterface[backendv1.Backend](client, schema.GroupVersionResource{ Resource: "backends", Group: "examples.com", Version: "v1", @@ -3682,7 +3682,7 @@ func TestIdentityWithError(t *testing.T) { Version: "v1", } - testIntf := k8s.TypedInterface[CR](client.Dynamic, testGVR).Namespace("default") + testIntf := k8s.TypedInterface[CR](client, testGVR).Namespace("default") test, err := testIntf.Create( context.Background(), @@ -3798,7 +3798,7 @@ func TestInvalidFlightURL(t *testing.T) { Resource: "backends", } - backendIntf := k8s.TypedInterface[backendv1.Backend](client.Dynamic, backendGVR).Namespace("default") + backendIntf := k8s.TypedInterface[backendv1.Backend](client, backendGVR).Namespace("default") be, err := backendIntf.Create( context.Background(), @@ -3890,7 +3890,7 @@ func TestInvalidChecksum(t *testing.T) { Resource: "backends", } - backendIntf := k8s.TypedInterface[backendv1.Backend](client.Dynamic, backendGVR).Namespace("default") + backendIntf := k8s.TypedInterface[backendv1.Backend](client, backendGVR).Namespace("default") be, err := backendIntf.Create( context.Background(), diff --git a/internal/atc/reconciler_flight.go b/internal/atc/reconciler_flight.go index 3575d622..e8b2ae42 100644 --- a/internal/atc/reconciler_flight.go +++ b/internal/atc/reconciler_flight.go @@ -55,7 +55,7 @@ func flightReconciler(modules *cache.ModuleCache, clusterScope bool) ctrl.Funcs var ( client = (*k8s.Client)(ctrl.Client(ctx)) commander = yoke.FromK8Client(client) - flightIntf = k8s.TypedInterface[AltFlight](client.Dynamic, gvr).Namespace(evt.Namespace) + flightIntf = k8s.TypedInterface[AltFlight](client, gvr).Namespace(evt.Namespace) flightCache = ctrl.CacheFromEvent[AltFlight](ctx, evt) ) diff --git a/internal/k8s/client.go b/internal/k8s/client.go index f09af972..aaebb701 100644 --- a/internal/k8s/client.go +++ b/internal/k8s/client.go @@ -36,7 +36,7 @@ func (c TypedIntf[T]) Get(ctx context.Context, name string, options metav1.GetOp } var result T if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &result); err != nil { - return nil, fmt.Errorf("failed to convert unstructerd value to typed api: %w", err) + return nil, fmt.Errorf("failed to convert unstructured value to typed api: %w", err) } return &result, nil } @@ -46,6 +46,9 @@ func (c TypedIntf[T]) Create(ctx context.Context, api *T, options metav1.CreateO if err != nil { return nil, fmt.Errorf("failed to convert typed api to unstructured object: %w", err) } + if ns := obj.GetNamespace(); ns != "" { + c = c.Namespace(ns) + } obj, err = c.getIntf().Create(ctx, obj, options) if err != nil { return nil, err @@ -62,6 +65,9 @@ func (c TypedIntf[T]) Update(ctx context.Context, api *T, options metav1.UpdateO if err != nil { return nil, fmt.Errorf("failed to convert typed api to unstructured object: %w", err) } + if ns := obj.GetNamespace(); ns != "" { + c = c.Namespace(ns) + } obj, err = c.getIntf().Update(ctx, obj, options) if err != nil { return nil, err @@ -78,6 +84,9 @@ func (c TypedIntf[T]) Apply(ctx context.Context, api *T, options metav1.ApplyOpt if err != nil { return nil, fmt.Errorf("failed to convert typed api to unstructured object: %w", err) } + if ns := obj.GetNamespace(); ns != "" { + c = c.Namespace(ns) + } obj, err = c.getIntf().Apply(ctx, obj.GetName(), obj, options) if err != nil { return nil, err @@ -94,6 +103,9 @@ func (c TypedIntf[T]) UpdateStatus(ctx context.Context, api *T, options metav1.U if err != nil { return nil, fmt.Errorf("failed to convert typed api to unstructured object: %w", err) } + if ns := obj.GetNamespace(); ns != "" { + c = c.Namespace(ns) + } obj, err = c.getIntf().UpdateStatus(ctx, obj, options) if err != nil { return nil, err @@ -130,9 +142,9 @@ type MetaObject[T any] interface { metav1.Object } -func TypedInterface[T any, obj MetaObject[T]](client *dynamic.DynamicClient, resource schema.GroupVersionResource) TypedIntf[T] { +func TypedInterface[T any, obj MetaObject[T]](client *Client, resource schema.GroupVersionResource) TypedIntf[T] { return TypedIntf[T]{ - intf: client.Resource(resource), + intf: client.Dynamic.Resource(resource), ns: "", } } diff --git a/internal/k8s/k8s.go b/internal/k8s/k8s.go index 73618a93..e93706c0 100644 --- a/internal/k8s/k8s.go +++ b/internal/k8s/k8s.go @@ -51,8 +51,8 @@ type Client struct { DefaultNamespace string } -func (client Client) AirwayIntf() TypedIntf[v1alpha1.Airway] { - return TypedInterface[v1alpha1.Airway](client.Dynamic, v1alpha1.AirwayGVR()) +func (client *Client) AirwayIntf() TypedIntf[v1alpha1.Airway] { + return TypedInterface[v1alpha1.Airway](client, v1alpha1.AirwayGVR()) } func NewClientFromConfigFlags(cfgFlags *genericclioptions.ConfigFlags) (*Client, error) { diff --git a/pkg/k8s/client.go b/pkg/k8s/client.go index f1aadbe9..5a327d23 100644 --- a/pkg/k8s/client.go +++ b/pkg/k8s/client.go @@ -7,7 +7,6 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/dynamic" "k8s.io/client-go/rest" "github.com/yokecd/yoke/internal/k8s" @@ -30,8 +29,8 @@ type TypedIntf[T any] = k8s.TypedIntf[T] // TypedInterface returns a typed wrapper over the client-go dynamic client. // // TODO: once go1.27 is out and generic functions are added this should become a method of the standard client. -func TypedInterface[T any, obj k8s.MetaObject[T]](client *dynamic.DynamicClient, resource schema.GroupVersionResource) TypedIntf[T] { - return k8s.TypedInterface[T, obj](client, resource) +func TypedInterface[T any, obj k8s.MetaObject[T]](client *Client, resource schema.GroupVersionResource) TypedIntf[T] { + return k8s.TypedInterface[T, obj]((*k8s.Client)(client), resource) } type WaitOptions = k8s.WaitOptions