|
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