diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2ba40eb..e570442 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,10 +1,9 @@ name: goreleaser on: - pull_request: push: tags: - - "*" + - "v*" permissions: contents: write diff --git a/cmd/attack.go b/cmd/attack.go deleted file mode 100644 index e5dd144..0000000 --- a/cmd/attack.go +++ /dev/null @@ -1,118 +0,0 @@ -// MIT License - -// Copyright (c) 2023 Yamasaki Shotaro - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package cmd - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "time" - - "github.com/seipan/bulma/lib" - vegeta "github.com/tsenart/vegeta/lib" -) - -func ParseAndAttack(ctx context.Context, ignore []string, beseEndpoint string, path string, freq int, duration time.Duration) error { - oapi := lib.NewOpenAPI(path) - paths, err := oapi.Parse(ctx) - if err != nil { - return fmt.Errorf("failed to parse openapi: %w", err) - } - - ignores := lib.NewIgnore(ignore) - - atks, err := ParthOpenAPItoAttacker(paths, ignores, beseEndpoint, freq, duration) - if err != nil { - return fmt.Errorf("failed to convert openapi to attacker: %w", err) - } - fmt.Println("--------------------------bulma attack start-------------------------------") - for _, atk := range atks { - metrics := atk.Attack() - outputMetrics(metrics, &atk) - } - fmt.Println("--------------------------bulma attack finish-------------------------------") - return nil -} - -func ParthOpenAPItoAttacker(pathes []lib.Path, ignores lib.Ignore, beseEndpoint string, freq int, duration time.Duration) ([]lib.Attacker, error) { - var res []lib.Attacker - for i, path := range pathes { - if ignores.IsIgnored(path.Path()) { - continue - } - - mtd := path.Method(0) - bodys := mtd.Bodys() - body, err := createBody(bodys) - if err != nil { - return nil, err - } - path.SetPath(beseEndpoint + path.Path()) - atk := lib.Attacker{ - Path: path, - MethodIndex: i, - Body: body, - Header: http.Header{ - "Content-Type": []string{"application/json"}, - }, - Frequency: freq, - Duration: duration, - } - res = append(res, atk) - } - return res, nil -} - -func createBody(bodys []lib.Body) ([]byte, error) { - mp := make(map[string]interface{}) - for _, body := range bodys { - mp[body.Name] = body.Shema.Value.Example - } - jsonData, err := json.Marshal(mp) - if err != nil { - return nil, err - } - return jsonData, nil -} - -func outputMetrics(metrics vegeta.Metrics, atk *lib.Attacker) { - fmt.Printf("--------------------------vegeta attack to %s--------------------------\n", atk.Path.Path()) - mtd := atk.Path.Method(atk.MethodIndex) - fmt.Printf("vegeta attack to method: %s\n", mtd.Method()) - fmt.Printf("path StatusCode: %v\n", metrics.StatusCodes) - - fmt.Println() - - fmt.Printf("max percentile: %s\n", metrics.Latencies.Max) - fmt.Printf("mean percentile: %s\n", metrics.Latencies.Mean) - fmt.Printf("total percentile: %s\n", metrics.Latencies.Total) - fmt.Printf("99th percentile: %s\n", metrics.Latencies.P99) - - fmt.Println() - - fmt.Printf(" earliest: %v\n", metrics.Earliest) - fmt.Printf(" latest: %v\n", metrics.Latest) - - fmt.Println("-----------------------------------------------------------------------") -} diff --git a/cmd/root.go b/cmd/root.go index 91a0ee8..8c32a4d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -26,6 +26,7 @@ import ( "log" "os" + "github.com/seipan/bulma/lib" "github.com/spf13/cobra" ) @@ -54,7 +55,7 @@ var rootCmd = &cobra.Command{ if err != nil { log.Println(err) } - err = ParseAndAttack(cmd.Context(), ignore, base, path, freq, duration) + err = lib.ParseAndAttack(cmd.Context(), ignore, base, path, freq, duration) if err != nil { log.Println(err) } diff --git a/lib/attack.go b/lib/attack.go index 7e972e9..b6be0e6 100644 --- a/lib/attack.go +++ b/lib/attack.go @@ -23,27 +23,95 @@ package lib import ( + "context" + "encoding/json" + "fmt" + "net/http" "time" vegeta "github.com/tsenart/vegeta/lib" ) -func (atk *Attacker) Attack() vegeta.Metrics { - target := vegeta.Target{ - Method: atk.Path.method[atk.MethodIndex].method, - URL: atk.Path.path, - Body: atk.Body, - Header: atk.Header, +func ParseAndAttack(ctx context.Context, ignore []string, beseEndpoint string, path string, freq int, duration time.Duration) error { + oapi := NewOpenAPI(path) + paths, err := oapi.Parse(ctx) + if err != nil { + return fmt.Errorf("failed to parse openapi: %w", err) } - targeter := vegeta.NewStaticTargeter(target) - rate := vegeta.Rate{Freq: atk.Frequency, Per: time.Second} - attacker := vegeta.NewAttacker() - var metrics vegeta.Metrics - for res := range attacker.Attack(targeter, rate, atk.Duration, "Vegeta Load Testing") { - metrics.Add(res) + ignores := NewIgnore(ignore) + + atks, err := ParthOpenAPItoAttacker(paths, ignores, beseEndpoint, freq, duration) + if err != nil { + return fmt.Errorf("failed to convert openapi to attacker: %w", err) + } + fmt.Println("--------------------------bulma attack start-------------------------------") + for _, atk := range atks { + metrics := atk.Attack() + outputMetrics(metrics, &atk) + } + fmt.Println("--------------------------bulma attack finish-------------------------------") + return nil +} + +func ParthOpenAPItoAttacker(pathes []Path, ignores Ignore, beseEndpoint string, freq int, duration time.Duration) ([]Attacker, error) { + var res []Attacker + for i, path := range pathes { + if ignores.IsIgnored(path.Path()) { + continue + } + + mtd := path.Method(0) + bodys := mtd.Bodys() + body, err := createBody(bodys) + if err != nil { + return nil, err + } + path.SetPath(beseEndpoint + path.Path()) + atk := Attacker{ + Path: path, + MethodIndex: i, + Body: body, + Header: http.Header{ + "Content-Type": []string{"application/json"}, + }, + Frequency: freq, + Duration: duration, + } + res = append(res, atk) + } + return res, nil +} + +func createBody(bodys []Body) ([]byte, error) { + mp := make(map[string]interface{}) + for _, body := range bodys { + mp[body.Name] = body.Shema.Value.Example + } + jsonData, err := json.Marshal(mp) + if err != nil { + return nil, err } - metrics.Close() + return jsonData, nil +} + +func outputMetrics(metrics vegeta.Metrics, atk *Attacker) { + fmt.Printf("--------------------------vegeta attack to %s--------------------------\n", atk.Path.Path()) + mtd := atk.Path.Method(atk.MethodIndex) + fmt.Printf("vegeta attack to method: %s\n", mtd.Method()) + fmt.Printf("path StatusCode: %v\n", metrics.StatusCodes) + + fmt.Println() + + fmt.Printf("max percentile: %s\n", metrics.Latencies.Max) + fmt.Printf("mean percentile: %s\n", metrics.Latencies.Mean) + fmt.Printf("total percentile: %s\n", metrics.Latencies.Total) + fmt.Printf("99th percentile: %s\n", metrics.Latencies.P99) + + fmt.Println() + + fmt.Printf(" earliest: %v\n", metrics.Earliest) + fmt.Printf(" latest: %v\n", metrics.Latest) - return metrics + fmt.Println("-----------------------------------------------------------------------") } diff --git a/lib/attack_test.go b/lib/attack_test.go index a60dc93..8d25ad2 100644 --- a/lib/attack_test.go +++ b/lib/attack_test.go @@ -21,33 +21,3 @@ // SOFTWARE. package lib - -import ( - "fmt" - "testing" -) - -func TestAttack(t *testing.T) { - t.Run("Attack", func(t *testing.T) { - atk := Attacker{ - Path: Path{ - path: "http://localhost:8080/health", - method: []Method{ - { - method: "GET", - }, - }, - }, - MethodIndex: 0, - Frequency: 110, - Duration: 2, - } - metrics := atk.Attack() - fmt.Printf("99th percentile: %s\n", metrics.Latencies.P99) - fmt.Printf("max percentile: %s\n", metrics.Latencies.Max) - fmt.Printf("mean percentile: %s\n", metrics.Latencies.Mean) - fmt.Printf("total percentile: %s\n", metrics.Latencies.Total) - - fmt.Printf("status code: %v\n", metrics.StatusCodes) - }) -} diff --git a/lib/vegeta.go b/lib/attacker.go similarity index 70% rename from lib/vegeta.go rename to lib/attacker.go index 8cfdaa2..c286374 100644 --- a/lib/vegeta.go +++ b/lib/attacker.go @@ -25,6 +25,8 @@ package lib import ( "net/http" "time" + + vegeta "github.com/tsenart/vegeta/lib" ) type Attacker struct { @@ -35,3 +37,23 @@ type Attacker struct { Frequency int Duration time.Duration } + +func (atk *Attacker) Attack() vegeta.Metrics { + target := vegeta.Target{ + Method: atk.Path.method[atk.MethodIndex].method, + URL: atk.Path.path, + Body: atk.Body, + Header: atk.Header, + } + targeter := vegeta.NewStaticTargeter(target) + rate := vegeta.Rate{Freq: atk.Frequency, Per: time.Second} + + attacker := vegeta.NewAttacker() + var metrics vegeta.Metrics + for res := range attacker.Attack(targeter, rate, atk.Duration, "Vegeta Load Testing") { + metrics.Add(res) + } + metrics.Close() + + return metrics +} diff --git a/lib/attacker_test.go b/lib/attacker_test.go new file mode 100644 index 0000000..a60dc93 --- /dev/null +++ b/lib/attacker_test.go @@ -0,0 +1,53 @@ +// MIT License + +// Copyright (c) 2023 Yamasaki Shotaro + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package lib + +import ( + "fmt" + "testing" +) + +func TestAttack(t *testing.T) { + t.Run("Attack", func(t *testing.T) { + atk := Attacker{ + Path: Path{ + path: "http://localhost:8080/health", + method: []Method{ + { + method: "GET", + }, + }, + }, + MethodIndex: 0, + Frequency: 110, + Duration: 2, + } + metrics := atk.Attack() + fmt.Printf("99th percentile: %s\n", metrics.Latencies.P99) + fmt.Printf("max percentile: %s\n", metrics.Latencies.Max) + fmt.Printf("mean percentile: %s\n", metrics.Latencies.Mean) + fmt.Printf("total percentile: %s\n", metrics.Latencies.Total) + + fmt.Printf("status code: %v\n", metrics.StatusCodes) + }) +} diff --git a/lib/ignore.go b/lib/ignore.go index 0dc5a32..897a0f9 100644 --- a/lib/ignore.go +++ b/lib/ignore.go @@ -1,3 +1,25 @@ +// MIT License + +// Copyright (c) 2023 Yamasaki Shotaro + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + package lib type Ignore struct { diff --git a/lib/ignore_test.go b/lib/ignore_test.go index 55c21f8..8d25ad2 100644 --- a/lib/ignore_test.go +++ b/lib/ignore_test.go @@ -1 +1,23 @@ +// MIT License + +// Copyright (c) 2023 Yamasaki Shotaro + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + package lib