Skip to content

Commit 7830124

Browse files
committed
enhance resource interpreter test framework
Signed-off-by: zhzhuang-zju <[email protected]>
1 parent 152433f commit 7830124

File tree

3 files changed

+321
-8
lines changed

3 files changed

+321
-8
lines changed
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Thirdparty Resource Interpreter
2+
3+
This directory contains third-party resource interpreters for Karmada. These interpreters define how Karmada should handle custom resources from various third-party applications and operators.
4+
5+
## Files
6+
7+
- `thirdparty.go` - Main implementation of the third-party resource interpreter
8+
- `thirdparty_test.go` - Test suite for validating resource interpreter customizations
9+
- `resourcecustomizations/` - Directory containing resource customization definitions organized by API version and kind
10+
11+
## Directory Structure
12+
13+
The resource customizations are organized in the following structure:
14+
15+
```
16+
resourcecustomizations/
17+
├── <group>/
18+
│ └── <version>/
19+
│ └── <kind>/
20+
│ ├── customizations.yaml # Resource interpreter customization rules
21+
│ ├── customizations_tests.yaml # Test cases for the customizations
22+
│ └── testdata/ # Test input and expected output files
23+
│ ├── desired_xxx.yaml # Input resource for desired state
24+
│ ├── observed_xxx.yaml # Input resource for observed state
25+
│ ├── status_xxx.yaml # Input aggregated status items
26+
```
27+
28+
## How to test
29+
30+
### Running Tests
31+
32+
To run all third-party resource interpreter tests:
33+
34+
```bash
35+
cd pkg/resourceinterpreter/default/thirdparty
36+
go test -v
37+
```
38+
39+
### Creating Test Cases
40+
41+
#### 1. Create Test Structure
42+
43+
For a new resource type, create the directory structure:
44+
45+
```bash
46+
mkdir -p resourcecustomizations/<group>/<version>/<kind>/testdata
47+
```
48+
49+
#### 2. Create Test Data Files
50+
51+
Test data files are generally divided into three categories:
52+
53+
- `desired_xxx.yaml`: Resource definitions deployed on the control plane
54+
- `observed_xxx.yaml`: Resource definitions observed in a member cluster
55+
- `status_xxx.yaml`: Status information of the resource on each member cluster, with structure `[]workv1alpha2.AggregatedStatusItem`
56+
57+
Multiple test data files can be created for each category as needed. Pay attention to naming distinctions, as they will ultimately be referenced in `customizations_tests.yaml`.
58+
59+
#### 3. Create Test Configuration
60+
61+
Create `customizations_tests.yaml` to define test cases:
62+
63+
```yaml
64+
tests:
65+
- observedInputPath: testdata/observed-flinkdeployment.yaml
66+
operation: InterpretReplica
67+
desiredResults:
68+
replica: 2
69+
```
70+
71+
Where:
72+
- `operation` specifies the operation of resource interpreter
73+
- `desiredResults` defines the expected output, which is a key-value mapping where the key is the field name of the expected result and the value is the expected result
74+
75+
The keys in `desiredResults` for different operations correspond to the Name field of the results returned by the corresponding resource interpreter operation `RuleResult.Results`.
76+
77+
For example:
78+
```go
79+
func (h *healthInterpretationRule) Run(interpreter *declarative.ConfigurableInterpreter, args RuleArgs) *RuleResult {
80+
obj, err := args.getObjectOrError()
81+
if err != nil {
82+
return newRuleResultWithError(err)
83+
}
84+
healthy, enabled, err := interpreter.InterpretHealth(obj)
85+
if err != nil {
86+
return newRuleResultWithError(err)
87+
}
88+
if !enabled {
89+
return newRuleResultWithError(fmt.Errorf("rule is not enabled"))
90+
}
91+
return newRuleResult().add("healthy", healthy)
92+
}
93+
```
94+
95+
The `desiredResults` for operation `InterpretHealth` should contain the `healthy` key.
96+
97+
### Supported Operations
98+
99+
The test framework supports the following operations:
100+
101+
- `InterpretReplica` - Extract replica count from resource
102+
- `InterpretComponent` - Extract component information from resource
103+
- `ReviseReplica` - Modify replica count in resource
104+
- `InterpretStatus` - Extract status information
105+
- `InterpretHealth` - Determine resource health status
106+
- `InterpretDependency` - Extract resource dependencies
107+
- `AggregateStatus` - Aggregate status from multiple clusters
108+
- `Retain` - Retain the desired resource template.
109+
110+
### Test Validation
111+
112+
The test framework validates:
113+
114+
1. **Lua Script Syntax** - Ensures all Lua scripts are syntactically correct
115+
2. **Execution Results** - Compares actual results with expected results
116+
117+
### Debugging Tests
118+
119+
To debug failing tests:
120+
121+
1. **Check Lua Script Syntax** - Ensure your Lua scripts are valid
122+
2. **Verify Test Data** - Confirm test input files are properly formatted
123+
3. **Review Expected Results** - Make sure expected results match the actual operation output
124+
4. **Use Verbose Output** - Run tests with `-v` flag for detailed output
125+
126+
### Best Practices
127+
128+
1. **Comprehensive Coverage** - Test all supported operations for your resource type
129+
2. **Edge Cases** - Include tests for edge cases and error conditions
130+
3. **Realistic Data** - Use realistic resource definitions in test data
131+
4. **Clear Naming** - Use descriptive names for test files and cases
132+
133+
For more information about resource interpreter customizations, see the [Karmada documentation](https://karmada.io/docs/userguide/globalview/customizing-resource-interpreter/).

pkg/resourceinterpreter/default/thirdparty/resourcecustomizations/flink.apache.org/v1beta1/FlinkDeployment/customizations_tests.yaml

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,116 @@ tests:
22
- desiredInputPath: testdata/desired-flinkdeployment.yaml
33
statusInputPath: testdata/status-file.yaml
44
operation: AggregateStatus
5-
- observedInputPath: testdata/observed-flinkdeployment.yaml
5+
desiredResults:
6+
aggregatedStatus:
7+
apiVersion: flink.apache.org/v1beta1
8+
kind: FlinkDeployment
9+
metadata:
10+
name: basic-example
11+
namespace: test-namespace
12+
spec:
13+
flinkConfiguration:
14+
taskmanager.numberOfTaskSlots: "2"
15+
flinkVersion: v1_17
16+
image: flink:1.17
17+
job:
18+
jarURI: local:///opt/flink/examples/streaming/StateMachineExample.jar
19+
parallelism: 2
20+
upgradeMode: stateless
21+
jobManager:
22+
replicas: 1
23+
resource:
24+
cpu: 1
25+
memory: 2048m
26+
mode: native
27+
serviceAccount: flink
28+
taskManager:
29+
resource:
30+
cpu: 1
31+
memory: 2048m
32+
status:
33+
clusterInfo:
34+
flink-revision: 2750d5c @ 2023-05-19T10:45:46+02:00
35+
flink-version: 1.17.1
36+
total-cpu: "2.0"
37+
total-memory: "4294967296"
38+
jobManagerDeploymentStatus: READY
39+
jobStatus:
40+
checkpointInfo:
41+
lastPeriodicCheckpointTimestamp: 0
42+
jobId: 44cc5573945d1d4925732d915c70b9ac
43+
jobName: Minimal Spec Example
44+
savepointInfo:
45+
lastPeriodicSavepointTimestamp: 0
46+
startTime: "1717599166365"
47+
state: RUNNING
48+
updateTime: "1717599182544"
49+
lifecycleState: STABLE
50+
observedGeneration: 1
51+
reconciliationStatus:
52+
lastReconciledSpec: '{"spec":{"job":{"jarURI":"local:///opt/flink/examples/streaming/StateMachineExample.jar","parallelism":2,"entryClass":null,"args":[],"state":"running","savepointTriggerNonce":null,"initialSavepointPath":null,"checkpointTriggerNonce":null,"upgradeMode":"stateless","allowNonRestoredState":null,"savepointRedeployNonce":null},"restartNonce":null,"flinkConfiguration":{"taskmanager.numberOfTaskSlots":"2"},"image":"flink:1.17","imagePullPolicy":null,"serviceAccount":"flink","flinkVersion":"v1_17","ingress":null,"podTemplate":null,"jobManager":{"resource":{"cpu":1.0,"memory":"2048m","ephemeralStorage":null},"replicas":1,"podTemplate":null},"taskManager":{"resource":{"cpu":1.0,"memory":"2048m","ephemeralStorage":null},"replicas":null,"podTemplate":null},"logConfiguration":null,"mode":null},"resource_metadata":{"apiVersion":"flink.apache.org/v1beta1","metadata":{"generation":2},"firstDeployment":true}}'
53+
lastStableSpec: '{"spec":{"job":{"jarURI":"local:///opt/flink/examples/streaming/StateMachineExample.jar","parallelism":2,"entryClass":null,"args":[],"state":"running","savepointTriggerNonce":null,"initialSavepointPath":null,"checkpointTriggerNonce":null,"upgradeMode":"stateless","allowNonRestoredState":null,"savepointRedeployNonce":null},"restartNonce":null,"flinkConfiguration":{"taskmanager.numberOfTaskSlots":"2"},"image":"flink:1.17","imagePullPolicy":null,"serviceAccount":"flink","flinkVersion":"v1_17","ingress":null,"podTemplate":null,"jobManager":{"resource":{"cpu":1.0,"memory":"2048m","ephemeralStorage":null},"replicas":1,"podTemplate":null},"taskManager":{"resource":{"cpu":1.0,"memory":"2048m","ephemeralStorage":null},"replicas":null,"podTemplate":null},"logConfiguration":null,"mode":null},"resource_metadata":{"apiVersion":"flink.apache.org/v1beta1","metadata":{"generation":2},"firstDeployment":true}}'
54+
reconciliationTimestamp: 1717599148930
55+
state: DEPLOYED
56+
taskManager:
57+
labelSelector: component=taskmanager,app=basic-example
58+
replicas: 1
59+
- observedInputPath: testdata/desired-flinkdeployment.yaml
660
operation: InterpretReplica
61+
desiredResults:
62+
replica: 2
63+
requires:
64+
resourceRequest:
65+
"cpu": "1"
66+
"memory": "2048m"
67+
namespace: "test-namespace"
68+
- observedInputPath: testdata/desired-flinkdeployment.yaml
69+
operation: InterpretComponent
70+
desiredResults:
71+
components:
72+
- name: jobmanager
73+
replicas: 1
74+
replicaRequirements:
75+
resourceRequest:
76+
"cpu": "1"
77+
"memory": "2048m"
78+
- name: taskmanager
79+
replicas: 1
80+
replicaRequirements:
81+
resourceRequest:
82+
"cpu": "1"
83+
"memory": "2048m"
784
- observedInputPath: testdata/observed-flinkdeployment.yaml
885
operation: InterpretHealth
86+
desiredResults:
87+
healthy: true
988
- observedInputPath: testdata/observed-flinkdeployment.yaml
1089
operation: InterpretStatus
90+
desiredResults:
91+
status:
92+
clusterInfo:
93+
flink-revision: 2750d5c @ 2023-05-19T10:45:46+02:00
94+
flink-version: 1.17.1
95+
total-cpu: "2.0"
96+
total-memory: "4294967296"
97+
jobManagerDeploymentStatus: READY
98+
jobStatus:
99+
checkpointInfo:
100+
lastPeriodicCheckpointTimestamp: 0
101+
jobId: 44cc5573945d1d4925732d915c70b9ac
102+
jobName: Minimal Spec Example
103+
savepointInfo:
104+
lastPeriodicSavepointTimestamp: 0
105+
startTime: "1717599166365"
106+
state: RUNNING
107+
updateTime: "1717599182544"
108+
lifecycleState: STABLE
109+
observedGeneration: 1
110+
reconciliationStatus:
111+
lastReconciledSpec: '{"spec":{"job":{"jarURI":"local:///opt/flink/examples/streaming/StateMachineExample.jar","parallelism":2,"entryClass":null,"args":[],"state":"running","savepointTriggerNonce":null,"initialSavepointPath":null,"checkpointTriggerNonce":null,"upgradeMode":"stateless","allowNonRestoredState":null,"savepointRedeployNonce":null},"restartNonce":null,"flinkConfiguration":{"taskmanager.numberOfTaskSlots":"2"},"image":"flink:1.17","imagePullPolicy":null,"serviceAccount":"flink","flinkVersion":"v1_17","ingress":null,"podTemplate":null,"jobManager":{"resource":{"cpu":1.0,"memory":"2048m","ephemeralStorage":null},"replicas":1,"podTemplate":null},"taskManager":{"resource":{"cpu":1.0,"memory":"2048m","ephemeralStorage":null},"replicas":null,"podTemplate":null},"logConfiguration":null,"mode":null},"resource_metadata":{"apiVersion":"flink.apache.org/v1beta1","metadata":{"generation":2},"firstDeployment":true}}'
112+
lastStableSpec: '{"spec":{"job":{"jarURI":"local:///opt/flink/examples/streaming/StateMachineExample.jar","parallelism":2,"entryClass":null,"args":[],"state":"running","savepointTriggerNonce":null,"initialSavepointPath":null,"checkpointTriggerNonce":null,"upgradeMode":"stateless","allowNonRestoredState":null,"savepointRedeployNonce":null},"restartNonce":null,"flinkConfiguration":{"taskmanager.numberOfTaskSlots":"2"},"image":"flink:1.17","imagePullPolicy":null,"serviceAccount":"flink","flinkVersion":"v1_17","ingress":null,"podTemplate":null,"jobManager":{"resource":{"cpu":1.0,"memory":"2048m","ephemeralStorage":null},"replicas":1,"podTemplate":null},"taskManager":{"resource":{"cpu":1.0,"memory":"2048m","ephemeralStorage":null},"replicas":null,"podTemplate":null},"logConfiguration":null,"mode":null},"resource_metadata":{"apiVersion":"flink.apache.org/v1beta1","metadata":{"generation":2},"firstDeployment":true}}'
113+
reconciliationTimestamp: 1717599148930
114+
state: DEPLOYED
115+
taskManager:
116+
labelSelector: component=taskmanager,app=basic-example
117+
replicas: 1

pkg/resourceinterpreter/default/thirdparty/thirdparty_test.go

Lines changed: 80 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,13 @@ import (
2323
"io"
2424
"os"
2525
"path/filepath"
26+
"reflect"
2627
"strings"
2728
"testing"
2829
"time"
2930

31+
"k8s.io/apimachinery/pkg/api/equality"
32+
"k8s.io/apimachinery/pkg/api/resource"
3033
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3134
"k8s.io/apimachinery/pkg/util/json"
3235
"k8s.io/apimachinery/pkg/util/yaml"
@@ -39,6 +42,7 @@ import (
3942
)
4043

4144
var rules interpreter.Rules = interpreter.AllResourceInterpreterCustomizationRules
45+
var checker = equality.Semantic.Copy()
4246

4347
func checkScript(script string) error {
4448
ctx, cancel := context.WithTimeout(context.TODO(), time.Second)
@@ -102,11 +106,11 @@ type TestStructure struct {
102106
}
103107

104108
type IndividualTest struct {
105-
DesiredInputPath string `yaml:"desiredInputPath,omitempty"`
106-
ObservedInputPath string `yaml:"observedInputPath,omitempty"`
107-
StatusInputPath string `yaml:"statusInputPath,omitempty"`
108-
DesiredReplica int64 `yaml:"desiredReplicas,omitempty"`
109-
Operation string `yaml:"operation"`
109+
DesiredInputPath string `yaml:"desiredInputPath,omitempty"`
110+
ObservedInputPath string `yaml:"observedInputPath,omitempty"`
111+
StatusInputPath string `yaml:"statusInputPath,omitempty"`
112+
DesiredResults map[string]interface{} `yaml:"desiredResults,omitempty"`
113+
Operation string `yaml:"operation"`
110114
}
111115

112116
func checkInterpretationRule(t *testing.T, path string, configs []*configv1alpha1.ResourceInterpreterCustomization) {
@@ -133,7 +137,7 @@ func checkInterpretationRule(t *testing.T, path string, configs []*configv1alpha
133137
if err != nil {
134138
t.Fatalf("checking %s of %s, expected nil, but got: %v", rule.Name(), customization.Name, err)
135139
}
136-
args := interpreter.RuleArgs{Replica: input.DesiredReplica}
140+
args := interpreter.RuleArgs{}
137141
if input.DesiredInputPath != "" {
138142
args.Desired = getObj(t, dir+"/"+strings.TrimPrefix(input.DesiredInputPath, "/"))
139143
}
@@ -143,10 +147,79 @@ func checkInterpretationRule(t *testing.T, path string, configs []*configv1alpha
143147
if input.StatusInputPath != "" {
144148
args.Status = getAggregatedStatusItems(t, dir+"/"+strings.TrimPrefix(input.StatusInputPath, "/"))
145149
}
146-
if result := rule.Run(ipt, args); result.Err != nil {
150+
result := rule.Run(ipt, args)
151+
if result.Err != nil {
147152
t.Fatalf("execute %s %s error: %v\n", customization.Name, rule.Name(), result.Err)
148153
}
154+
for _, res := range result.Results {
155+
expected, ok := input.DesiredResults[res.Name]
156+
if !ok {
157+
t.Logf("no expected result for %s", res.Name)
158+
continue
159+
}
160+
161+
if equal, err := deepEqual(expected, res.Value); err != nil || !equal {
162+
t.Fatal("unexpected result for", res.Name, "expected:", expected, "got:", res.Value, "error:", err)
163+
}
164+
}
165+
}
166+
}
167+
}
168+
169+
func deepEqual(expected, actualValue interface{}) (bool, error) {
170+
err := checker.AddFuncs(
171+
func(a, b resource.Quantity) bool {
172+
return a.Equal(b)
173+
})
174+
if err != nil {
175+
return false, fmt.Errorf("failed to add custom equality function: %w", err)
176+
}
177+
178+
expectedJSONBytes, err := json.Marshal(expected)
179+
if err != nil {
180+
return false, fmt.Errorf("failed to marshal expected value: %w", err)
181+
}
182+
183+
// Handle known types for semantic comparison
184+
switch typedActual := actualValue.(type) {
185+
case *workv1alpha2.ReplicaRequirements:
186+
var unmarshaledExpected workv1alpha2.ReplicaRequirements
187+
if err := json.Unmarshal(expectedJSONBytes, &unmarshaledExpected); err != nil {
188+
return false, fmt.Errorf("failed to unmarshal expected JSON into ReplicaRequirements: %w", err)
189+
}
190+
return checker.DeepEqual(&unmarshaledExpected, typedActual), nil
191+
192+
case *configv1alpha1.DependentObjectReference:
193+
var unmarshaledExpected configv1alpha1.DependentObjectReference
194+
if err := json.Unmarshal(expectedJSONBytes, &unmarshaledExpected); err != nil {
195+
return false, fmt.Errorf("failed to unmarshal expected JSON into DependentObjectReference: %w", err)
196+
}
197+
return reflect.DeepEqual(&unmarshaledExpected, typedActual), nil
198+
199+
case []workv1alpha2.Component:
200+
var unmarshaledExpected []workv1alpha2.Component
201+
if err := json.Unmarshal(expectedJSONBytes, &unmarshaledExpected); err != nil {
202+
return false, fmt.Errorf("failed to unmarshal expected JSON into []Component: %w", err)
203+
}
204+
205+
return checker.DeepEqual(unmarshaledExpected, typedActual), nil
206+
207+
case *unstructured.Unstructured:
208+
var unmarshaledExpected unstructured.Unstructured
209+
210+
if err := json.Unmarshal(expectedJSONBytes, &unmarshaledExpected); err != nil {
211+
fmt.Println(err)
212+
return false, fmt.Errorf("failed to unmarshal expected JSON into Unstructured: %w", err)
213+
}
214+
return reflect.DeepEqual(&unmarshaledExpected, typedActual), nil
215+
216+
default:
217+
// Fallback: marshal actualValue and do byte-wise comparison
218+
actualJSON, err := json.Marshal(actualValue)
219+
if err != nil {
220+
return false, fmt.Errorf("failed to marshal actual value: %w", err)
149221
}
222+
return bytes.Equal(expectedJSONBytes, actualJSON), nil
150223
}
151224
}
152225

0 commit comments

Comments
 (0)