Skip to content

Commit

Permalink
Merge pull request #421 from mvshao/qg-load-test
Browse files Browse the repository at this point in the history
[QG][KIM] Application that generates loads by creating or deleting Runtime CR
  • Loading branch information
mvshao authored Oct 21, 2024
2 parents e7f8174 + 5419233 commit 73b9c8b
Show file tree
Hide file tree
Showing 6 changed files with 577 additions and 0 deletions.
78 changes: 78 additions & 0 deletions hack/performance/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Runtime Load Generator

## Overview

The `rt-load` program generates performance test loads by creating or deleting runtime resources in a Kubernetes cluster. All of the runtime custom resources created by this program are linked to the same Gardener cluster.

## Building the Binary

To build the `rt-load` binary from the `main.go` file, follow these steps:

1. Open a terminal and navigate to the directory containing the `main.go` file.
2. Run the following command to build the binary:

```sh
go build -o rt-load main.go
```

## Usage

```sh
rt-load <command> [options]
```

## Commands

### `create`

Creates a specified number of runtime resources.

#### Options

- `--load-id <STRING>`: The identifier (label) of the created load (**required**).
- `--name-prefix <STRING>`: The prefix used to generate each runtime name (**required**).
- `--kubeconfig <STRING>`: The path to the Kubeconfig file (**required**).
- `--rt-number <INT>`: The number of the runtimes to be created (**required**).
- `--rt-template <STRING>`: The absolute path to the YAML file with the runtime template (**required**).
- `--run-on-ci <BOOL>`: Identifies if the load is running on CI (**optional**, default is `false`).

#### Example

```sh
./rt-load create --load-id my-load --name-prefix my-runtime --kubeconfig /path/to/kubeconfig --rt-number 10 --rt-template /path/to/template.yaml
```

### `delete`

Deletes runtime resources based on the specified load ID.

#### Options

- `--load-id <LOAD-ID>`: The identifier of the created load (**required**).
- `--kubeconfig <FILE>`: The path to the Kubeconfig file (**required**).

#### Example

```sh
./rt-load delete --load-id my-load --kubeconfig /path/to/kubeconfig
```

## Running on CI

When running the `rt-load` program in a Continuous Integration (CI) environment, you can use the `--run-on-ci` option to bypass interactive prompts. This is useful for automated CI/CD pipelines where user interaction is not possible.

### Example

To create runtime resources in a CI environment, use the following command:

```sh
./rt-load create --load-id my-load --name-prefix my-runtime --kubeconfig /path/to/kubeconfig --rt-number 10 --rt-template /path/to/template.yaml --run-on-ci true
```

In this example, the `--run-on-ci` option is set to `true`, which ensures that the program runs without requiring any user input.

## Notes

- Ensure that the `kubeconfig` file points to the correct Kubernetes cluster where the runtime resources are to be created or deleted.
- The `--load-id ` will be included as a value of the label `kim.performance.loadId` on the Runtime CR that was created by this program.
- The `--run-on-ci` option is useful for automated CI/CD pipelines to bypass interactive prompts.
113 changes: 113 additions & 0 deletions hack/performance/action/worker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package action

import (
"context"
"math/rand"

imv1 "github.com/kyma-project/infrastructure-manager/api/v1"
"k8s.io/client-go/tools/clientcmd"
"sigs.k8s.io/controller-runtime/pkg/client"
)

type Worker interface {
Create() error
Delete() error
}

type WorkerData struct {
loadID string
namePrefix string
rtNumber int
k8sClient client.Client
rtTemplate imv1.Runtime
}

func NewWorker(loadID, namePrefix, kubeconfigPath string, rtNumber int, rtTemplate imv1.Runtime) (Worker, error) {
config, err := clientcmd.BuildConfigFromFlags("", kubeconfigPath)
if err != nil {
return nil, err
}

k8sClient, err := client.New(config, client.Options{})
if err != nil {
return nil, err
}

err = imv1.AddToScheme(k8sClient.Scheme())
if err != nil {
return nil, err
}

return &WorkerData{
loadID: loadID,
namePrefix: namePrefix,
rtNumber: rtNumber,
k8sClient: k8sClient,
rtTemplate: rtTemplate,
}, nil
}

func (w WorkerData) Create() error {
runtimes := w.prepareRuntimeBatch()

for i := 0; i < w.rtNumber; i++ {
err := w.k8sClient.Create(context.Background(), &runtimes.Items[i])
if err != nil {
return err
}
}

return nil
}

func (w WorkerData) Delete() error {
runtimes, err := w.deleteRuntimeBatch()
if err != nil {
return err
}
for _, item := range runtimes.Items {
err = w.k8sClient.Delete(context.Background(), &item)
if err != nil {
return err
}
}
return nil
}

func (w WorkerData) prepareRuntimeBatch() imv1.RuntimeList {

baseRuntime := w.rtTemplate.DeepCopy()
baseRuntime.Name = ""
baseRuntime.GenerateName = w.namePrefix + "-"
baseRuntime.Labels["kim.performance.loadId"] = w.loadID

if baseRuntime.Spec.Shoot.Name == "" {
baseRuntime.Spec.Shoot.Name = generateRandomName(7) + "-" + w.loadID
}

runtimeBatch := imv1.RuntimeList{}

for i := 0; i < w.rtNumber; i++ {
runtimeBatch.Items = append(runtimeBatch.Items, *baseRuntime)
}

return runtimeBatch
}

