diff --git a/api/types/load_traffic.go b/api/types/load_traffic.go index a38ad82..d648224 100644 --- a/api/types/load_traffic.go +++ b/api/types/load_traffic.go @@ -8,7 +8,9 @@ import ( "fmt" "strings" + metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation" apitypes "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/validation/field" ) // ContentType represents the format of response. @@ -203,6 +205,18 @@ type RequestPostDel struct { // PayloadSize is the size in bytes of a random padding string injected // into the created object (e.g. as a Pod env var value). 0 means no padding. PayloadSize int `json:"payloadSize" yaml:"payloadSize"` + // Pod contains options for Pods created by POST operations. + Pod *PostDelPodOptions `json:"pod,omitempty" yaml:"pod,omitempty"` +} + +type PostDelPodOptions struct { + // NodeSelector schedules workload Pods separately from runner Pods, for + // example on a dedicated node pool: + // + // pod: + // nodeSelector: + // agentpool: benchmark + NodeSelector map[string]string `json:"nodeSelector,omitempty" yaml:"nodeSelector,omitempty"` } // Validate verifies fields of LoadProfile. @@ -426,5 +440,16 @@ func (r *RequestPostDel) Validate() error { return fmt.Errorf("payloadSize must >= 0: %v", r.PayloadSize) } + if r.Pod != nil && len(r.Pod.NodeSelector) > 0 { + if r.Group != "" || r.Version != "v1" || r.Resource != "pods" { + return fmt.Errorf("pod nodeSelector only supports core/v1/pods") + } + + errs := metav1validation.ValidateLabels(r.Pod.NodeSelector, field.NewPath("pod", "nodeSelector")) + if len(errs) > 0 { + return fmt.Errorf("invalid pod nodeSelector: %v", errs.ToAggregate()) + } + } + return nil } diff --git a/api/types/load_traffic_test.go b/api/types/load_traffic_test.go index 90456a8..fface1f 100644 --- a/api/types/load_traffic_test.go +++ b/api/types/load_traffic_test.go @@ -218,3 +218,118 @@ func TestWeightedRequest(t *testing.T) { }) } } + +func TestRequestPostDelNodeSelectorValidation(t *testing.T) { + tests := []struct { + name string + group string + version string + resource string + pod *PostDelPodOptions + wantErr bool + }{ + { + name: "not configured", + version: "v1", + resource: "pods", + }, + { + name: "other resource without node selector", + group: "apps", + version: "v1", + resource: "deployments", + }, + { + name: "configured", + version: "v1", + resource: "pods", + pod: &PostDelPodOptions{ + NodeSelector: map[string]string{"agentpool": "benchmark"}, + }, + }, + { + name: "unsupported group", + group: "apps", + version: "v1", + resource: "pods", + pod: &PostDelPodOptions{ + NodeSelector: map[string]string{"agentpool": "benchmark"}, + }, + wantErr: true, + }, + { + name: "unsupported version", + version: "v2", + resource: "pods", + pod: &PostDelPodOptions{ + NodeSelector: map[string]string{"agentpool": "benchmark"}, + }, + wantErr: true, + }, + { + name: "unsupported resource", + version: "v1", + resource: "deployments", + pod: &PostDelPodOptions{ + NodeSelector: map[string]string{"agentpool": "benchmark"}, + }, + wantErr: true, + }, + { + name: "invalid selector key", + version: "v1", + resource: "pods", + pod: &PostDelPodOptions{ + NodeSelector: map[string]string{"invalid key": "benchmark"}, + }, + wantErr: true, + }, + { + name: "invalid selector value", + version: "v1", + resource: "pods", + pod: &PostDelPodOptions{ + NodeSelector: map[string]string{"agentpool": "invalid value"}, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + request := RequestPostDel{ + KubeGroupVersionResource: KubeGroupVersionResource{ + Group: tt.group, + Version: tt.version, + Resource: tt.resource, + }, + Pod: tt.pod, + } + + if tt.wantErr { + assert.Error(t, request.Validate()) + } else { + assert.NoError(t, request.Validate()) + } + }) + } +} + +func TestRequestPostDelNodeSelectorUnmarshalFromYAML(t *testing.T) { + input := ` +version: v1 +resource: pods +namespace: default +deleteRatio: 0.4 +payloadSize: 1024 +pod: + nodeSelector: + agentpool: benchmark +` + + var request RequestPostDel + require.NoError(t, yaml.Unmarshal([]byte(input), &request)) + require.NotNil(t, request.Pod) + assert.Equal(t, map[string]string{"agentpool": "benchmark"}, request.Pod.NodeSelector) + assert.NoError(t, request.Validate()) +} diff --git a/contrib/internal/manifests/workload/pods/templates/pod.tpl b/contrib/internal/manifests/workload/pods/templates/pod.tpl index 9cec5db..2703889 100644 --- a/contrib/internal/manifests/workload/pods/templates/pod.tpl +++ b/contrib/internal/manifests/workload/pods/templates/pod.tpl @@ -1,6 +1,7 @@ {{- $name:= .Values.namePattern }} {{- $namespace:= .Values.namespace }} {{- $payload:= .Values.payload }} +{{- $nodeSelector:= .Values.nodeSelector }} apiVersion: v1 kind: Pod metadata: @@ -9,6 +10,12 @@ metadata: labels: app: fake-pod spec: + {{- if $nodeSelector }} + nodeSelector: + {{- range $key, $value := $nodeSelector }} + "{{ $key }}": "{{ $value }}" + {{- end }} + {{- end }} # Use pause image so pods reach Running with few events, avoiding an # ImagePullBackOff loop that keeps producing events and filling etcd. containers: diff --git a/request/random.go b/request/random.go index e2892ef..a5a88a9 100644 --- a/request/random.go +++ b/request/random.go @@ -452,6 +452,7 @@ type requestPostDelBuilder struct { namespace string deleteRatio float64 payloadSize int + nodeSelector map[string]string maxRetries int // Per-builder cache for created resources @@ -462,6 +463,11 @@ type requestPostDelBuilder struct { } func newRequestPostDelBuilder(src *types.RequestPostDel, resourceVersion string, maxRetries int) *requestPostDelBuilder { + var nodeSelector map[string]string + if src.Pod != nil { + nodeSelector = src.Pod.NodeSelector + } + return &requestPostDelBuilder{ version: schema.GroupVersion{Group: src.Group, Version: src.Version}, resource: src.Resource, @@ -469,6 +475,7 @@ func newRequestPostDelBuilder(src *types.RequestPostDel, resourceVersion string, namespace: src.Namespace, deleteRatio: src.DeleteRatio, payloadSize: src.PayloadSize, + nodeSelector: nodeSelector, maxRetries: maxRetries, cache: InitCache(), // Initialize the cache } @@ -520,9 +527,10 @@ func (b *requestPostDelBuilder) Build(cli rest.Interface) Requester { name := fmt.Sprintf("%d-%d", timestamp, counter) body, err := utils.RenderTemplate(b.resource, map[string]interface{}{ - "namePattern": name, - "namespace": b.namespace, - "payload": randomPayload(b.payloadSize), + "namePattern": name, + "namespace": b.namespace, + "payload": randomPayload(b.payloadSize), + "nodeSelector": b.nodeSelector, }) if err != nil { panic(fmt.Errorf("failed to render %s template: %w", b.resource, err))