Skip to content

Commit c411a8c

Browse files
committed
fix: convert typed objects to unstructured in dfake
This fixes an issue where typed objects passed to NewFakeDynamicClient would cause a panic due to scheme incompatibility. The fix converts typed objects to unstructured before passing them to the upstream dynamic fake client, ensuring compatibility with the dynamic scheme that only knows about unstructured types. Fixes: 'no kind is registered for the type' panic when using typed objects
1 parent d5b36f1 commit c411a8c

File tree

2 files changed

+194
-2
lines changed

2 files changed

+194
-2
lines changed

client/fake/fake.go

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -492,8 +492,40 @@ func newFakeClientWithSpec(scheme *runtime.Scheme, gvrToListKind map[schema.Grou
492492
// and custom types not in the schema
493493
fieldManagedTracker := clientgotesting.NewFieldManagedObjectTracker(scheme, decoder, typeConverter)
494494

495-
// Create upstream dynamic client with dynamic scheme
496-
upstreamClient := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(dynamicScheme, gvrToListKind, objects...)
495+
// Convert typed objects to unstructured before passing to upstream client
496+
// This ensures compatibility with the dynamic scheme that only knows about unstructured types
497+
unstructuredObjects := make([]runtime.Object, len(objects))
498+
for i, obj := range objects {
499+
if unstructuredObj, ok := obj.(*unstructured.Unstructured); ok {
500+
// Already unstructured, use as-is
501+
unstructuredObjects[i] = unstructuredObj
502+
} else {
503+
// Convert typed object to unstructured
504+
unstructuredMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
505+
if err != nil {
506+
panic(fmt.Sprintf("failed to convert object to unstructured: %v", err))
507+
}
508+
unstructuredObj := &unstructured.Unstructured{Object: unstructuredMap}
509+
510+
// Set GVK from the original object
511+
gvk := obj.GetObjectKind().GroupVersionKind()
512+
if gvk.Empty() {
513+
// If GVK is empty, try to get it from the original scheme
514+
gvks, _, err := scheme.ObjectKinds(obj)
515+
if err != nil {
516+
panic(fmt.Sprintf("failed to get GVK for object: %v", err))
517+
}
518+
if len(gvks) > 0 {
519+
gvk = gvks[0]
520+
}
521+
}
522+
unstructuredObj.SetGroupVersionKind(gvk)
523+
unstructuredObjects[i] = unstructuredObj
524+
}
525+
}
526+
527+
// Create upstream dynamic client with dynamic scheme and unstructured objects
528+
upstreamClient := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(dynamicScheme, gvrToListKind, unstructuredObjects...)
497529

498530
// Add reactor to handle raw Apply Patch operations that bypass our Apply method
499531
upstreamClient.PrependReactor("patch", "*", func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {

client/fake/fake_test.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"testing"
1010

1111
"github.com/stretchr/testify/require"
12+
appsv1 "k8s.io/api/apps/v1"
13+
corev1 "k8s.io/api/core/v1"
1214
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
1315
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1416
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -1841,6 +1843,164 @@ func TestDoubleRegistrationWithTypedStruct(t *testing.T) {
18411843
require.Equal(t, "test-cluster", retrieved.GetName())
18421844
}
18431845

1846+
func TestTypedObjectsSupport(t *testing.T) {
1847+
scheme := setupScheme()
1848+
1849+
// Create a typed Kubernetes object (Deployment)
1850+
typedDeployment := &appsv1.Deployment{
1851+
ObjectMeta: metav1.ObjectMeta{
1852+
Name: "test-deployment",
1853+
Namespace: "default",
1854+
},
1855+
Spec: appsv1.DeploymentSpec{
1856+
Replicas: ptr.To(int32(3)),
1857+
Selector: &metav1.LabelSelector{
1858+
MatchLabels: map[string]string{
1859+
"app": "test",
1860+
},
1861+
},
1862+
Template: corev1.PodTemplateSpec{
1863+
ObjectMeta: metav1.ObjectMeta{
1864+
Labels: map[string]string{
1865+
"app": "test",
1866+
},
1867+
},
1868+
Spec: corev1.PodSpec{
1869+
Containers: []corev1.Container{
1870+
{
1871+
Name: "test-container",
1872+
Image: "nginx:1.21",
1873+
},
1874+
},
1875+
},
1876+
},
1877+
},
1878+
}
1879+
1880+
// Create a typed ConfigMap
1881+
typedConfigMap := &corev1.ConfigMap{
1882+
ObjectMeta: metav1.ObjectMeta{
1883+
Name: "test-config",
1884+
Namespace: "default",
1885+
},
1886+
Data: map[string]string{
1887+
"key1": "value1",
1888+
"key2": "value2",
1889+
},
1890+
}
1891+
1892+
client := NewFakeDynamicClient(scheme, typedDeployment, typedConfigMap)
1893+
require.NotNil(t, client)
1894+
1895+
// Test that we can interact with the typed objects
1896+
deploymentGVR := schema.GroupVersionResource{
1897+
Group: "apps",
1898+
Version: "v1",
1899+
Resource: "deployments",
1900+
}
1901+
1902+
configMapGVR := schema.GroupVersionResource{
1903+
Group: "",
1904+
Version: "v1",
1905+
Resource: "configmaps",
1906+
}
1907+
1908+
// Test List operation
1909+
deploymentList, err := client.Resource(deploymentGVR).Namespace("default").List(t.Context(), metav1.ListOptions{})
1910+
require.NoError(t, err)
1911+
require.Len(t, deploymentList.Items, 1)
1912+
require.Equal(t, "test-deployment", deploymentList.Items[0].GetName())
1913+
1914+
configMapList, err := client.Resource(configMapGVR).Namespace("default").List(t.Context(), metav1.ListOptions{})
1915+
require.NoError(t, err)
1916+
require.Len(t, configMapList.Items, 1)
1917+
require.Equal(t, "test-config", configMapList.Items[0].GetName())
1918+
1919+
// Test Get operation
1920+
deployment, err := client.Resource(deploymentGVR).Namespace("default").Get(t.Context(), "test-deployment", metav1.GetOptions{})
1921+
require.NoError(t, err)
1922+
require.Equal(t, "test-deployment", deployment.GetName())
1923+
1924+
// Verify the deployment has the correct spec
1925+
replicas, found, err := unstructured.NestedInt64(deployment.Object, "spec", "replicas")
1926+
require.NoError(t, err)
1927+
require.True(t, found)
1928+
require.Equal(t, int64(3), replicas)
1929+
1930+
configMap, err := client.Resource(configMapGVR).Namespace("default").Get(t.Context(), "test-config", metav1.GetOptions{})
1931+
require.NoError(t, err)
1932+
require.Equal(t, "test-config", configMap.GetName())
1933+
1934+
// Verify the configmap has the correct data
1935+
data, found, err := unstructured.NestedStringMap(configMap.Object, "data")
1936+
require.NoError(t, err)
1937+
require.True(t, found)
1938+
require.Equal(t, "value1", data["key1"])
1939+
require.Equal(t, "value2", data["key2"])
1940+
}
1941+
1942+
func TestMixedTypedAndUnstructuredObjects(t *testing.T) {
1943+
scheme := setupScheme()
1944+
1945+
// Create a typed object
1946+
typedPod := &corev1.Pod{
1947+
ObjectMeta: metav1.ObjectMeta{
1948+
Name: "typed-pod",
1949+
Namespace: "default",
1950+
},
1951+
Spec: corev1.PodSpec{
1952+
Containers: []corev1.Container{
1953+
{
1954+
Name: "test-container",
1955+
Image: "nginx:1.21",
1956+
},
1957+
},
1958+
},
1959+
}
1960+
1961+
// Create an unstructured object
1962+
unstructuredConfigMap := &unstructured.Unstructured{
1963+
Object: map[string]interface{}{
1964+
"apiVersion": "v1",
1965+
"kind": "ConfigMap",
1966+
"metadata": map[string]interface{}{
1967+
"name": "unstructured-config",
1968+
"namespace": "default",
1969+
},
1970+
"data": map[string]interface{}{
1971+
"key": "value",
1972+
},
1973+
},
1974+
}
1975+
1976+
// This should handle both typed and unstructured objects correctly
1977+
client := NewFakeDynamicClient(scheme, typedPod, unstructuredConfigMap)
1978+
require.NotNil(t, client)
1979+
1980+
// Test that both objects are accessible
1981+
podGVR := schema.GroupVersionResource{
1982+
Group: "",
1983+
Version: "v1",
1984+
Resource: "pods",
1985+
}
1986+
1987+
configMapGVR := schema.GroupVersionResource{
1988+
Group: "",
1989+
Version: "v1",
1990+
Resource: "configmaps",
1991+
}
1992+
1993+
// Get the typed pod
1994+
pod, err := client.Resource(podGVR).Namespace("default").Get(t.Context(), "typed-pod", metav1.GetOptions{})
1995+
require.NoError(t, err)
1996+
require.Equal(t, "typed-pod", pod.GetName())
1997+
1998+
// Get the unstructured configmap
1999+
configMap, err := client.Resource(configMapGVR).Namespace("default").Get(t.Context(), "unstructured-config", metav1.GetOptions{})
2000+
require.NoError(t, err)
2001+
require.Equal(t, "unstructured-config", configMap.GetName())
2002+
}
2003+
18442004
func TestNewClientWithOptions(t *testing.T) {
18452005
scheme := setupScheme()
18462006

0 commit comments

Comments
 (0)