From b18dd6b1696997c114bf41a8964364291ff2a638 Mon Sep 17 00:00:00 2001 From: Dejan Pejchev Date: Mon, 29 Jan 2024 16:07:49 +0100 Subject: [PATCH] add more flags --- cmd/simulator/cmd/run.go | 12 ++- cmd/simulator/config/config.go | 8 +- hack/fake-deployment.yaml | 33 ++++++ internal/k8s/manager.go | 6 +- internal/ratelimiter/executor/kubernetes.go | 12 ++- .../ratelimiter/executor/kubernetes_test.go | 12 +-- internal/simulator/data/stages.yaml | 4 +- internal/simulator/resources/resources.go | 102 ++++++++++++++---- .../simulator/resources/resources_test.go | 57 ++++++++++ 9 files changed, 208 insertions(+), 38 deletions(-) create mode 100644 internal/simulator/resources/resources_test.go diff --git a/cmd/simulator/cmd/run.go b/cmd/simulator/cmd/run.go index 5534cca..6d9d7e4 100644 --- a/cmd/simulator/cmd/run.go +++ b/cmd/simulator/cmd/run.go @@ -7,6 +7,8 @@ import ( "sync" "time" + "github.com/dejanzele/batch-simulator/internal/simulator/resources" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" @@ -46,6 +48,9 @@ The process is designed to mimic real-world Kubernetes environments for testing } pterm.Success.Println("kubernetes client initialized successfully!") + pterm.Info.Printf("setting the default env vars type to %s type", config.DefaultEnvVarsType) + resources.SetDefaultEnvVarsType(config.DefaultEnvVarsType) + if config.Remote { pterm.Success.Println("running simulation in remote Kubernetes cluster") err = runRemote(cmd.Context(), client) @@ -75,6 +80,8 @@ func runRemote(ctx context.Context, client kubernetes.Interface) error { "--job-creator-frequency", config.JobCreatorFrequency.String(), "--job-creator-requests", fmt.Sprintf("%d", config.JobCreatorRequests), "--job-creator-limit", fmt.Sprintf("%d", config.JobCreatorLimit), + "--random-env-vars", fmt.Sprintf("%t", config.RandomEnvVars), + "--default-env-vars-type", config.DefaultEnvVarsType, "--namespace", config.Namespace, "--no-gui", "--verbose", @@ -103,7 +110,8 @@ func runLocal(ctx context.Context, client kubernetes.Interface) error { pterm.Info.Println("initializing kubernetes resource manager...") managerConfig := k8s.ManagerConfig{ - Namespace: config.Namespace, + Namespace: config.Namespace, + RandomEnvVars: config.RandomEnvVars, PodRateLimiterConfig: k8s.RateLimiterConfig{ Frequency: config.PodCreatorFrequency, Requests: config.PodCreatorRequests, @@ -152,6 +160,8 @@ func NewRunCmd() *cobra.Command { runCmd.Flags().StringVarP(&config.Namespace, "namespace", "n", config.Namespace, "namespace in which to create simulation resources") runCmd.Flags().BoolVarP(&config.Remote, "remote", "r", config.Remote, "run the simulator in a Kubernetes cluster") runCmd.Flags().IntVar(&config.PodSpecSize, "pod-spec-size", config.PodSpecSize, "size of the pod spec in bytes") + runCmd.Flags().BoolVar(&config.RandomEnvVars, "random-env-vars", config.RandomEnvVars, "use random env vars") + runCmd.Flags().StringVar(&config.DefaultEnvVarsType, "default-env-vars-type", config.DefaultEnvVarsType, "default env vars type") return runCmd } diff --git a/cmd/simulator/config/config.go b/cmd/simulator/config/config.go index 15cac5a..327ed94 100644 --- a/cmd/simulator/config/config.go +++ b/cmd/simulator/config/config.go @@ -1,6 +1,8 @@ package config -import "time" +import ( + "time" +) var ( // QPS configures maximum queries per second to use while talking with Kubernetes API. @@ -51,4 +53,8 @@ var ( SimulatorImage = "dpejcev/batchsim" // SimulatorTag is the tag used for the simulator. SimulatorTag = "latest" + // RandomEnvVars configures whether the simulator should use random envvars. + RandomEnvVars = true + // DefaultEnvVarsType is the default envvar type which are generated when creating fake pods. + DefaultEnvVarsType = "medium" ) diff --git a/hack/fake-deployment.yaml b/hack/fake-deployment.yaml index e69de29..74bbf2d 100644 --- a/hack/fake-deployment.yaml +++ b/hack/fake-deployment.yaml @@ -0,0 +1,33 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: fake-deployment + namespace: default +spec: + replicas: 5000 + selector: + matchLabels: + app: fake-pod + template: + metadata: + labels: + app: fake-pod + spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: type + operator: In + values: + - kwok + # A taints was added to an automatically created Node. + # You can remove taints of Node or add this tolerations. + tolerations: + - key: "kwok.x-k8s.io/node" + operator: "Exists" + effect: "NoSchedule" + containers: + - name: fake-container + image: fake-image diff --git a/internal/k8s/manager.go b/internal/k8s/manager.go index e6571b8..114a916 100644 --- a/internal/k8s/manager.go +++ b/internal/k8s/manager.go @@ -52,6 +52,8 @@ type ManagerConfig struct { Namespace string // Logger is the logger that should be used by the Manager. Logger *slog.Logger + // RandomEnvVars is used to determine whether random environment variables should be added to created Pods or Jobs. + RandomEnvVars bool // PodRateLimiterConfig is the configuration for the rate limited PodCreator. PodRateLimiterConfig RateLimiterConfig // NodeRateLimiterConfig is the configuration for the rate limited NodeCreator. @@ -80,14 +82,14 @@ func NewManager(client kubernetes.Interface, cfg *ManagerConfig) *Manager { defaultedConfig.NodeRateLimiterConfig.Limit, nodeExecutor, ) - podExecutor := executor.NewPodCreator(client, defaultedConfig.Namespace) + podExecutor := executor.NewPodCreator(client, defaultedConfig.Namespace, defaultedConfig.RandomEnvVars) podRateLimiter := ratelimiter.New[*corev1.Pod]( defaultedConfig.PodRateLimiterConfig.Frequency, defaultedConfig.PodRateLimiterConfig.Requests, defaultedConfig.PodRateLimiterConfig.Limit, podExecutor, ) - jobExecutor := executor.NewJobCreator(client, defaultedConfig.Namespace) + jobExecutor := executor.NewJobCreator(client, defaultedConfig.Namespace, defaultedConfig.RandomEnvVars) jobRateLimiter := ratelimiter.New[*batchv1.Job]( defaultedConfig.JobRateLimiterConfig.Frequency, defaultedConfig.JobRateLimiterConfig.Requests, diff --git a/internal/ratelimiter/executor/kubernetes.go b/internal/ratelimiter/executor/kubernetes.go index de668dd..6cff698 100644 --- a/internal/ratelimiter/executor/kubernetes.go +++ b/internal/ratelimiter/executor/kubernetes.go @@ -25,14 +25,16 @@ type kubernetesExecutor struct { // PodCreator is used to create Pods. type PodCreator struct { kubernetesExecutor + randomEnvVars bool } -func NewPodCreator(client kubernetes.Interface, namespace string) *PodCreator { +func NewPodCreator(client kubernetes.Interface, namespace string, randomEnvVars bool) *PodCreator { return &PodCreator{ kubernetesExecutor: kubernetesExecutor{ client: client, namespace: namespace, }, + randomEnvVars: randomEnvVars, } } @@ -44,7 +46,7 @@ func (c *PodCreator) Identifier() string { // Execute creates a Pod. func (c *PodCreator) Execute(ctx context.Context) error { name := fmt.Sprintf("fake-pod-%s", util.RandomRFC1123Name(16)) - item := resources.NewFakePod(name, c.namespace) + item := resources.NewFakePod(name, c.namespace, c.randomEnvVars) _, err := c.client.CoreV1().Pods(c.namespace).Create(ctx, item, metav1.CreateOptions{}) if err != nil { return ratelimiter.NewCreateError(err, "v1", "Pod", item) @@ -86,14 +88,16 @@ var _ ratelimiter.Executor[*corev1.Node] = &NodeCreator{} type JobCreator struct { kubernetesExecutor + randomEnvVars bool } -func NewJobCreator(client kubernetes.Interface, namespace string) *JobCreator { +func NewJobCreator(client kubernetes.Interface, namespace string, randomEnvVars bool) *JobCreator { return &JobCreator{ kubernetesExecutor: kubernetesExecutor{ client: client, namespace: namespace, }, + randomEnvVars: randomEnvVars, } } @@ -105,7 +109,7 @@ func (c *JobCreator) Identifier() string { // Execute creates a Node. func (c *JobCreator) Execute(ctx context.Context) error { name := fmt.Sprintf("fake-job-%s", util.RandomRFC1123Name(16)) - item := resources.NewFakeJob(name, c.namespace) + item := resources.NewFakeJob(name, c.namespace, c.randomEnvVars) _, err := c.client.BatchV1().Jobs(c.namespace).Create(ctx, item, metav1.CreateOptions{}) if err != nil { return ratelimiter.NewCreateError(err, "batch/v1", "Job", item) diff --git a/internal/ratelimiter/executor/kubernetes_test.go b/internal/ratelimiter/executor/kubernetes_test.go index adbce1c..cdc4eb4 100644 --- a/internal/ratelimiter/executor/kubernetes_test.go +++ b/internal/ratelimiter/executor/kubernetes_test.go @@ -19,7 +19,7 @@ import ( func TestNewPodCreator(t *testing.T) { t.Parallel() - creator := NewPodCreator(fake.NewSimpleClientset(), "test") + creator := NewPodCreator(fake.NewSimpleClientset(), "test", false) assert.Equal(t, "test", creator.namespace) assert.NotNil(t, creator.client) } @@ -31,7 +31,7 @@ func TestPodCreator(t *testing.T) { t.Parallel() fakeClient := fake.NewSimpleClientset() - executor := NewPodCreator(fakeClient, "default") + executor := NewPodCreator(fakeClient, "default", false) ctx := context.Background() if err := executor.Execute(ctx); err != nil { @@ -57,7 +57,7 @@ func TestPodCreator(t *testing.T) { PrependReactor("create", "pods", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { return true, &corev1.Pod{}, errors.New("error creating pod") }) - executor := NewPodCreator(fakeClient, "default") + executor := NewPodCreator(fakeClient, "default", false) ctx := context.Background() err := executor.Execute(ctx) @@ -127,7 +127,7 @@ func TestNodeCreator(t *testing.T) { func TestNewJobCreator(t *testing.T) { t.Parallel() - creator := NewJobCreator(fake.NewSimpleClientset(), "test") + creator := NewJobCreator(fake.NewSimpleClientset(), "test", false) assert.Equal(t, "test", creator.namespace) assert.NotNil(t, creator.client) } @@ -139,7 +139,7 @@ func TestJobCreator(t *testing.T) { t.Parallel() fakeClient := fake.NewSimpleClientset() - executor := NewJobCreator(fakeClient, "default") + executor := NewJobCreator(fakeClient, "default", false) ctx := context.Background() if err := executor.Execute(ctx); err != nil { @@ -165,7 +165,7 @@ func TestJobCreator(t *testing.T) { PrependReactor("create", "jobs", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { return true, &batchv1.Job{}, errors.New("error creating job") }) - executor := NewJobCreator(fakeClient, "default") + executor := NewJobCreator(fakeClient, "default", false) ctx := context.Background() err := executor.Execute(ctx) diff --git a/internal/simulator/data/stages.yaml b/internal/simulator/data/stages.yaml index b913c05..72e5e0b 100644 --- a/internal/simulator/data/stages.yaml +++ b/internal/simulator/data/stages.yaml @@ -169,8 +169,8 @@ spec: values: - Running delay: - durationMilliseconds: 10800000 - jitterDurationMilliseconds: 21600000 + durationMilliseconds: 600000 + jitterDurationMilliseconds: 90000 --- apiVersion: kwok.x-k8s.io/v1alpha1 kind: Stage diff --git a/internal/simulator/resources/resources.go b/internal/simulator/resources/resources.go index a476e3c..1509de0 100644 --- a/internal/simulator/resources/resources.go +++ b/internal/simulator/resources/resources.go @@ -2,8 +2,7 @@ package resources import ( "fmt" - - "github.com/dejanzele/batch-simulator/cmd/simulator/config" + "math/rand" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" @@ -15,12 +14,69 @@ import ( ) const ( - defaultEnvVarCount = 10 + defaultEnvVarCount = 5 +) + +var ( + nano = newEnvVars(defaultEnvVarCount, 100, "SOME_ENV_VAR_NANO") + micro = newEnvVars(defaultEnvVarCount, 200, "SOME_ENV_VAR_MICRO") + xsmall = newEnvVars(defaultEnvVarCount, 500, "SOME_ENV_VAR_XSMALL") + small = newEnvVars(defaultEnvVarCount, 1024, "SOME_ENV_VAR_SMALL") + medium = newEnvVars(defaultEnvVarCount, 2*1024, "SOME_ENV_VAR_MEDIUM") + large = newEnvVars(defaultEnvVarCount, 4*1024, "SOME_ENV_VAR_LARGE") + xlarge = newEnvVars(defaultEnvVarCount, 8*1024, "SOME_ENV_VAR_XLARGE") + xlarge2 = newEnvVars(defaultEnvVarCount, 10*1024, "SOME_ENV_VAR_XLARGE2") ) -var envVars = newEnvVars(defaultEnvVarCount, config.PodSpecSize/defaultEnvVarCount) +// DefaultEnvVarsType is the default envvar slice type. +var DefaultEnvVarsType = medium + +// SetDefaultEnvVarsType sets the default envvar slice type. +func SetDefaultEnvVarsType(envVarType string) { + switch envVarType { + case "nano": + DefaultEnvVarsType = nano + case "micro": + DefaultEnvVarsType = micro + case "xsmall": + DefaultEnvVarsType = xsmall + case "small": + DefaultEnvVarsType = small + case "medium": + DefaultEnvVarsType = medium + case "large": + DefaultEnvVarsType = large + case "xlarge": + DefaultEnvVarsType = xlarge + case "xlarge2": + DefaultEnvVarsType = xlarge2 + default: + DefaultEnvVarsType = medium + } +} + +// envVarsByType is a slice of different envvar slice types. +var envVarsByType = [][]corev1.EnvVar{nano, micro, xsmall, small, medium, large, xlarge, xlarge2} + +// newEnvVars creates a slice of envvars with the specified count and size. +func newEnvVars(count, size int, prefix string) []corev1.EnvVar { + envVars := make([]corev1.EnvVar, 0, count) + for i := 0; i < count; i++ { + envVars = append(envVars, newEnvVar(fmt.Sprintf("%s_%d", prefix, i), size)) + + } + return envVars +} -// NewFakeNode creates a fake Kubernetes Node resource with the specified nodeName. +// newEnvVar creates a new envvar with the specified name and size. +func newEnvVar(name string, size int) corev1.EnvVar { + return corev1.EnvVar{ + Name: name, + Value: util.RandomText(size), + } +} + +// NewFakeNode creates a fake Kubernetes Node resource, managed by KWOK, with the specified name. func NewFakeNode(nodeName string) *corev1.Node { return &corev1.Node{ TypeMeta: metav1.TypeMeta{ @@ -81,7 +137,8 @@ func NewFakeNode(nodeName string) *corev1.Node { } } -func NewFakeJob(name, namespace string) *batchv1.Job { +// NewFakeJob creates a fake Kubernetes Job resource, managed by KWOK, with the specified name and namespace. +func NewFakeJob(name, namespace string, randomEnvVars bool) *batchv1.Job { return &batchv1.Job{ TypeMeta: metav1.TypeMeta{ APIVersion: "v1", @@ -98,13 +155,14 @@ func NewFakeJob(name, namespace string) *batchv1.Job { Spec: batchv1.JobSpec{ TTLSecondsAfterFinished: ptr.To[int32](30), Template: corev1.PodTemplateSpec{ - Spec: newPodSpec(), + Spec: newPodSpec(randomEnvVars), }, }, } } -func NewFakePod(name, namespace string) *corev1.Pod { +// NewFakePod creates a fake Kubernetes Pod resource, managed by KWOK, with the specified name and namespace. +func NewFakePod(name, namespace string, randomEnvVars bool) *corev1.Pod { return &corev1.Pod{ TypeMeta: metav1.TypeMeta{ APIVersion: "v1", @@ -118,11 +176,17 @@ func NewFakePod(name, namespace string) *corev1.Pod { "type": "kwok", }, }, - Spec: newPodSpec(), + Spec: newPodSpec(randomEnvVars), } } -func newPodSpec() corev1.PodSpec { +// newPodSpec creates a new pod spec. +// If randomEnvVars is true, a random envvar slice will be used, otherwise the default (large) envvar slice will be used. +func newPodSpec(randomEnvVars bool) corev1.PodSpec { + envVars := DefaultEnvVarsType + if randomEnvVars { + envVars = getRandomEnvVarType() + } return corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyNever, Affinity: newAffinity(), @@ -143,6 +207,12 @@ func newPodSpec() corev1.PodSpec { } } +// getRandomEnvVarType returns a random envvar slice from the envVarsByType. +func getRandomEnvVarType() []corev1.EnvVar { + return envVarsByType[rand.Intn(len(envVarsByType))] +} + +// newAffinity creates a new affinity which matches nodes with the type kwok. func newAffinity() *corev1.Affinity { return &corev1.Affinity{ NodeAffinity: &corev1.NodeAffinity{ @@ -162,15 +232,3 @@ func newAffinity() *corev1.Affinity { }, } } - -func newEnvVars(count, size int) []corev1.EnvVar { - envVars := make([]corev1.EnvVar, 0, count) - for i := 0; i < count; i++ { - envVars = append(envVars, corev1.EnvVar{ - Name: fmt.Sprintf("SOME_ENV_VAR_%d", i), - Value: util.RandomText(size), - }) - - } - return envVars -} diff --git a/internal/simulator/resources/resources_test.go b/internal/simulator/resources/resources_test.go new file mode 100644 index 0000000..3b95eca --- /dev/null +++ b/internal/simulator/resources/resources_test.go @@ -0,0 +1,57 @@ +package resources + +import ( + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + "testing" +) + +func TestNewPodSpec(t *testing.T) { + t.Parallel() + + t.Run("should return a valid PodSpec with default envvars if randomEnvVars is false", func(t *testing.T) { + t.Parallel() + + podSpec := newPodSpec(false) + assert.Len(t, podSpec.Containers[0].Env, defaultEnvVarCount) + assert.Equal(t, medium, podSpec.Containers[0].Env) + }) + + t.Run("should return a valid PodSpec with random envvars if randomEnvVars is true", func(t *testing.T) { + t.Parallel() + + podSpec := newPodSpec(true) + assert.Len(t, podSpec.Containers[0].Env, defaultEnvVarCount) + }) +} + +// TestSetDefaultEnvVarsType tests the SetDefaultEnvVarsType function. +func TestSetDefaultEnvVarsType(t *testing.T) { + t.Parallel() + + // Define the test cases + testCases := []struct { + name string + envVarType string + expectedType []corev1.EnvVar // Assuming EnvVarsType is the type of DefaultEnvVarsType + }{ + {"nano type", "nano", nano}, + {"micro type", "micro", micro}, + {"xsmall type", "xsmall", xsmall}, + {"small type", "small", small}, + {"medium type", "medium", medium}, + {"large type", "large", large}, + {"xlarge type", "xlarge", xlarge}, + {"xlarge2 type", "xlarge2", xlarge2}, + {"default type", "unknown", medium}, // Test for an unknown type + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + SetDefaultEnvVarsType(tc.envVarType) + assert.Equal(t, tc.expectedType, DefaultEnvVarsType) + }) + } +}