init + tmp

This commit is contained in:
r8zavetr8v 2024-09-08 01:40:03 +03:00
commit 5a1f062597
10 changed files with 339 additions and 0 deletions

57
README.md Normal file
View File

@ -0,0 +1,57 @@
# Gustav
Gustav (also known as [Schwerer Gustav](https://en.wikipedia.org/wiki/Schwerer_Gustav)) is a traffic and load generator for testing web applications.
TBD
## Configuration
load.pipeline.yaml:
```yaml
name: "My pipeline" # The name of pipeline
description: "Test pipeline"
steps: # Steps
step1:
# The preparatory phase of our pipeline. Here we can perform authorizations and other actions that must
# be performed BEFORE the main part.
prepare:
type: "http_call" # Call type.
expected_codes: [200] # Expected return codes
# The name of the variable in which the result of this call will be stored.
# This value will be accessible only in scope of current stop.
save_into: "prepare_step1_result"
# The name of the variable in which the result of this call will be stored.
# This value will be accessible globally.
global_save_into: "global_variable"
call: # Request configuration
target: "http://127.0.0.1:8080/login" # Endpoint
headers:
secret: "xxx"
logon-with: "password"
method: "POST"
body: |-
{
"login":"admin",
"password":"password"
}
repeat: 1000
# The main part of the step. The set of requests that will be executed.
# They will be looped until the repeat number reached
shells:
shell1:
type: "http_call"
call:
target: "http://127.0.0.1:8080/main"
headers:
secret: "xxx"
token: "${REGISTRY}.token" ## Accessing prepare step result from registry
method: "GET"
shell2:
type: "http_call"
call:
target: "http://127.0.0.1:8080/profile"
headers:
secret: "xxx"
token: "${REGISTRY}.token" ## Accessing prepare step result from registry
method: "GET"
# ...
```

5
cmd/main.go Normal file
View File

@ -0,0 +1,5 @@
package main
func main() {
}

8
go.mod Normal file
View File

@ -0,0 +1,8 @@
module git.optclblast.xyz/optclblast/gustav
go 1.23.0
require (
golang.org/x/sync v0.8.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

7
go.sum Normal file
View File

@ -0,0 +1,7 @@
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -0,0 +1,7 @@
package client
import "context"
type Client[InT, OutT any] interface {
Do(ctx context.Context, in InT) (OutT, error)
}

View File

@ -0,0 +1,4 @@
package config
type Config struct {
}

View File

@ -0,0 +1,106 @@
package pipeline
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"git.optclblast.xyz/optclblast/gustav/internal/client"
)
type ExecuteOpts func() // TODO
type HttpClient = client.Client[*http.Request, *http.Response]
func ExecutePipeline(ctx context.Context, cc HttpClient, pipeline *Pipeline, opts ...ExecuteOpts) error {
if pipeline == nil || pipeline.Steps == nil {
return fmt.Errorf("pipeline is empty")
}
globalRegistry := make(map[string]any)
slog.Info("Started executing pipeline", slog.String("name", pipeline.Name))
for i, s := range pipeline.Steps {
slog.Debug("Execuping step", slog.Int("number", i), slog.String("name", s.Name))
registry := make(map[string]any)
// the idea is to save response json just as a map, and then access data simply with path.to.data pattent
prepareResult, err := prepareShelling[map[string]any](ctx, cc, s.Prepare)
if err != nil {
panic(err) // TODO handle
}
if s.Prepare.GlobalSaveInto != "" {
globalRegistry[s.Prepare.GlobalSaveInto] = *prepareResult
}
if prepareResult != nil {
registry[s.Prepare.SaveInto] = *prepareResult
}
for i := 0; i < s.Repeat; i++ {
for _, sh := range s.Shells {
bodyData := bytes.NewBuffer(sh.Body)
req, err := http.NewRequestWithContext(ctx, sh.Method, sh.Target, bodyData)
if err != nil {
slog.Error("failed to create a new request", slog.String("error", err.Error()))
continue
}
_, err = cc.Do(ctx, req) // TODO assert expected result and the one we got
if err != nil {
slog.Error("failed to do prepare request", slog.String("error", err.Error()))
continue
}
}
}
}
return nil
}
func prepareShelling[OutT any](ctx context.Context, cc HttpClient, pre Prepare) (*OutT, error) {
const op = "prepare_shelling"
if pre.Type != "http_call" {
return nil, fmt.Errorf("[%s] unknown preshelling type", op)
}
bodyData := bytes.NewBuffer(pre.Call.Body)
req, err := http.NewRequestWithContext(ctx, pre.Call.Method, pre.Call.Target, bodyData)
if err != nil {
return nil, fmt.Errorf("[%s] failed to create a new request: %w", op, err)
}
resp, err := cc.Do(ctx, req)
if err != nil {
return nil, fmt.Errorf("[%s] failed to do prepare request: %w", op, err)
}
defer func() {
if err := resp.Body.Close(); err != nil {
slog.Error("failed to close response body", slog.String("op", op), slog.String("error", err.Error()))
}
}()
respBody, err := io.ReadAll(req.Body)
if err != nil {
return nil, fmt.Errorf("[%s] failed to read response body: %w", err)
}
out := new(OutT)
if err := json.Unmarshal(respBody, out); err != nil {
return nil, fmt.Errorf("[%s] failed to parse response: %w", op, err)
}
return out, nil
}

View File

@ -0,0 +1,41 @@
package pipeline
type Pipeline struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
Steps []Step `yaml:"steps"`
}
type Step struct {
// TODO
// DependsOn *Step
// AllowParallel bool
// Trigger Trigger
Name string `yaml:"name"`
RateOfFire int `yaml:"rate_of_fire"`
Prepare Prepare `yaml:"prepare"`
Repeat int `yaml:"repeat"`
Shells []*Shell `yaml:"shells"`
}
type Prepare struct {
Type string `yaml:"type"` // http_call
Call Call `yaml:"call"` // TODO may be pretier
ReturnType string `yaml:"return_type"` // What is expected
ExpectedCodes []int `yaml:"expected_codes"`
SaveInto string `yaml:"save_into"` // Save the result into register X
GlobalSaveInto string `yaml:"global_save_into"`
}
type Shell struct {
Name string `yaml:"name"`
Call
// Onfail callback
}
type Call struct {
Target string `yaml:"traget"`
Headers map[string]string `yaml:"headers"`
Method string `yaml:"method"`
Body []byte `yaml:"body"`
}

View File

@ -0,0 +1,73 @@
package pipeline
import (
"context"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"runtime"
"strings"
"golang.org/x/sync/errgroup"
"gopkg.in/yaml.v3"
)
func FindPipelines(path string) []string {
_, b, _, _ := runtime.Caller(0)
root := filepath.Join(filepath.Dir(b), "../..")
pipelinesPaths := []string{}
filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if strings.Contains(d.Name(), "pipeline.yaml") || strings.Contains(d.Name(), "pipeline.yml") {
pipelinesPaths = append(pipelinesPaths, path)
}
return nil
})
return pipelinesPaths
}
func LoadPipelines(ctx context.Context, paths []string) ([]*Pipeline, error) {
pipelines := make([]*Pipeline, len(paths))
eg, ctx := errgroup.WithContext(ctx)
for i, p := range paths {
i := i
eg.Go(func() error {
f, err := os.Open(p)
if err != nil {
return fmt.Errorf("failed to read pipeline file (%s): %w", p, err)
}
data, err := io.ReadAll(f)
if err != nil {
return fmt.Errorf("failed to read pipeline config (%s): %w", p, err)
}
pipeline := new(Pipeline)
if err := yaml.Unmarshal(data, pipeline); err != nil {
return fmt.Errorf("failed to unmarshall pipeline file (%s): %w", p, err)
}
pipelines[i] = pipeline
return nil
})
}
if err := eg.Wait(); err != nil {
return nil, err
}
return pipelines, nil
}

31
pipeline.yaml Normal file
View File

@ -0,0 +1,31 @@
name: "Test"
description: "Test pipeline"
steps:
step1:
prepare:
type: "http_call"
expected_codes: [200]
save_into: "prepare_step1_result"
global_save_into: "global_variable"
call:
target: "http://127.0.0.1:8080/login"
headers:
secret: "xxx"
logon-with: "password"
method: "POST"
body: |-
{
"login":"admin",
"password":"password"
}
repeat: 1000
shells:
shell1:
type: "http_call"
call:
target: "http://127.0.0.1:8080/main"
headers:
secret: "xxx"
token: "${REGISTRY}.token" ## Accessing prepare result from registry
method: "GET"