init + tmp
This commit is contained in:
commit
5a1f062597
57
README.md
Normal file
57
README.md
Normal 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
5
cmd/main.go
Normal file
@ -0,0 +1,5 @@
|
||||
package main
|
||||
|
||||
func main() {
|
||||
|
||||
}
|
8
go.mod
Normal file
8
go.mod
Normal 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
7
go.sum
Normal 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=
|
7
internal/client/client.go
Normal file
7
internal/client/client.go
Normal file
@ -0,0 +1,7 @@
|
||||
package client
|
||||
|
||||
import "context"
|
||||
|
||||
type Client[InT, OutT any] interface {
|
||||
Do(ctx context.Context, in InT) (OutT, error)
|
||||
}
|
4
internal/config/config.go
Normal file
4
internal/config/config.go
Normal file
@ -0,0 +1,4 @@
|
||||
package config
|
||||
|
||||
type Config struct {
|
||||
}
|
106
internal/pipeline/executor.go
Normal file
106
internal/pipeline/executor.go
Normal 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
|
||||
}
|
41
internal/pipeline/pipeline.go
Normal file
41
internal/pipeline/pipeline.go
Normal 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"`
|
||||
}
|
73
internal/pipeline/reader.go
Normal file
73
internal/pipeline/reader.go
Normal 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
31
pipeline.yaml
Normal 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"
|
Loading…
Reference in New Issue
Block a user