From 5a1f06259717e2968ebdaf793c73f81ab9065fc3 Mon Sep 17 00:00:00 2001 From: optclblast Date: Sun, 8 Sep 2024 01:40:03 +0300 Subject: [PATCH] init + tmp --- README.md | 57 ++++++++++++++++++ cmd/main.go | 5 ++ go.mod | 8 +++ go.sum | 7 +++ internal/client/client.go | 7 +++ internal/config/config.go | 4 ++ internal/pipeline/executor.go | 106 ++++++++++++++++++++++++++++++++++ internal/pipeline/pipeline.go | 41 +++++++++++++ internal/pipeline/reader.go | 73 +++++++++++++++++++++++ pipeline.yaml | 31 ++++++++++ 10 files changed, 339 insertions(+) create mode 100644 README.md create mode 100644 cmd/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/client/client.go create mode 100644 internal/config/config.go create mode 100644 internal/pipeline/executor.go create mode 100644 internal/pipeline/pipeline.go create mode 100644 internal/pipeline/reader.go create mode 100644 pipeline.yaml diff --git a/README.md b/README.md new file mode 100644 index 0000000..d7bc544 --- /dev/null +++ b/README.md @@ -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" + # ... +``` \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..7905807 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,5 @@ +package main + +func main() { + +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b32419e --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f0318be --- /dev/null +++ b/go.sum @@ -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= diff --git a/internal/client/client.go b/internal/client/client.go new file mode 100644 index 0000000..738e148 --- /dev/null +++ b/internal/client/client.go @@ -0,0 +1,7 @@ +package client + +import "context" + +type Client[InT, OutT any] interface { + Do(ctx context.Context, in InT) (OutT, error) +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..809bc99 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,4 @@ +package config + +type Config struct { +} diff --git a/internal/pipeline/executor.go b/internal/pipeline/executor.go new file mode 100644 index 0000000..7af79fc --- /dev/null +++ b/internal/pipeline/executor.go @@ -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 +} diff --git a/internal/pipeline/pipeline.go b/internal/pipeline/pipeline.go new file mode 100644 index 0000000..1f609f3 --- /dev/null +++ b/internal/pipeline/pipeline.go @@ -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"` +} diff --git a/internal/pipeline/reader.go b/internal/pipeline/reader.go new file mode 100644 index 0000000..0ed7eca --- /dev/null +++ b/internal/pipeline/reader.go @@ -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 +} diff --git a/pipeline.yaml b/pipeline.yaml new file mode 100644 index 0000000..7aafa34 --- /dev/null +++ b/pipeline.yaml @@ -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" \ No newline at end of file