Skip to content

Commit f581a11

Browse files
authored
Merge pull request #3390 from alaypatel07/cl2-dependency
add ability to declare Dependency in tests
2 parents ba88f51 + 867a3ff commit f581a11

File tree

10 files changed

+540
-3
lines changed

10 files changed

+540
-3
lines changed

clusterloader2/api/types.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ type TestScenario struct {
4343
type Config struct {
4444
// Name of the test case.
4545
Name string `json:"name"`
46+
// Dependencies is a list of dependencies that must be set up before steps begin
47+
// and torn down after test completion.
48+
Dependencies []*Dependency `json:"dependencies,omitempty"`
4649
// TODO(#1696): Clean up after removing automanagedNamespaces
4750
AutomanagedNamespaces int32 `json:"automanagedNamespaces,omitempty"`
4851
// Namespace is a structure for namespace configuration.
@@ -222,6 +225,21 @@ type Measurement struct {
222225
Instances []*MeasurementInstanceConfig
223226
}
224227

228+
// Dependency defines the dependency which will call either install or teardown process for configuring cluster
229+
// dependencies.
230+
type Dependency struct {
231+
// Name is a human-readable name for this dependency instance.
232+
Name string `json:"name"`
233+
// Method is a name of a method registered in the ClusterLoader dependency factory.
234+
Method string `json:"method"`
235+
// Timeout is the maximum duration for both setup and teardown operations.
236+
// If set to 0, operations will wait forever.
237+
Timeout Duration `json:"timeout,omitempty"`
238+
// Params is a map of {name: value} pairs which will be passed to the dependency method - allowing for injection
239+
// of arbitrary parameters to it.
240+
Params map[string]interface{} `json:"params"`
241+
}
242+
225243
// QPSLoad starts one operation every 1/QPS seconds.
226244
type QPSLoad struct {
227245
// QPS specifies requested qps.

clusterloader2/api/validation.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ func (v *ConfigValidator) Validate() *errors.ErrorList {
5151

5252
allErrs = append(allErrs, v.validateNamespace(&c.Namespace, field.NewPath("namespace"))...)
5353

54+
for i := range c.Dependencies {
55+
allErrs = append(allErrs, v.validateDependency(c.Dependencies[i], field.NewPath("dependencies").Index(i))...)
56+
}
57+
5458
for i := range c.TuningSets {
5559
allErrs = append(allErrs, v.validateTuningSet(c.TuningSets[i], field.NewPath("tuningSets").Index(i))...)
5660
}
@@ -297,6 +301,20 @@ func (v *ConfigValidator) validatePoissonLoad(rl *PoissonLoad, fldPath *field.Pa
297301
return allErrs
298302
}
299303

304+
func (v *ConfigValidator) validateDependency(d *Dependency, fldPath *field.Path) field.ErrorList {
305+
allErrs := field.ErrorList{}
306+
if d.Name == "" {
307+
allErrs = append(allErrs, field.Required(fldPath.Child("name"), "name is required"))
308+
}
309+
if d.Method == "" {
310+
allErrs = append(allErrs, field.Required(fldPath.Child("method"), "method is required"))
311+
}
312+
if d.Timeout < 0 {
313+
allErrs = append(allErrs, field.Invalid(fldPath.Child("timeout"), d.Timeout, "timeout cannot be negative"))
314+
}
315+
return allErrs
316+
}
317+
300318
func (v *ConfigValidator) fileExists(path string) bool {
301319
cwd, _ := os.Getwd()
302320
_, err := os.Stat(fmt.Sprintf("%s/%s/%s", cwd, v.configDir, path))

clusterloader2/api/validation_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,84 @@ func TestVerifyMeasurement(t *testing.T) {
358358
}
359359
}
360360

361+
func TestVerifyDependency(t *testing.T) {
362+
for _, test := range []struct {
363+
name string
364+
input Dependency
365+
expected bool
366+
}{
367+
{
368+
name: "Valid dependency - name and method specified",
369+
input: Dependency{
370+
Name: "test-dependency",
371+
Method: "TestMethod",
372+
Timeout: Duration(300000000000), // 5 minutes in nanoseconds
373+
Params: map[string]interface{}{
374+
"param1": "value1",
375+
},
376+
},
377+
expected: true,
378+
},
379+
{
380+
name: "Valid dependency - zero timeout",
381+
input: Dependency{
382+
Name: "test-dependency",
383+
Method: "TestMethod",
384+
Timeout: Duration(0),
385+
},
386+
expected: true,
387+
},
388+
{
389+
name: "Valid dependency - minimal fields",
390+
input: Dependency{
391+
Name: "test-dependency",
392+
Method: "TestMethod",
393+
},
394+
expected: true,
395+
},
396+
{
397+
name: "Invalid dependency - negative timeout",
398+
input: Dependency{
399+
Name: "test-dependency",
400+
Method: "TestMethod",
401+
Timeout: Duration(-1),
402+
},
403+
expected: false,
404+
},
405+
{
406+
name: "Invalid dependency - missing name",
407+
input: Dependency{
408+
Method: "TestMethod",
409+
},
410+
expected: false,
411+
},
412+
{
413+
name: "Invalid dependency - missing method",
414+
input: Dependency{
415+
Name: "test-dependency",
416+
},
417+
expected: false,
418+
},
419+
{
420+
name: "Invalid dependency - both name and method missing",
421+
input: Dependency{
422+
Params: map[string]interface{}{
423+
"param1": "value1",
424+
},
425+
},
426+
expected: false,
427+
},
428+
} {
429+
t.Run(test.name, func(t *testing.T) {
430+
v := NewConfigValidator("", &Config{})
431+
got := isValid(v.validateDependency(&test.input, field.NewPath("")))
432+
if test.expected != got {
433+
t.Errorf("wanted: %v, got: %v", test.expected, got)
434+
}
435+
})
436+
}
437+
}
438+
361439
func TestVerifyStep(t *testing.T) {
362440
for _, test := range []struct {
363441
name string
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# ClusterLoader2 Dependency System
2+
3+
The dependency system in ClusterLoader2 allows you to install and manage cluster dependencies as part of your test configuration. Dependencies are now executed at the test level with explicit setup and teardown phases, providing better lifecycle management and error handling.
4+
5+
## Overview
6+
7+
Dependencies are executed in two phases:
8+
- **Setup Phase**: Dependencies are set up before any test steps begin execution
9+
- **Teardown Phase**: Dependencies are torn down after all test resources are cleaned up
10+
11+
Key features:
12+
- Dependencies run before steps and are torn down after cleanup
13+
- Timeout enforcement for both setup and teardown operations
14+
- If any dependency fails, the entire test workflow fails
15+
- Dependencies are torn down in reverse order of setup
16+
17+
## New Usage Format
18+
19+
Dependencies are now defined at the test level using the new name/method format:
20+
21+
```yaml
22+
name: dra-steady-state
23+
dependencies:
24+
- name: install dra driver
25+
method: DRAInstall
26+
timeout: 10m
27+
params:
28+
foo: bar
29+
steps:
30+
- name: step1
31+
# ... step configuration
32+
```
33+
34+
### Dependency Fields
35+
36+
- `name` (required): A unique identifier for this dependency instance
37+
- `method` (required): The dependency method registered in the factory
38+
- `timeout` (optional): Maximum duration for both setup and teardown. If 0, operations wait forever
39+
- `params` (optional): Parameters passed to the dependency
40+
41+
## Creating Custom Dependencies
42+
43+
To create a custom dependency with the new interface:
44+
45+
1. Create a new package under `pkg/dependency/`
46+
2. Implement the new `Dependency` interface:
47+
```go
48+
type Dependency interface {
49+
Setup(config *Config) error
50+
Teardown(config *Config) error
51+
String() string
52+
}
53+
```
54+
55+
3. Example implementation:
56+
```go
57+
type myDependency struct{}
58+
59+
func (d *myDependency) Setup(config *Config) error {
60+
// Setup logic here
61+
return nil
62+
}
63+
64+
func (d *myDependency) Teardown(config *Config) error {
65+
// Teardown logic here
66+
return nil
67+
}
68+
69+
func (d *myDependency) String() string {
70+
return "MyDependency"
71+
}
72+
```
73+
74+
4. Register your dependency in an `init()` function:
75+
```go
76+
func init() {
77+
if err := dependency.Register("MyDependency", createMyDependency); err != nil {
78+
klog.Fatalf("Cannot register %s: %v", "MyDependency", err)
79+
}
80+
}
81+
```
82+
83+
5. Import your dependency package in `cmd/clusterloader.go`
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package dependency
18+
19+
import (
20+
"fmt"
21+
"sync"
22+
)
23+
24+
// factory is a default global factory instance.
25+
var factory = newDependencyFactory()
26+
27+
func newDependencyFactory() *dependencyFactory {
28+
return &dependencyFactory{
29+
createFuncs: make(map[string]createDependencyFunc),
30+
}
31+
}
32+
33+
// dependencyFactory is a factory that creates dependency instances.
34+
type dependencyFactory struct {
35+
lock sync.RWMutex
36+
createFuncs map[string]createDependencyFunc
37+
}
38+
39+
func (df *dependencyFactory) register(methodName string, createFunc createDependencyFunc) error {
40+
df.lock.Lock()
41+
defer df.lock.Unlock()
42+
_, exists := df.createFuncs[methodName]
43+
if exists {
44+
return fmt.Errorf("dependency with method %v already exists", methodName)
45+
}
46+
df.createFuncs[methodName] = createFunc
47+
return nil
48+
}
49+
50+
func (df *dependencyFactory) createDependency(methodName string) (Dependency, error) {
51+
df.lock.RLock()
52+
defer df.lock.RUnlock()
53+
createFunc, exists := df.createFuncs[methodName]
54+
if !exists {
55+
return nil, fmt.Errorf("unknown dependency method %s", methodName)
56+
}
57+
return createFunc(), nil
58+
}
59+
60+
// Register registers create dependency function in dependency factory.
61+
func Register(methodName string, createFunc createDependencyFunc) error {
62+
return factory.register(methodName, createFunc)
63+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package dependency
18+
19+
import (
20+
"k8s.io/perf-tests/clusterloader2/pkg/config"
21+
"k8s.io/perf-tests/clusterloader2/pkg/framework"
22+
"k8s.io/perf-tests/clusterloader2/pkg/provider"
23+
24+
"k8s.io/apimachinery/pkg/version"
25+
)
26+
27+
// Config provides client and parameters required for the dependency execution.
28+
type Config struct {
29+
// ClusterFramework returns cluster framework.
30+
ClusterFramework *framework.Framework
31+
// PrometheusFramework returns prometheus framework.
32+
PrometheusFramework *framework.Framework
33+
// Params is a map of {name: value} pairs enabling for injection of arbitrary config
34+
// into the Execute method.
35+
Params map[string]interface{}
36+
// TemplateProvider provides templated objects.
37+
TemplateProvider *config.TemplateProvider
38+
ClusterLoaderConfig *config.ClusterLoaderConfig
39+
40+
// Method identifies this instance of dependency.
41+
Method string
42+
CloudProvider provider.Provider
43+
44+
// ClusterVersion contains the version of the cluster and is used to select
45+
// available features.
46+
ClusterVersion version.Info
47+
}
48+
49+
// Dependency is a common interface for all dependency methods. It should be implemented by the user to
50+
// allow dependency method to be registered in the dependency factory.
51+
type Dependency interface {
52+
// Setup sets up the dependency and returns an error if setup fails.
53+
Setup(config *Config) error
54+
// Teardown tears down the dependency and returns an error if teardown fails.
55+
Teardown(config *Config) error
56+
// String returns a string representation of the dependency.
57+
String() string
58+
}
59+
60+
type createDependencyFunc func() Dependency

0 commit comments

Comments
 (0)