diff --git a/.github/workflows/benchmark.yaml b/.github/workflows/benchmark.yaml index 24ef5dbf1..1d8bcfa16 100644 --- a/.github/workflows/benchmark.yaml +++ b/.github/workflows/benchmark.yaml @@ -40,6 +40,12 @@ jobs: shell: bash run: | ./hack/e2e-test.sh e2e/kwokctl/benchmark + + - name: Test Benchmark + shell: bash + run: | + ./hack/e2e-test.sh e2e/kwokctl/benchmark-hack + - name: Upload logs uses: actions/upload-artifact@v4 if: failure() diff --git a/test/e2e/benchmark.go b/test/e2e/benchmark.go index 6482c4650..5489f203f 100644 --- a/test/e2e/benchmark.go +++ b/test/e2e/benchmark.go @@ -19,6 +19,7 @@ package e2e import ( "context" "fmt" + "io" "os/exec" "strconv" "strings" @@ -64,6 +65,45 @@ func waitResource(ctx context.Context, t *testing.T, kwokctlPath, name, resource } } +func readerPodYaml(size int) io.Reader { + r, w := io.Pipe() + go func() { + defer w.Close() + for i := 0; i < size; i++ { + _, _ = fmt.Fprintf(w, podYaml, i, i) + } + }() + return r +} + +var podYaml = ` +apiVersion: v1 +kind: Pod +metadata: + name: pod-%d + namespace: default + uid: 00000000-0000-0000-0001-%012d +spec: + containers: + - image: busybox + name: container-0 + nodeName: node-0 +--- +` + +func scaleCreatePodWithHack(ctx context.Context, t *testing.T, kwokctlPath string, name string, size int) error { + scaleCmd := exec.CommandContext(ctx, kwokctlPath, "--name", name, "hack", "put", "--path", "-") + scaleCmd.Stdin = readerPodYaml(size) + if err := scaleCmd.Start(); err != nil { + return fmt.Errorf("failed to start scale command: %w", err) + } + + if err := waitResource(ctx, t, kwokctlPath, name, "Pod", "Running", size, 5, 10); err != nil { + return fmt.Errorf("failed to wait for resource: %w", err) + } + return nil +} + func scaleCreatePod(ctx context.Context, t *testing.T, kwokctlPath string, name string, size int) error { cmd := exec.CommandContext(ctx, kwokctlPath, "--name", name, "kubectl", "get", "node", "-o", "jsonpath={.items.*.metadata.name}") // #nosec G204 out, err := cmd.Output() @@ -105,6 +145,59 @@ func scaleDeletePod(ctx context.Context, t *testing.T, kwokctlPath string, name return nil } +func readerNodeYaml(size int) io.Reader { + r, w := io.Pipe() + go func() { + defer w.Close() + for i := 0; i < size; i++ { + _, _ = fmt.Fprintf(w, nodeYaml, i, i) + } + }() + return r +} + +var nodeYaml = ` +apiVersion: v1 +kind: Node +metadata: + annotations: + kwok.x-k8s.io/node: fake + node.alpha.kubernetes.io/ttl: "0" + labels: + beta.kubernetes.io/arch: amd64 + beta.kubernetes.io/os: linux + kubernetes.io/arch: amd64 + kubernetes.io/os: linux + kubernetes.io/role: agent + node-role.kubernetes.io/agent: "" + type: kwok + name: node-%d + uid: 00000000-0000-0000-0000-%012d +status: + allocatable: + cpu: "32" + memory: 256Gi + pods: "110" + capacity: + cpu: "32" + memory: 256Gi + pods: "110" +--- +` + +func scaleCreateNodeWithHack(ctx context.Context, t *testing.T, kwokctlPath string, name string, size int) error { + scaleCmd := exec.CommandContext(ctx, kwokctlPath, "--name", name, "hack", "put", "--path", "-") + scaleCmd.Stdin = readerNodeYaml(size) + if err := scaleCmd.Start(); err != nil { + return fmt.Errorf("failed to start scale command: %w", err) + } + + if err := waitResource(ctx, t, kwokctlPath, name, "Node", "Ready", size, 10, 10); err != nil { + return fmt.Errorf("failed to wait for resource: %w", err) + } + return nil +} + func scaleCreateNode(ctx context.Context, t *testing.T, kwokctlPath string, name string, size int) error { scaleCmd := exec.CommandContext(ctx, kwokctlPath, "--name", name, "scale", "node", "fake-node", "--replicas", strconv.Itoa(size)) // #nosec G204 if err := scaleCmd.Start(); err != nil { @@ -150,3 +243,37 @@ func CaseBenchmark(kwokctlPath, clusterName string) *features.FeatureBuilder { return ctx }) } + +func CaseBenchmarkWithHack(kwokctlPath, clusterName string) *features.FeatureBuilder { + return features.New("Benchmark"). + Assess("Create nodes", func(ctx context.Context, t *testing.T, c *envconf.Config) context.Context { + ctx0, cancel := context.WithTimeout(ctx, 180*time.Second) + defer cancel() + + err := scaleCreateNodeWithHack(ctx0, t, kwokctlPath, clusterName, 5000) + if err != nil { + t.Fatal(err) + } + return ctx + }). + Assess("Create pods", func(ctx context.Context, t *testing.T, c *envconf.Config) context.Context { + ctx0, cancel := context.WithTimeout(ctx, 240*time.Second) + defer cancel() + + err := scaleCreatePodWithHack(ctx0, t, kwokctlPath, clusterName, 10000) + if err != nil { + t.Fatal(err) + } + return ctx + }). + Assess("Delete pods", func(ctx context.Context, t *testing.T, c *envconf.Config) context.Context { + ctx0, cancel := context.WithTimeout(ctx, 240*time.Second) + defer cancel() + + err := scaleDeletePod(ctx0, t, kwokctlPath, clusterName, 0) + if err != nil { + t.Fatal(err) + } + return ctx + }) +} diff --git a/test/e2e/kwokctl/benchmark-hack/kubectl_test.go b/test/e2e/kwokctl/benchmark-hack/kubectl_test.go new file mode 100644 index 000000000..67482ef80 --- /dev/null +++ b/test/e2e/kwokctl/benchmark-hack/kubectl_test.go @@ -0,0 +1,29 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package benchmark_hack_test + +import ( + "testing" + + "sigs.k8s.io/kwok/test/e2e" +) + +func TestBenchmarkWithHack(t *testing.T) { + f0 := e2e.CaseBenchmarkWithHack(kwokctlPath, clusterName). + Feature() + testEnv.Test(t, f0) +} diff --git a/test/e2e/kwokctl/benchmark-hack/main_test.go b/test/e2e/kwokctl/benchmark-hack/main_test.go new file mode 100644 index 000000000..e1702cd67 --- /dev/null +++ b/test/e2e/kwokctl/benchmark-hack/main_test.go @@ -0,0 +1,72 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package benchmark_hack_test is a test benchmarking environment for kwok. +package benchmark_hack_test + +import ( + "os" + "runtime" + "testing" + + "sigs.k8s.io/e2e-framework/pkg/env" + "sigs.k8s.io/e2e-framework/pkg/envconf" + "sigs.k8s.io/e2e-framework/support/kwok" + + "sigs.k8s.io/kwok/pkg/consts" + "sigs.k8s.io/kwok/pkg/utils/path" + "sigs.k8s.io/kwok/test/e2e/helper" +) + +var ( + runtimeEnv = consts.RuntimeTypeBinary + testEnv env.Environment + pwd = os.Getenv("PWD") + rootDir = path.Join(pwd, "../../../..") + logsDir = path.Join(rootDir, "logs") + clusterName = envconf.RandomName("kwok-e2e-benchmark-hack", 24) + kwokPath = path.Join(rootDir, "bin", runtime.GOOS, runtime.GOARCH, "kwok"+helper.BinSuffix) + kwokctlPath = path.Join(rootDir, "bin", runtime.GOOS, runtime.GOARCH, "kwokctl"+helper.BinSuffix) + baseArgs = []string{ + "--kwok-controller-binary=" + kwokPath, + "--runtime=" + runtimeEnv, + "--wait=15m", + "--disable-kube-scheduler", + "--disable-qps-limits", + } +) + +func init() { + _ = os.Setenv("KWOK_WORKDIR", path.Join(rootDir, "workdir")) +} + +func TestMain(m *testing.M) { + testEnv = helper.Environment() + + k := kwok.NewProvider(). + WithName(clusterName). + WithPath(kwokctlPath) + testEnv.Setup( + helper.BuildKwokBinary(rootDir), + helper.BuildKwokctlBinary(rootDir), + helper.CreateCluster(k, baseArgs...), + ) + testEnv.Finish( + helper.ExportLogs(k, logsDir), + helper.DestroyCluster(k), + ) + os.Exit(testEnv.Run(m)) +}