Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[QG][KIM] Application that generates loads by creating or deleting Runtime CR #421

Merged
merged 11 commits into from
Oct 21, 2024
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
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if a user provides anything different than "false" ex "FALSE" or "aaaaa" the code under if statement will not be executed.
I suggest:

  1. use flag.Bool type for this parameter runOnCi
  2. Change this code into:
    if *runOnCi == false {
    }

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error value is not handled

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
Loading