Skip to content

Commit 7c7fe9e

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

File tree

3 files changed

+323
-8
lines changed

3 files changed

+323
-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: 82 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ import (
2727
"testing"
2828
"time"
2929

30+
"k8s.io/apimachinery/pkg/api/resource"
3031
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
32+
"k8s.io/apimachinery/pkg/conversion"
3133
"k8s.io/apimachinery/pkg/util/json"
3234
"k8s.io/apimachinery/pkg/util/yaml"
3335

@@ -39,6 +41,10 @@ import (
3941
)
4042

4143
var rules interpreter.Rules = interpreter.AllResourceInterpreterCustomizationRules
44+
var checker = conversion.EqualitiesOrDie(
45+
func(a, b resource.Quantity) bool {
46+
return a.Equal(b)
47+
})
4248

4349
func checkScript(script string) error {
4450
ctx, cancel := context.WithTimeout(context.TODO(), time.Second)
@@ -102,11 +108,11 @@ type TestStructure struct {
102108
}
103109

104110
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"`
111+
DesiredInputPath string `yaml:"desiredInputPath,omitempty"`
112+
ObservedInputPath string `yaml:"observedInputPath,omitempty"`
113+
StatusInputPath string `yaml:"statusInputPath,omitempty"`
114+
DesiredResults map[string]interface{} `yaml:"desiredResults,omitempty"`
115+
Operation string `yaml:"operation"`
110116
}
111117

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

0 commit comments

Comments
 (0)