From 0fee410533fe10ab0a559e63318522d389ef8919 Mon Sep 17 00:00:00 2001 From: Wei Fu Date: Thu, 30 Nov 2023 08:03:19 +0000 Subject: [PATCH] init layout of codebase Signed-off-by: Wei Fu --- .gitignore | 1 + .golangci.yml | 14 +++ Makefile | 29 ++++++ api/types/load_traffic.go | 91 +++++++++++++++++ api/types/load_traffic_test.go | 94 ++++++++++++++++++ cmd/kperf/commands/multirunners/runner.go | 113 ++++++++++++++++++++++ cmd/kperf/commands/root.go | 20 ++++ cmd/kperf/commands/runner/runner.go | 40 ++++++++ cmd/kperf/main.go | 16 +++ go.mod | 14 +++ go.sum | 26 +++++ random.go | 6 ++ 12 files changed, 464 insertions(+) create mode 100644 .golangci.yml create mode 100644 Makefile create mode 100644 api/types/load_traffic.go create mode 100644 api/types/load_traffic_test.go create mode 100644 cmd/kperf/commands/multirunners/runner.go create mode 100644 cmd/kperf/commands/root.go create mode 100644 cmd/kperf/commands/runner/runner.go create mode 100644 cmd/kperf/main.go create mode 100644 go.sum create mode 100644 random.go diff --git a/.gitignore b/.gitignore index 3b735ec..0bfa83c 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out +bin/ # Dependency directories (remove the comment below to include it) # vendor/ diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..120a8b6 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,14 @@ +linters: + enable: + - gofmt + - goimports + - gosec + - ineffassign + - misspell + - nolintlint + - revive + - staticcheck + - unconvert + - unused + - vet + - errcheck diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8c0049b --- /dev/null +++ b/Makefile @@ -0,0 +1,29 @@ +COMMANDS=kperf + +BINARIES=$(addprefix bin/,$(COMMANDS)) + +# default recipe is build +.DEFAULT_GOAL := build + +# Always build +ALWAYS: + +bin/%: cmd/% ALWAYS + @go build -o $@ ./$< + +build: $(BINARIES) ## build binaries + @echo "$@" + +test: ## run test + @go test -v ./... + +lint: ## run lint + @golangci-lint run --config .golangci.yml + +.PHONY: clean +clean: ## clean up binaries + @rm -f $(BINARIES) + +.PHONY: help +help: ## this help + @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-36s\033[0m%s\n", $$1, $$2}' $(MAKEFILE_LIST) diff --git a/api/types/load_traffic.go b/api/types/load_traffic.go new file mode 100644 index 0000000..a395c3a --- /dev/null +++ b/api/types/load_traffic.go @@ -0,0 +1,91 @@ +package types + +// LoadProfile defines how to create load traffic from one host to kube-apiserver. +type LoadProfile struct { + // Version defines the version of this object. + Version int `json:"version" yaml:"version"` + // Description is a string value to describe this object. + Description string `json:"description,omitempty" yaml:"description"` + // Spec defines behavior of load profile. + Spec LoadProfileSpec `json:"spec" yaml:"spec"` +} + +// LoadProfileSpec defines the load traffic for traget resource. +type LoadProfileSpec struct { + // Rate defines the maximum requests per second (zero is no limit). + Rate int `json:"rate" yaml:"rate"` + // Total defines the total number of requests. + Total int `json:"total" yaml:"total"` + // Conns defines total number of long connections used for traffic. + Conns int `json:"conns" yaml:"conns"` + // Requests defines the different kinds of requests with weights. + // The executor should randomly pick by weight. + Requests []*WeightedRequest +} + +// KubeTypeMeta represents metadata of kubernetes object. +type KubeTypeMeta struct { + // Kind is a string value representing the REST resource the object represents. + Kind string `json:"kind" yaml:"kind"` + // APIVersion defines the versioned schema of the representation of an object. + APIVersion string `json:"apiVersion" yaml:"apiVersion"` +} + +// WeightedRequest represents request with weight. +// Only one of request types may be specified. +type WeightedRequest struct { + // Shares defines weight in the same group. + Shares int `json:"shares" yaml:"shares"` + // StaleList means this list request with zero resource version. + StaleList *RequestList `json:"staleList" yaml:"staleList"` + // QuorumList means this list request without kube-apiserver cache. + QuorumList *RequestList `json:"quorumList" yaml:"quorumList"` + // StaleGet means this get request with zero resource version. + StaleGet *RequestGet `json:"staleGet" yaml:"staleGet"` + // QuorumGet means this get request without kube-apiserver cache. + QuorumGet *RequestGet `json:"quorumGet" yaml:"quorumGet"` + // Put means this is mutating request. + Put *RequestPut `json:"put" yaml:"put"` +} + +// RequestGet defines GET request for target object. +type RequestGet struct { + // KubeTypeMeta represents object's resource type. + KubeTypeMeta `yaml:",inline"` + // Namespace is object's namespace. + Namespace string `json:"namespace" yaml:"namespace"` + // Name is object's name. + Name string `json:"name" yaml:"name"` +} + +// RequestList defines LIST request for target objects. +type RequestList struct { + // KubeTypeMeta represents object's resource type. + KubeTypeMeta `yaml:",inline"` + // Namespace is object's namespace. + Namespace string `json:"namespace" yaml:"namespace"` + // Limit defines the page size. + Limit int `json:"limit" yaml:"limit"` + // Selector defines how to identify a set of objects. + Selector string `json:"seletor" yaml:"seletor"` +} + +// RequestPut defines PUT request for target resource type. +type RequestPut struct { + // KubeTypeMeta represents object's resource type. + // + // 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. + KubeTypeMeta `yaml:",inline"` + // Namespace is object's namespace. + Namespace string `json:"namespace" yaml:"namespace"` + // Name is object's prefix name. + 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"` +} diff --git a/api/types/load_traffic_test.go b/api/types/load_traffic_test.go new file mode 100644 index 0000000..16ea503 --- /dev/null +++ b/api/types/load_traffic_test.go @@ -0,0 +1,94 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" +) + +func TestLoadProfileUnmarshalFromYAML(t *testing.T) { + in := ` +version: 1 +description: test +spec: + rate: 100 + total: 10000 + conns: 2 + requests: + - staleGet: + kind: pods + apiVersion: v1 + namespace: default + name: x1 + shares: 100 + - quorumGet: + kind: configmap + apiVersion: v1 + namespace: default + name: x2 + shares: 150 + - staleList: + kind: pods + apiVersion: v1 + namespace: default + limit: 10000 + seletor: app=x2 + shares: 200 + - quorumList: + kind: configmap + apiVersion: v1 + namespace: default + limit: 10000 + seletor: app=x3 + shares: 400 + - put: + kind: configmap + apiVersion: v1 + namespace: kperf + name: kperf- + keySpaceSize: 1000 + valueSize: 1024 + shares: 1000 +` + + target := LoadProfile{} + require.NoError(t, yaml.Unmarshal([]byte(in), &target)) + assert.Equal(t, 1, target.Version) + assert.Equal(t, "test", target.Description) + assert.Equal(t, 100, target.Spec.Rate) + assert.Equal(t, 10000, target.Spec.Total) + assert.Equal(t, 2, target.Spec.Conns) + assert.Len(t, target.Spec.Requests, 5) + + assert.Equal(t, 100, target.Spec.Requests[0].Shares) + assert.NotNil(t, target.Spec.Requests[0].StaleGet) + assert.Equal(t, "pods", target.Spec.Requests[0].StaleGet.Kind) + assert.Equal(t, "v1", target.Spec.Requests[0].StaleGet.APIVersion) + assert.Equal(t, "default", target.Spec.Requests[0].StaleGet.Namespace) + assert.Equal(t, "x1", target.Spec.Requests[0].StaleGet.Name) + + assert.NotNil(t, target.Spec.Requests[1].QuorumGet) + assert.Equal(t, 150, target.Spec.Requests[1].Shares) + + assert.Equal(t, 200, target.Spec.Requests[2].Shares) + assert.NotNil(t, target.Spec.Requests[2].StaleList) + assert.Equal(t, "pods", target.Spec.Requests[2].StaleList.Kind) + assert.Equal(t, "v1", target.Spec.Requests[2].StaleList.APIVersion) + assert.Equal(t, "default", target.Spec.Requests[2].StaleList.Namespace) + assert.Equal(t, 10000, target.Spec.Requests[2].StaleList.Limit) + assert.Equal(t, "app=x2", target.Spec.Requests[2].StaleList.Selector) + + assert.NotNil(t, target.Spec.Requests[3].QuorumList) + assert.Equal(t, 400, target.Spec.Requests[3].Shares) + + assert.Equal(t, 1000, target.Spec.Requests[4].Shares) + assert.NotNil(t, target.Spec.Requests[4].Put) + assert.Equal(t, "configmap", target.Spec.Requests[4].Put.Kind) + assert.Equal(t, "v1", target.Spec.Requests[4].Put.APIVersion) + assert.Equal(t, "kperf", target.Spec.Requests[4].Put.Namespace) + assert.Equal(t, "kperf-", target.Spec.Requests[4].Put.Name) + assert.Equal(t, 1000, target.Spec.Requests[4].Put.KeySpaceSize) + assert.Equal(t, 1024, target.Spec.Requests[4].Put.ValueSize) +} diff --git a/cmd/kperf/commands/multirunners/runner.go b/cmd/kperf/commands/multirunners/runner.go new file mode 100644 index 0000000..72cda58 --- /dev/null +++ b/cmd/kperf/commands/multirunners/runner.go @@ -0,0 +1,113 @@ +package multirunners + +import ( + "fmt" + + "github.com/urfave/cli" +) + +// Command represents multirunners sub-command. +// +// Subcommand multirunners is to deploy multiple runners as kubernetes jobs. +// Since one runner could run out of networking bandwidth on one host, the +// multirunners deploys runners on different hosts and reduces the impact of +// limited networking resource. +// +// Command line interface: +// +// kperf mrunners run --help +// +// Options: +// +// --kubeconfig PATH (default: empty_string, use token if it's empty) +// --namespace STRING (default: empty_string, required) +// --runner-image STRING (default: empty_string, required) +// --runners []STRING (default: empty, required) +// --wait BOOLEAN (default: false) +// +// Details: +// +// The --runners format is defined by URI. +// +// - file:///abs_path?numbers=10 +// - configmap:///namespace/name?numbers=2 +// - ... +// +// The schema:://PATH is used to get runner's configuration. It can be local +// path or stored as configmap in target kubernetes cluster. The query part is +// to define what the job looks like. Currently, that command just requires +// the number of pods in that job. At the beginning, we just need to file://. +// The number of runners defines the number of jobs. +// +// All the jobs are referenced by one configmap (ownerReference). The configmap +// name will be output to stdout. The name is progress tracker ID. By default, +// there is only one progress tracker ID in one namespace. +// +// kperf mrunners wait --help +// +// Args: +// +// 0: namespace (STRING) +// +// Options: +// +// --kubeconfig PATH (default: empty_string, use token if it's empty) +// +// Wait it to wait until jobs finish. +// +// kperf mrunners result --help +// +// Args: +// +// 0: namespace (STRING) +// +// Options: +// +// --kubeconfig PATH (default: empty_string, use token if it's empty) +// +// Result retrieves the result for jobs. If jobs is still running, that command +// will fail. +var Command = cli.Command{ + Name: "multirunners", + ShortName: "mrunners", + Usage: "packages runner as job and deploy runners into kubernetes", + Subcommands: []cli.Command{ + runCommand, + waitCommand, + resultCommand, + }, +} + +var runCommand = cli.Command{ + Name: "run", + Flags: []cli.Flag{}, + Action: func(cliCtx *cli.Context) error { + // 1. Parse options + // 2. Deploy jobs for --runners + // 3. Wait + return fmt.Errorf("run - not implemented") + }, +} + +var waitCommand = cli.Command{ + Name: "wait", + Usage: "wait until jobs finish", + Flags: []cli.Flag{}, + Action: func(cliCtx *cli.Context) error { + // 1. Check the progress tracker name + // 2. Wait for the jobs + return fmt.Errorf("wait - not implemented") + }, +} + +var resultCommand = cli.Command{ + Name: "result", + Usage: "show the result", + Flags: []cli.Flag{}, + Action: func(cliCtx *cli.Context) error { + // 1. Check the progress tracker name + // 2. Ensure the jobs finished + // 3. Output the result + return fmt.Errorf("result - not implemented") + }, +} diff --git a/cmd/kperf/commands/root.go b/cmd/kperf/commands/root.go new file mode 100644 index 0000000..6bac055 --- /dev/null +++ b/cmd/kperf/commands/root.go @@ -0,0 +1,20 @@ +package commands + +import ( + "github.com/Azure/kperf/cmd/kperf/commands/multirunners" + "github.com/Azure/kperf/cmd/kperf/commands/runner" + + "github.com/urfave/cli" +) + +// App returns kperf application. +func App() *cli.App { + return &cli.App{ + Name: "kperf", + // TODO: add more fields + Commands: []cli.Command{ + runner.Command, + multirunners.Command, + }, + } +} diff --git a/cmd/kperf/commands/runner/runner.go b/cmd/kperf/commands/runner/runner.go new file mode 100644 index 0000000..72ab406 --- /dev/null +++ b/cmd/kperf/commands/runner/runner.go @@ -0,0 +1,40 @@ +package runner + +import ( + "fmt" + + "github.com/urfave/cli" +) + +// Command represents runner sub-command. +// +// Subcommand runner is to create request load to apiserver. +// +// NOTE: It can work with subcommand multirunners. The multirunners subcommand +// will deploy subcommand runner in pod. Details in ../multirunners. +// +// Command line interface: +// +// kperf runner --help +// +// Options: +// +// --kubeconfig PATH (default: empty_string, use token if it's empty) +// --load-config PATH (default: empty_string, required, the config defined in api/types/load_traffic.go) +// --conns INT (default: 1, Total number of connections. It can override corresponding value defined by --load-config) +// --rate INT (default: 0, Maximum requests per second. It can override corresponding value defined by --load-config) +// --total INT (default: 1000, Total number of request. It can override corresponding value defined by --load-config) +var Command = cli.Command{ + Name: "runner", + Usage: "run a load test to kube-apiserver", + Flags: []cli.Flag{}, + Action: func(cliCtx *cli.Context) error { + // 1. Parse options + // 2. Setup producer-consumer goroutines + // 2.1 Use go limter to generate request + // 2.2 Use client-go's client to file requests + // 3. Build progress tracker to track failure number and P99/P95/P90 latencies. + // 4. Export summary in stdout. + return fmt.Errorf("runner - not implemented") + }, +} diff --git a/cmd/kperf/main.go b/cmd/kperf/main.go new file mode 100644 index 0000000..5ffb318 --- /dev/null +++ b/cmd/kperf/main.go @@ -0,0 +1,16 @@ +package main + +import ( + "fmt" + "os" + + "github.com/Azure/kperf/cmd/kperf/commands" +) + +func main() { + app := commands.App() + if err := app.Run(os.Args); err != nil { + fmt.Fprintf(os.Stderr, "%s: %v\n", app.Name, err) + os.Exit(1) + } +} diff --git a/go.mod b/go.mod index b4c875b..11cb7c0 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,17 @@ module github.com/Azure/kperf go 1.20 + +require ( + github.com/stretchr/testify v1.8.4 + github.com/urfave/cli v1.22.14 + gopkg.in/yaml.v2 v2.4.0 +) + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2e3b51b --- /dev/null +++ b/go.sum @@ -0,0 +1,26 @@ +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= +github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/random.go b/random.go new file mode 100644 index 0000000..b5807f9 --- /dev/null +++ b/random.go @@ -0,0 +1,6 @@ +package kperf + +// WeightedRandomPick returns index randomly based on weights. +func WeightedRandomPick(_ []int) (_index int) { + panic("not implemented") +}