Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions api/types/load_traffic.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
115 changes: 115 additions & 0 deletions api/types/load_traffic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
7 changes: 7 additions & 0 deletions contrib/internal/manifests/workload/pods/templates/pod.tpl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{{- $name:= .Values.namePattern }}
{{- $namespace:= .Values.namespace }}
{{- $payload:= .Values.payload }}
{{- $nodeSelector:= .Values.nodeSelector }}
apiVersion: v1
kind: Pod
metadata:
Expand All @@ -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:
Expand Down
14 changes: 11 additions & 3 deletions request/random.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -462,13 +463,19 @@ 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,
resourceVersion: resourceVersion,
namespace: src.Namespace,
deleteRatio: src.DeleteRatio,
payloadSize: src.PayloadSize,
nodeSelector: nodeSelector,
maxRetries: maxRetries,
cache: InitCache(), // Initialize the cache
}
Expand Down Expand Up @@ -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))
Expand Down
Loading