|
1 | 1 | # Fake Dynamic Client |
2 | 2 |
|
3 | | -This package provides enhanced fake dynamic clients for testing Kubernetes controllers with Custom Resource Definitions (CRDs). |
| 3 | +Enhanced fake dynamic client for testing Kubernetes controllers with Custom Resource Definitions (CRDs). |
4 | 4 |
|
5 | 5 | ## Features |
6 | 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 |
| 7 | +- **Server-Side Apply**: Full field management and strategic merge patch support |
| 8 | +- **CRD Support**: Automatic registration and schema validation |
| 9 | +- **Multi-Version CRDs**: Support for CRDs with multiple API versions |
| 10 | +- **Embedded CRDs**: Load CRDs from `go:embed` byte data |
| 11 | +- **Flexible API**: Functional options for easy configuration |
12 | 12 |
|
13 | 13 | ## Quick Start |
14 | 14 |
|
15 | 15 | ### Basic Usage |
16 | 16 |
|
17 | 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" |
| 18 | +import "github.com/authzed/controller-idioms/client/fake" |
| 19 | + |
| 20 | +// Basic client |
| 21 | +client := fake.NewClient(scheme) |
| 22 | + |
| 23 | +// With CRDs and initial objects |
| 24 | +client := fake.NewClient(scheme, |
| 25 | + fake.WithCRDs(crd1, crd2), |
| 26 | + fake.WithObjects(existingObject), |
22 | 27 | ) |
23 | 28 |
|
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 | | -} |
| 29 | +// With embedded CRD files |
| 30 | +client := fake.NewClient(scheme, |
| 31 | + fake.WithCRDBytes(embeddedCRDs1, embeddedCRDs2), |
| 32 | +) |
34 | 33 | ``` |
35 | 34 |
|
36 | | -### Using Embedded CRDs |
37 | | - |
38 | | -The most convenient way to use CRDs in tests is with `go:embed`: |
| 35 | +### Using go:embed |
39 | 36 |
|
40 | 37 | ```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 | 38 | //go:embed testdata/my-crds.yaml |
56 | 39 | var embeddedCRDs []byte |
57 | 40 |
|
58 | | -func TestWithEmbeddedCRDs(t *testing.T) { |
59 | | - scheme := runtime.NewScheme() |
60 | | - |
61 | | - // Create client with embedded CRDs |
62 | | - client := fake.NewFakeDynamicClientWithCRDBytes(scheme, embeddedCRDs) |
| 41 | +func TestMyController(t *testing.T) { |
| 42 | + client := fake.NewClient(scheme, fake.WithCRDBytes(embeddedCRDs)) |
63 | 43 |
|
64 | | - // Create a custom resource |
| 44 | + // Create custom resources |
65 | 45 | myResource := &unstructured.Unstructured{ |
66 | 46 | Object: map[string]interface{}{ |
67 | 47 | "apiVersion": "example.com/v1", |
68 | 48 | "kind": "MyResource", |
69 | | - "metadata": map[string]interface{}{ |
70 | | - "name": "test-resource", |
71 | | - "namespace": "default", |
72 | | - }, |
73 | | - "spec": map[string]interface{}{ |
74 | | - "replicas": 3, |
75 | | - }, |
| 49 | + "metadata": map[string]interface{}{"name": "test"}, |
| 50 | + "spec": map[string]interface{}{"replicas": 3}, |
76 | 51 | }, |
77 | 52 | } |
78 | 53 |
|
79 | 54 | gvr := schema.GroupVersionResource{ |
80 | | - Group: "example.com", |
81 | | - Version: "v1", |
82 | | - Resource: "myresources", |
| 55 | + Group: "example.com", Version: "v1", Resource: "myresources", |
83 | 56 | } |
84 | 57 |
|
85 | | - // Test CRUD operations |
86 | 58 | created, err := client.Resource(gvr).Namespace("default").Create( |
87 | 59 | context.TODO(), myResource, metav1.CreateOptions{}, |
88 | 60 | ) |
89 | | - // ... rest of test |
| 61 | + // Test your controller logic... |
90 | 62 | } |
91 | 63 | ``` |
92 | 64 |
|
93 | | -### Multiple CRD Files |
| 65 | +## API Reference |
94 | 66 |
|
95 | | -You can combine multiple embedded files: |
| 67 | +### NewClient (Recommended) |
96 | 68 |
|
97 | 69 | ```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 | | -} |
| 70 | +func NewClient(scheme *runtime.Scheme, opts ...ClientOption) dynamic.Interface |
114 | 71 | ``` |
115 | 72 |
|
116 | | -### Multi-Version CRDs |
| 73 | +Main constructor with functional options: |
117 | 74 |
|
118 | | -The fake client fully supports CRDs with multiple versions: |
| 75 | +- `WithCRDs(crds...)` - Add CRD objects |
| 76 | +- `WithCRDBytes(data...)` - Add CRDs from YAML/JSON bytes |
| 77 | +- `WithCustomGVRMappings(mappings)` - Custom GVR to ListKind mappings |
| 78 | +- `WithOpenAPISpec(path)` - Custom OpenAPI spec file |
| 79 | +- `WithObjects(objects...)` - Initial objects in the client |
119 | 80 |
|
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 | | -``` |
| 81 | +### Other Constructors |
| 82 | + |
| 83 | +These constructors are provided to mirror the upstream dynamic client interface, |
| 84 | +but they are less flexible than `NewClient`: |
161 | 85 |
|
162 | 86 | ```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 | | -} |
| 87 | +func NewFakeDynamicClient(scheme *runtime.Scheme, objects ...runtime.Object) dynamic.Interface |
| 88 | +func NewFakeDynamicClientWithCustomListKinds(scheme *runtime.Scheme, gvrToListKind map[schema.GroupVersionResource]string, objects ...runtime.Object) dynamic.Interface |
179 | 89 | ``` |
180 | 90 |
|
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: |
| 91 | +## CRD Format |
206 | 92 |
|
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: |
| 93 | +Supports YAML and JSON, single or multi-document: |
212 | 94 |
|
213 | 95 | ```yaml |
214 | 96 | --- |
215 | 97 | apiVersion: apiextensions.k8s.io/v1 |
216 | 98 | kind: CustomResourceDefinition |
217 | 99 | metadata: |
218 | 100 | name: widgets.example.com |
219 | | -# ... widget CRD spec |
| 101 | +spec: |
| 102 | + group: example.com |
| 103 | + names: |
| 104 | + kind: Widget |
| 105 | + plural: widgets |
| 106 | + # ... rest of CRD spec |
220 | 107 | --- |
221 | 108 | apiVersion: apiextensions.k8s.io/v1 |
222 | 109 | kind: CustomResourceDefinition |
223 | 110 | metadata: |
224 | 111 | name: gadgets.example.com |
225 | | -# ... gadget CRD spec |
| 112 | +# ... second CRD |
226 | 113 | ``` |
227 | 114 |
|
228 | | -## Error Handling |
| 115 | +## Examples |
229 | 116 |
|
230 | | -All constructors panic on errors since they're designed for test usage. Common issues: |
| 117 | +### Multiple CRD Sources |
231 | 118 |
|
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 |
| 119 | +```go |
| 120 | +client := fake.NewClient(scheme, |
| 121 | + fake.WithCRDs(parsedCRD), // From CRD objects |
| 122 | + fake.WithCRDBytes(widgetCRDs, gadgetCRDs), // From embedded bytes |
| 123 | + fake.WithObjects(existingResources...), // Pre-existing objects |
| 124 | +) |
| 125 | +``` |
245 | 126 |
|
246 | | -The fake client supports server-side apply with proper field management: |
| 127 | +### Server-Side Apply |
247 | 128 |
|
248 | 129 | ```go |
249 | | -// Apply with field manager |
250 | 130 | applied, err := client.Resource(gvr).Namespace("default").Apply( |
251 | | - context.TODO(), "resource-name", resource, |
| 131 | + context.TODO(), "resource-name", resource, |
252 | 132 | metav1.ApplyOptions{ |
253 | 133 | FieldManager: "my-controller", |
254 | | - Force: true, // Use when conflicts are expected |
| 134 | + Force: true, |
255 | 135 | }, |
256 | 136 | ) |
257 | 137 | ``` |
258 | 138 |
|
259 | | -The fake client will track field ownership and handle strategic merge patches according to the CRD's OpenAPI schema. |
| 139 | +### Multi-Version CRDs |
| 140 | + |
| 141 | +```go |
| 142 | +// Different versions of the same resource |
| 143 | +v1alpha1GVR := schema.GroupVersionResource{Group: "example.com", Version: "v1alpha1", Resource: "apps"} |
| 144 | +v1GVR := schema.GroupVersionResource{Group: "example.com", Version: "v1", Resource: "apps"} |
| 145 | + |
| 146 | +// Both work independently |
| 147 | +client.Resource(v1alpha1GVR).Create(...) |
| 148 | +client.Resource(v1GVR).Create(...) |
| 149 | +``` |
0 commit comments