func (w WorkerData) deleteRuntimeBatch() (imv1.RuntimeList, error) {
runtimesToDelete := imv1.RuntimeList{}
err := w.k8sClient.List(context.Background(), &runtimesToDelete, client.MatchingLabels{"kim.performance.loadId": w.loadID})
if err != nil {
return imv1.RuntimeList{}, err
}
return runtimesToDelete, nil
}

func generateRandomName(count int) string {
letterRunes := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
runes := make([]rune, count)
for i := range runes {
runes[i] = letterRunes[rand.Intn(len(letterRunes))]
}
return string(runes)
}
118 changes: 118 additions & 0 deletions hack/performance/cmd/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package cmd

import (
"flag"
"fmt"
"io"
"os"

imv1 "github.com/kyma-project/infrastructure-manager/api/v1"
"github.com/kyma-project/infrastructure-manager/hack/performance/action"
"k8s.io/apimachinery/pkg/util/yaml"
)

type OperationType int

const (
Create OperationType = iota
Delete
Unknown
)

func Execute() (OperationType, action.Worker, error) {
var parsedRuntime imv1.Runtime

createCmd := flag.NewFlagSet("create", flag.ExitOnError)
deleteCmd := flag.NewFlagSet("delete", flag.ExitOnError)

loadID := createCmd.String("load-id", "", "the identifier (label) of the created load (required)")
namePrefix := createCmd.String("name-prefix", "", "the prefix used to generate each runtime name (required)")
kubeconfig := createCmd.String("kubeconfig", "", "the path to the kubeconfig file (required)")
rtNumber := createCmd.Int("rt-number", 0, "the number of the runtimes to be created (required)")
templatePath := createCmd.String("rt-template", "", "the path to the yaml file with the runtime template (required)")
runOnCi := createCmd.Bool("run-on-ci", false, "identifies if the load is running on CI")

loadIDDelete := deleteCmd.String("load-id", "", "the identifier (label) of the created load (required)")
kubeconfigDelete := deleteCmd.String("kubeconfig", "", "the path to the kubeconfig file (required)")

if len(os.Args) < 2 {
fmt.Println("expected 'create' or 'delete' subcommands")
os.Exit(1)
}

switch os.Args[1] {
case "create":
createCmd.Parse(os.Args[2:])
if *loadID == "" || *namePrefix == "" || *kubeconfig == "" || *rtNumber == 0 || *templatePath == "" {
fmt.Println("all flags --load-id, --name-prefix, --kubeconfig, --template-path and --rt-number are required")
createCmd.Usage()
os.Exit(1)
}

file, err := os.Open(*templatePath)
if err != nil {
fmt.Fprintln(os.Stderr, "error opening file:", err)
return Unknown, nil, err
}
defer func(file *os.File) {
err = file.Close()
if err != nil {
fmt.Fprintln(os.Stderr, "error closing file:", err)
}
}(file)
parsedRuntime, err = readFromSource(file)
if err != nil {
return Unknown, nil, err
}

if *runOnCi == false {
var response string
fmt.Printf("Do you want to create %d runtimes? [y/n]: ", *rtNumber)
fmt.Scanln(&response)
if response != "y" {
fmt.Println("Operation cancelled.")
os.Exit(1)
}
}
fmt.Printf("Creating load with ID: %s, Name Prefix: %s, Kubeconfig: %s, Runtime Number: %d\n", *loadID, *namePrefix, *kubeconfig, *rtNumber)
worker, err := action.NewWorker(*loadID, *namePrefix, *kubeconfig, *rtNumber, parsedRuntime)
return Create, worker, err
case "delete":
deleteCmd.Parse(os.Args[2:])
if *loadIDDelete == "" || *kubeconfigDelete == "" {
fmt.Println("all flags --load-id and --kubeconfig are required")
deleteCmd.Usage()
os.Exit(1)
}
fmt.Printf("Deleting load with ID: %s, Kubeconfig: %s\n", *loadIDDelete, *kubeconfigDelete)
worker, err := action.NewWorker(*loadIDDelete, "", *kubeconfigDelete, 0, imv1.Runtime{})
return Delete, worker, err
default:
fmt.Println("expected 'create' or 'delete' subcommands")
os.Exit(1)
}
return Unknown, nil, nil
}

func readFromSource(reader io.Reader) (imv1.Runtime, error) {
data, err := io.ReadAll(reader)
if err != nil {
fmt.Fprintln(os.Stderr, "error reading file:", err)
return imv1.Runtime{}, err
}
runtime, err := parseInputToRuntime(data)
if err != nil {
fmt.Fprintln(os.Stderr, "error parsing input:", err)
return imv1.Runtime{}, err
}
return runtime, nil
}

func parseInputToRuntime(data []byte) (imv1.Runtime, error) {
runtime := imv1.Runtime{}
err := yaml.Unmarshal(data, &runtime)
if err != nil {
return imv1.Runtime{}, err
}
return runtime, nil
}
56 changes: 56 additions & 0 deletions hack/performance/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
module github.com/kyma-project/infrastructure-manager/hack/performance

go 1.23.1

require (
github.com/kyma-project/infrastructure-manager v0.0.0-20241010165136-c9d296aadebd
k8s.io/apimachinery v0.31.0
k8s.io/client-go v0.31.0
sigs.k8s.io/controller-runtime v0.19.0
)

require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/gardener/gardener v1.100.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/x448/float16 v0.8.4 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/term v0.23.0 // indirect
golang.org/x/text v0.17.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.31.0 // indirect
k8s.io/apiextensions-apiserver v0.31.0 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)
Loading

0 comments on commit 73b9c8b

Please sign in to comment.