Skip to content

Commit 386c596

Browse files
committed
feat: add NewClient constructor with functional options
Also updates the (fake pkg) readme to be more concise
1 parent f1b1c96 commit 386c596

File tree

3 files changed

+736
-263
lines changed

3 files changed

+736
-263
lines changed

client/fake/README.md

Lines changed: 74 additions & 184 deletions
Original file line numberDiff line numberDiff line change
@@ -1,259 +1,149 @@
11
# Fake Dynamic Client
22

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).
44

55
## Features
66

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
1212

1313
## Quick Start
1414

1515
### Basic Usage
1616

1717
```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),
2227
)
2328

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+
)
3433
```
3534

36-
### Using Embedded CRDs
37-
38-
The most convenient way to use CRDs in tests is with `go:embed`:
35+
### Using go:embed
3936

4037
```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
5538
//go:embed testdata/my-crds.yaml
5639
var embeddedCRDs []byte
5740

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))
6343

64-
// Create a custom resource
44+
// Create custom resources
6545
myResource := &unstructured.Unstructured{
6646
Object: map[string]interface{}{
6747
"apiVersion": "example.com/v1",
6848
"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},
7651
},
7752
}
7853

7954
gvr := schema.GroupVersionResource{
80-
Group: "example.com",
81-
Version: "v1",
82-
Resource: "myresources",
55+
Group: "example.com", Version: "v1", Resource: "myresources",
8356
}
8457

85-
// Test CRUD operations
8658
created, err := client.Resource(gvr).Namespace("default").Create(
8759
context.TODO(), myResource, metav1.CreateOptions{},
8860
)
89-
// ... rest of test
61+
// Test your controller logic...
9062
}
9163
```
9264

93-
### Multiple CRD Files
65+
## API Reference
9466

95-
You can combine multiple embedded files:
67+
### NewClient (Recommended)
9668

9769
```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
11471
```
11572

116-
### Multi-Version CRDs
73+
Main constructor with functional options:
11774

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
11980

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`:
16185

16286
```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
17989
```
18090

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
20692

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:
21294

21395
```yaml
21496
---
21597
apiVersion: apiextensions.k8s.io/v1
21698
kind: CustomResourceDefinition
21799
metadata:
218100
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
220107
---
221108
apiVersion: apiextensions.k8s.io/v1
222109
kind: CustomResourceDefinition
223110
metadata:
224111
name: gadgets.example.com
225-
# ... gadget CRD spec
112+
# ... second CRD
226113
```
227114

228-
## Error Handling
115+
## Examples
229116

230-
All constructors panic on errors since they're designed for test usage. Common issues:
117+
### Multiple CRD Sources
231118

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+
```
245126

246-
The fake client supports server-side apply with proper field management:
127+
### Server-Side Apply
247128

248129
```go
249-
// Apply with field manager
250130
applied, err := client.Resource(gvr).Namespace("default").Apply(
251-
context.TODO(), "resource-name", resource,
131+
context.TODO(), "resource-name", resource,
252132
metav1.ApplyOptions{
253133
FieldManager: "my-controller",
254-
Force: true, // Use when conflicts are expected
134+
Force: true,
255135
},
256136
)
257137
```
258138

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

Comments
 (0)