diff --git a/api/types/load_traffic.go b/api/types/load_traffic.go index 816c5ab4..b89e8576 100644 --- a/api/types/load_traffic.go +++ b/api/types/load_traffic.go @@ -137,14 +137,15 @@ type RequestWatchList struct { } // RequestPut defines PUT request for target resource type. +// +// NOTE: Today only `configmaps` (core/v1) is supported. The PUT builder +// constructs a ConfigMap body inline (`data.payload = `) and overwrites an existing object named `-`. The target objects must be pre-populated (e.g. via +// `runkperf data cm add`); a request against a non-existent object will +// receive a 404 from the apiserver and be counted as a failure. type RequestPut struct { // KubeGroupVersionResource identifies the resource URI. - // - // NOTE: Currently, it should be configmap or secrets because we can - // generate random bytes as blob for it. However, for the pod resource, - // we need to ensure a lot of things are ready, for instance, volumes, - // resource capacity. It's not easy to generate it randomly. Maybe we - // can introduce pod template in the future. KubeGroupVersionResource `yaml:",inline"` // Namespace is object's namespace. Namespace string `json:"namespace" yaml:"namespace"` @@ -152,8 +153,9 @@ type RequestPut struct { Name string `json:"name" yaml:"name"` // KeySpaceSize is used to generate random number as name's suffix. KeySpaceSize int `json:"keySpaceSize" yaml:"keySpaceSize"` - // ValueSize is the object's size in bytes. - ValueSize int `json:"valueSize" yaml:"valueSize"` + // PayloadSize is the size in bytes of a random string written into the + // ConfigMap body's `data.payload` field. + PayloadSize int `json:"payloadSize" yaml:"payloadSize"` } // RequestPatch defines PATCH request for target resource type. @@ -191,6 +193,9 @@ type RequestPostDel struct { KubeGroupVersionResource `yaml:",inline"` Namespace string `json:"namespace" yaml:"namespace"` DeleteRatio float64 `json:"deleteRatio" yaml:"deleteRatio"` + // 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"` } // Validate verifies fields of LoadProfile. @@ -316,15 +321,20 @@ func (r *RequestPut) Validate() error { return fmt.Errorf("kube metadata: %v", err) } - // TODO: check resource type + // Only core/v1 ConfigMap is supported today (see RequestPut doc comment). + if (r.Group != "" && r.Group != "core") || r.Version != "v1" || r.Resource != "configmaps" { + return fmt.Errorf("put currently only supports core/v1 configmaps, got group=%q version=%q resource=%q", + r.Group, r.Version, r.Resource) + } + if r.Name == "" { return fmt.Errorf("name pattern is required") } if r.KeySpaceSize <= 0 { return fmt.Errorf("keySpaceSize must > 0") } - if r.ValueSize <= 0 { - return fmt.Errorf("valueSize must > 0") + if r.PayloadSize <= 0 { + return fmt.Errorf("payloadSize must > 0") } return nil } @@ -405,5 +415,9 @@ func (r *RequestPostDel) Validate() error { return fmt.Errorf("delete ratio must be between 0 and 0.5: %v, create proportion should be greater than delete", r.DeleteRatio) } + if r.PayloadSize < 0 { + return fmt.Errorf("payloadSize must >= 0: %v", r.PayloadSize) + } + return nil } diff --git a/api/types/load_traffic_test.go b/api/types/load_traffic_test.go index b7976005..90456a82 100644 --- a/api/types/load_traffic_test.go +++ b/api/types/load_traffic_test.go @@ -59,7 +59,7 @@ specs: namespace: kperf name: kperf- keySpaceSize: 1000 - valueSize: 1024 + payloadSize: 1024 shares: 1000 - getPodLog: namespace: default @@ -119,7 +119,7 @@ specs: assert.Equal(t, "kperf", target.Specs[0].Requests[4].Put.Namespace) assert.Equal(t, "kperf-", target.Specs[0].Requests[4].Put.Name) assert.Equal(t, 1000, target.Specs[0].Requests[4].Put.KeySpaceSize) - assert.Equal(t, 1024, target.Specs[0].Requests[4].Put.ValueSize) + assert.Equal(t, 1024, target.Specs[0].Requests[4].Put.PayloadSize) assert.Equal(t, 10, target.Specs[0].Requests[5].Shares) assert.NotNil(t, target.Specs[0].Requests[5].GetPodLog) diff --git a/contrib/internal/manifests/workload/pods/templates/pod.tpl b/contrib/internal/manifests/workload/pods/templates/pod.tpl index eb30ed60..c9654d50 100644 --- a/contrib/internal/manifests/workload/pods/templates/pod.tpl +++ b/contrib/internal/manifests/workload/pods/templates/pod.tpl @@ -1,5 +1,6 @@ {{- $name:= .Values.namePattern }} {{- $namespace:= .Values.namespace }} +{{- $payload:= .Values.payload }} apiVersion: v1 kind: Pod metadata: @@ -11,3 +12,6 @@ spec: containers: - name: fake-container image: fake-image + env: + - name: PAYLOAD + value: {{ $payload | quote }} diff --git a/request/random.go b/request/random.go index 32012e04..a9351228 100644 --- a/request/random.go +++ b/request/random.go @@ -6,6 +6,7 @@ package request import ( "context" "crypto/rand" + "encoding/json" "fmt" "math/big" "sync" @@ -64,8 +65,10 @@ func NewWeightedRandomRequests(spec *types.LoadProfileSpec) (*WeightedRandomRequ builder = newRequestPatchBuilder(r.Patch, "", spec.MaxRetries) case r.PostDel != nil: builder = newRequestPostDelBuilder(r.PostDel, "", spec.MaxRetries) + case r.Put != nil: + builder = newRequestPutBuilder(r.Put, spec.MaxRetries) default: - return nil, fmt.Errorf("not implement for PUT yet") + return nil, fmt.Errorf("unknown request type: %+v", r) } reqBuilders = append(reqBuilders, builder) } @@ -429,6 +432,7 @@ type requestPostDelBuilder struct { resourceVersion string namespace string deleteRatio float64 + payloadSize int maxRetries int // Per-builder cache for created resources @@ -445,6 +449,7 @@ func newRequestPostDelBuilder(src *types.RequestPostDel, resourceVersion string, resourceVersion: resourceVersion, namespace: src.Namespace, deleteRatio: src.DeleteRatio, + payloadSize: src.PayloadSize, maxRetries: maxRetries, cache: InitCache(), // Initialize the cache } @@ -498,6 +503,7 @@ func (b *requestPostDelBuilder) Build(cli rest.Interface) Requester { body, _ := utils.RenderTemplate(b.resource, map[string]interface{}{ "namePattern": name, "namespace": b.namespace, + "payload": randomPayload(b.payloadSize), }) return &PostDelDiscardRequester{ @@ -545,3 +551,101 @@ func (reqr *PostDelDiscardRequester) Do(ctx context.Context) (bytes int64, err e func toPtr[T any](v T) *T { return &v } + +// requestPutBuilder builds PUT requests that overwrite an existing ConfigMap +// named `-` with a body whose `data.payload` field +// carries `payloadSize` random bytes. The target ConfigMap must already exist +// (e.g. created via `runkperf data cm add`); otherwise the apiserver returns +// 404 and the request is counted as a failure. Only `configmaps` is supported +// today — see RequestPut.Validate. +type requestPutBuilder struct { + version schema.GroupVersion + resource string + namespace string + name string + keySpaceSize int + payloadSize int + maxRetries int +} + +func newRequestPutBuilder(src *types.RequestPut, maxRetries int) *requestPutBuilder { + return &requestPutBuilder{ + version: schema.GroupVersion{ + Group: src.Group, + Version: src.Version, + }, + resource: src.Resource, + namespace: src.Namespace, + name: src.Name, + keySpaceSize: src.KeySpaceSize, + payloadSize: src.PayloadSize, + maxRetries: maxRetries, + } +} + +// Build implements RESTRequestBuilder.Build. +func (b *requestPutBuilder) Build(cli rest.Interface) Requester { + comps := []string{"api", b.version.Version, "namespaces", b.namespace, b.resource} + + randomInt, _ := rand.Int(rand.Reader, big.NewInt(int64(b.keySpaceSize))) + finalName := fmt.Sprintf("%s-%d", b.name, randomInt.Int64()) + comps = append(comps, finalName) + + cm := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "ConfigMap"}, + ObjectMeta: metav1.ObjectMeta{ + Name: finalName, + Namespace: b.namespace, + }, + Data: map[string]string{"payload": randomPayload(b.payloadSize)}, + } + body, _ := json.Marshal(cm) + + return &DiscardRequester{ + BaseRequester: BaseRequester{ + method: "PUT", + req: cli.Put().AbsPath(comps...).Body(body).MaxRetries(b.maxRetries), + }, + } +} + +// randomPayload returns a string of exactly n bytes, uniformly sampled from +// [a-zA-Z0-9] via crypto/rand with rejection sampling to avoid modulo bias. +// Returns "" when n <= 0. +func randomPayload(n int) string { + if n <= 0 { + return "" + } + const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + // Only use byte values in [0, threshold). Since len(alphabet)=62 and + // 248 is the largest multiple of 62 below 256, mapping b%62 is uniform + // for accepted bytes. Bytes 248..255 are skipped to avoid modulo bias. + const threshold = byte(256 - 256%len(alphabet)) + + out := make([]byte, n) + + bufSize := n + if bufSize > 4096 { + bufSize = 4096 + } + buf := make([]byte, bufSize) + + for i := 0; i < n; { + if _, err := rand.Read(buf); err != nil { + panic(fmt.Errorf("failed to read random bytes: %w", err)) + } + + for _, b := range buf { + if b >= threshold { + continue + } + out[i] = alphabet[int(b)%len(alphabet)] + i++ + if i == n { + return string(out) + } + } + } + + return string(out) +}