Skip to content

Commit f1b1c96

Browse files
committed
add new dynamic fake
this mimics the upstream dynamic fake but supports server-side apply semantics
1 parent b98da97 commit f1b1c96

File tree

9 files changed

+96307
-181
lines changed

9 files changed

+96307
-181
lines changed

client/fake/README.md

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
# Fake Dynamic Client
2+
3+
This package provides enhanced fake dynamic clients for testing Kubernetes controllers with Custom Resource Definitions (CRDs).
4+
5+
## Features
6+
7+
- **CRD Support**: Automatic registration of CRD types with proper schema validation
8+
- **Multi-Version CRDs**: Full support for CRDs with multiple API versions
9+
- **Server-Side Apply**: Proper field management and strategic merge patch support
10+
- **Embedded CRDs**: Load CRDs from embedded byte data using `go:embed`
11+
- **OpenAPI Integration**: Custom OpenAPI spec support for different Kubernetes versions
12+
13+
## Quick Start
14+
15+
### Basic Usage
16+
17+
```go
18+
import (
19+
"github.com/authzed/controller-idioms/client/fake"
20+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
21+
"k8s.io/apimachinery/pkg/runtime"
22+
)
23+
24+
func TestMyController(t *testing.T) {
25+
scheme := runtime.NewScheme()
26+
27+
// Create a fake client with CRDs
28+
client := fake.NewFakeDynamicClientWithCRDs(scheme, []*apiextensionsv1.CustomResourceDefinition{
29+
myCRD,
30+
})
31+
32+
// Use the client in your tests...
33+
}
34+
```
35+
36+
### Using Embedded CRDs
37+
38+
The most convenient way to use CRDs in tests is with `go:embed`:
39+
40+
```go
41+
package mycontroller_test
42+
43+
import (
44+
"context"
45+
_ "embed"
46+
"testing"
47+
48+
"github.com/authzed/controller-idioms/client/fake"
49+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
50+
"k8s.io/apimachinery/pkg/runtime"
51+
"k8s.io/apimachinery/pkg/runtime/schema"
52+
)
53+
54+
// Embed your CRDs directly into the test binary
55+
//go:embed testdata/my-crds.yaml
56+
var embeddedCRDs []byte
57+
58+
func TestWithEmbeddedCRDs(t *testing.T) {
59+
scheme := runtime.NewScheme()
60+
61+
// Create client with embedded CRDs
62+
client := fake.NewFakeDynamicClientWithCRDBytes(scheme, embeddedCRDs)
63+
64+
// Create a custom resource
65+
myResource := &unstructured.Unstructured{
66+
Object: map[string]interface{}{
67+
"apiVersion": "example.com/v1",
68+
"kind": "MyResource",
69+
"metadata": map[string]interface{}{
70+
"name": "test-resource",
71+
"namespace": "default",
72+
},
73+
"spec": map[string]interface{}{
74+
"replicas": 3,
75+
},
76+
},
77+
}
78+
79+
gvr := schema.GroupVersionResource{
80+
Group: "example.com",
81+
Version: "v1",
82+
Resource: "myresources",
83+
}
84+
85+
// Test CRUD operations
86+
created, err := client.Resource(gvr).Namespace("default").Create(
87+
context.TODO(), myResource, metav1.CreateOptions{},
88+
)
89+
// ... rest of test
90+
}
91+
```
92+
93+
### Multiple CRD Files
94+
95+
You can combine multiple embedded files:
96+
97+
```go
98+
//go:embed testdata/widgets.yaml
99+
var widgetCRD []byte
100+
101+
//go:embed testdata/gadgets.yaml
102+
var gadgetCRD []byte
103+
104+
func TestMultipleCRDs(t *testing.T) {
105+
// Combine multiple CRD files
106+
combinedCRDs := append(widgetCRD, []byte("\n---\n")...)
107+
combinedCRDs = append(combinedCRDs, gadgetCRD...)
108+
109+
scheme := runtime.NewScheme()
110+
client := fake.NewFakeDynamicClientWithCRDBytes(scheme, combinedCRDs)
111+
112+
// Both CRDs are now available...
113+
}
114+
```
115+
116+
### Multi-Version CRDs
117+
118+
The fake client fully supports CRDs with multiple versions:
119+
120+
```yaml
121+
# testdata/multi-version-crd.yaml
122+
apiVersion: apiextensions.k8s.io/v1
123+
kind: CustomResourceDefinition
124+
metadata:
125+
name: apps.example.com
126+
spec:
127+
group: example.com
128+
names:
129+
kind: App
130+
plural: apps
131+
singular: app
132+
scope: Namespaced
133+
versions:
134+
- name: v1alpha1
135+
served: true
136+
storage: false
137+
schema:
138+
openAPIV3Schema:
139+
type: object
140+
properties:
141+
spec:
142+
type: object
143+
properties:
144+
image:
145+
type: string
146+
- name: v1
147+
served: true
148+
storage: true
149+
schema:
150+
openAPIV3Schema:
151+
type: object
152+
properties:
153+
spec:
154+
type: object
155+
properties:
156+
image:
157+
type: string
158+
replicas:
159+
type: integer
160+
```
161+
162+
```go
163+
func TestMultiVersion(t *testing.T) {
164+
client := fake.NewFakeDynamicClientWithCRDBytes(scheme, embeddedCRD)
165+
166+
// Create resources using different API versions
167+
v1alpha1GVR := schema.GroupVersionResource{
168+
Group: "example.com", Version: "v1alpha1", Resource: "apps",
169+
}
170+
171+
v1GVR := schema.GroupVersionResource{
172+
Group: "example.com", Version: "v1", Resource: "apps",
173+
}
174+
175+
// Both versions work independently
176+
_, err := client.Resource(v1alpha1GVR).Create(/* ... */)
177+
_, err = client.Resource(v1GVR).Create(/* ... */)
178+
}
179+
```
180+
181+
## Available Constructors
182+
183+
### `NewFakeDynamicClient(scheme)`
184+
185+
Basic fake client without CRD support.
186+
187+
### `NewFakeDynamicClientWithCRDs(scheme, crds, objects...)`
188+
189+
Fake client with CRD support using parsed CRD objects.
190+
191+
### `NewFakeDynamicClientWithCRDBytes(scheme, crdData, objects...)`
192+
193+
Fake client with CRDs loaded from embedded byte data. Supports YAML and JSON, multiple documents separated by `---`.
194+
195+
### `NewFakeDynamicClientWithCRDBytesAndSpec(scheme, crdData, specPath, objects...)`
196+
197+
Like `NewFakeDynamicClientWithCRDBytes` but allows specifying a custom OpenAPI spec file.
198+
199+
### `NewFakeDynamicClientWithOpenAPISpec(scheme, specPath, crds, objects...)`
200+
201+
Fake client with custom OpenAPI spec support for testing against different Kubernetes versions.
202+
203+
## CRD File Format
204+
205+
The `crdData` parameter accepts:
206+
207+
- Single CRD in YAML or JSON format
208+
- Multiple CRDs separated by `---` (YAML multi-document format)
209+
- Mixed documents (non-CRD documents are ignored)
210+
211+
Example multi-document format:
212+
213+
```yaml
214+
---
215+
apiVersion: apiextensions.k8s.io/v1
216+
kind: CustomResourceDefinition
217+
metadata:
218+
name: widgets.example.com
219+
# ... widget CRD spec
220+
---
221+
apiVersion: apiextensions.k8s.io/v1
222+
kind: CustomResourceDefinition
223+
metadata:
224+
name: gadgets.example.com
225+
# ... gadget CRD spec
226+
```
227+
228+
## Error Handling
229+
230+
All constructors panic on errors since they're designed for test usage. Common issues:
231+
232+
- **Invalid YAML/JSON**: Ensure your embedded files are valid
233+
- **Missing schema**: CRDs without OpenAPI schemas are supported but won't have validation
234+
- **Invalid CRD**: Non-CRD documents in the input are silently ignored
235+
236+
## Best Practices
237+
238+
1. **Use `go:embed`**: Embed CRDs directly in your test files for better maintainability
239+
2. **Version your CRDs**: Test against multiple API versions if your controller supports them
240+
3. **Separate test data**: Keep CRD files in a `testdata/` directory
241+
4. **Validate schemas**: Include proper OpenAPI v3 schemas in your CRDs for realistic testing
242+
5. **Test field management**: Use server-side apply to test field management behavior
243+
244+
## Server-Side Apply Support
245+
246+
The fake client supports server-side apply with proper field management:
247+
248+
```go
249+
// Apply with field manager
250+
applied, err := client.Resource(gvr).Namespace("default").Apply(
251+
context.TODO(), "resource-name", resource,
252+
metav1.ApplyOptions{
253+
FieldManager: "my-controller",
254+
Force: true, // Use when conflicts are expected
255+
},
256+
)
257+
```
258+
259+
The fake client will track field ownership and handle strategic merge patches according to the CRD's OpenAPI schema.

0 commit comments

Comments
 (0)