From a8b4655953ab37a03971176ac568a4c5c52154f1 Mon Sep 17 00:00:00 2001 From: Niklas Treml Date: Wed, 25 Oct 2023 14:38:57 +0200 Subject: [PATCH] Feat/openapi generation (#4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: provide schema * feat: generate openapi spec from check * misc: vscode debug config * fix: register rtt check properly with placeholder config * fix: go test failing because of missing newline ¯\_(ツ)_/¯ * chore: remove comments * refactor: rename perfData * refactor: remove debug config --- .vscode/launch.json | 12 ++++++++ go.mod | 13 +++++++++ go.sum | 45 ++++++++++++++++++++++++++++++ pkg/checks/checks.go | 10 +++++-- pkg/checks/errors.go | 3 +- pkg/checks/oapi.go | 22 +++++++++++++++ pkg/checks/oapi_test.go | 37 ++++++++++++++++++++++++ pkg/checks/roundtrip.go | 15 ++++++++-- pkg/sparrow/run.go | 56 +++++++++++++++++++++++++++++++++++-- pkg/sparrow/run_test.go | 62 +++++++++++++++++++++++++++++++++++++++++ 10 files changed, 265 insertions(+), 10 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 go.sum create mode 100644 pkg/checks/oapi.go create mode 100644 pkg/checks/oapi_test.go create mode 100644 pkg/sparrow/run_test.go diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..bb050913 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/cmd/run" + } + ] +} diff --git a/go.mod b/go.mod index bc35df9c..b9edc129 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,16 @@ module github.com/caas-team/sparrow go 1.20 + +require github.com/getkin/kin-openapi v0.120.0 + +require ( + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/swag v0.22.4 // indirect + github.com/invopop/yaml v0.2.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..3c28eb85 --- /dev/null +++ b/go.sum @@ -0,0 +1,45 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/getkin/kin-openapi v0.120.0 h1:MqJcNJFrMDFNc07iwE8iFC5eT2k/NPUFDIpNeiZv8Jg= +github.com/getkin/kin-openapi v0.120.0/go.mod h1:PCWw/lfBrJY4HcdqE3jj+QFkaFK8ABoqo7PvqVhXXqw= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= +github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +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/pkg/checks/checks.go b/pkg/checks/checks.go index d579aefd..cb69fa99 100644 --- a/pkg/checks/checks.go +++ b/pkg/checks/checks.go @@ -3,6 +3,8 @@ package checks import ( "context" "time" + + "github.com/getkin/kin-openapi/openapi3" ) // Available Checks will be registered in this map @@ -29,14 +31,16 @@ type Check interface { SetConfig(ctx context.Context, config any) error // Name returns the name of the check Name() string + // Should return an openapi3.SchemaRef of the result type returned by the check + Schema() (*openapi3.SchemaRef, error) } type Result struct { // data contains performance metrics about the check run - Data any + Data any `json:"data"` // Timestamp is the UTC time the check was run - Timestamp time.Time + Timestamp time.Time `json:"timestamp"` // Err should be nil if the check ran successfully indicating the check is "healthy" // if the check failed, this should be an error message that will be logged and returned to an API user - Err error + Err string `json:"error"` } diff --git a/pkg/checks/errors.go b/pkg/checks/errors.go index 35ff74a2..5170af09 100644 --- a/pkg/checks/errors.go +++ b/pkg/checks/errors.go @@ -2,5 +2,4 @@ package checks import "errors" - -var ErrInvalidConfig = errors.New("invalid config") \ No newline at end of file +var ErrInvalidConfig = errors.New("invalid config") diff --git a/pkg/checks/oapi.go b/pkg/checks/oapi.go new file mode 100644 index 00000000..8d684e5f --- /dev/null +++ b/pkg/checks/oapi.go @@ -0,0 +1,22 @@ +package checks + +import ( + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/openapi3gen" +) + +// Takes in check perfdata and returns an openapi3.SchemaRef of a result wrapping the perfData +// this is a workaround, since the openapi3gen.NewSchemaRefForValue function does not work with any types +func OpenapiFromPerfData[T any](data T) (*openapi3.SchemaRef, error) { + checkSchema, err := openapi3gen.NewSchemaRefForValue(Result{}, openapi3.Schemas{}) + if err != nil { + return nil, err + } + perfdataSchema, err := openapi3gen.NewSchemaRefForValue(data, openapi3.Schemas{}, openapi3gen.UseAllExportedFields()) + if err != nil { + return nil, err + } + + checkSchema.Value.Properties["data"] = perfdataSchema + return checkSchema, nil +} diff --git a/pkg/checks/oapi_test.go b/pkg/checks/oapi_test.go new file mode 100644 index 00000000..23548639 --- /dev/null +++ b/pkg/checks/oapi_test.go @@ -0,0 +1,37 @@ +package checks + +import ( + "reflect" + "testing" + + "github.com/getkin/kin-openapi/openapi3" +) + +func TestOpenapiFromPerfData(t *testing.T) { + type args[T any] struct { + perfData T + } + type cases[T any] struct { + name string + args args[T] + want *openapi3.SchemaRef + wantErr bool + } + tests := []cases[string]{ + {name: "int", args: args[string]{perfData: "hello world"}, want: &openapi3.SchemaRef{Value: openapi3.NewObjectSchema().WithProperties(map[string]*openapi3.Schema{"error": {Type: "string"}, "data": {Type: "string"}, "timestamp": {Type: "string", Format: "date-time"}})}, wantErr: false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := OpenapiFromPerfData(tt.args.perfData) + if (err != nil) != tt.wantErr { + t.Errorf("OpenapiFromPerfData() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("OpenapiFromPerfData() = %v, want %v", got, tt.want) + } + + }) + } +} diff --git a/pkg/checks/roundtrip.go b/pkg/checks/roundtrip.go index 268b8ae9..317db767 100644 --- a/pkg/checks/roundtrip.go +++ b/pkg/checks/roundtrip.go @@ -3,18 +3,23 @@ package checks import ( "context" "net/http" + + "github.com/getkin/kin-openapi/openapi3" ) // ensure that RoundTrip implements the Check interface var _ Check = &RoundTrip{} -type roundTripConfig struct{} +type RoundTripConfig struct{} +type roundTripData struct { + Ms int64 `json:"ms"` +} // RoundTrip is a check that measures the round trip time of a request type RoundTrip struct { name string c chan<- Result - config roundTripConfig + config RoundTripConfig } // Constructor for the RoundtripCheck @@ -52,7 +57,7 @@ func (rt *RoundTrip) Name() string { } func (rt *RoundTrip) SetConfig(ctx context.Context, config any) error { - checkConfig, ok := config.(roundTripConfig) + checkConfig, ok := config.(RoundTripConfig) if !ok { return ErrInvalidConfig } @@ -60,3 +65,7 @@ func (rt *RoundTrip) SetConfig(ctx context.Context, config any) error { return nil } +func (rt *RoundTrip) Schema() (*openapi3.SchemaRef, error) { + return OpenapiFromPerfData[roundTripData](roundTripData{}) + +} diff --git a/pkg/sparrow/run.go b/pkg/sparrow/run.go index db956c77..b6e5d1ae 100644 --- a/pkg/sparrow/run.go +++ b/pkg/sparrow/run.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/caas-team/sparrow/pkg/checks" + "github.com/getkin/kin-openapi/openapi3" ) type Sparrow struct { @@ -15,11 +16,12 @@ type Sparrow struct { // New creates a new sparrow from a given configfile func New(config *Config) *Sparrow { - // TODO read this from config file - return &Sparrow{ + s := &Sparrow{ config: config, c: make(chan checks.Result), } + + return s } // Run starts the sparrow @@ -52,3 +54,53 @@ func (s *Sparrow) Run(ctx context.Context) error { } } } + +var oapiBoilerplate = openapi3.T{ + OpenAPI: "3.0.0", + Info: &openapi3.Info{ + Title: "Sparrow Metrics API", + Description: "Serves metrics collected by sparrows checks", + Contact: &openapi3.Contact{ + URL: "https://caas.telekom.de", + Email: "caas-request@telekom.de", + Name: "CaaS Team", + }, + }, + Paths: make(openapi3.Paths), + Extensions: make(map[string]interface{}), + Components: &openapi3.Components{ + Schemas: make(openapi3.Schemas), + }, + Servers: openapi3.Servers{}, +} + +func (s *Sparrow) Openapi() (openapi3.T, error) { + doc := oapiBoilerplate + for _, c := range s.checks { + ref, err := c.Schema() + if err != nil { + return openapi3.T{}, fmt.Errorf("failed to get schema for check %s: %w", c.Name(), err) + } + + routeDesc := fmt.Sprintf("Returns the performance data for check %s", c.Name()) + bodyDesc := fmt.Sprintf("Metrics for check %s", c.Name()) + doc.Paths["/v1/metrics/"+c.Name()] = &openapi3.PathItem{ + Description: c.Name(), + Get: &openapi3.Operation{ + Description: routeDesc, + Tags: []string{"Metrics", c.Name()}, + Responses: openapi3.Responses{ + "200": &openapi3.ResponseRef{ + Value: &openapi3.Response{ + Description: &bodyDesc, + Content: openapi3.NewContentWithSchemaRef(ref, []string{"application/json"}), + }, + }, + }, + }, + } + + } + + return doc, nil +} diff --git a/pkg/sparrow/run_test.go b/pkg/sparrow/run_test.go new file mode 100644 index 00000000..9e32d67b --- /dev/null +++ b/pkg/sparrow/run_test.go @@ -0,0 +1,62 @@ +package sparrow + +import ( + "reflect" + "testing" + + "github.com/caas-team/sparrow/pkg/checks" + "github.com/getkin/kin-openapi/openapi3" + "gopkg.in/yaml.v3" +) + +func TestSparrow_getOpenapi(t *testing.T) { + type fields struct { + checks []checks.Check + config *Config + c chan checks.Result + } + type test struct { + name string + fields fields + want openapi3.T + wantErr bool + } + tests := []test{ + {name: "no checks registered", fields: fields{checks: []checks.Check{}, config: NewConfig()}, want: oapiBoilerplate, wantErr: false}, + {name: "check registered", fields: fields{checks: []checks.Check{checks.GetRoundtripCheck("rtt")}, config: NewConfig()}, want: oapiBoilerplate, wantErr: false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &Sparrow{ + checks: tt.fields.checks, + config: tt.fields.config, + c: tt.fields.c, + } + got, err := s.Openapi() + if (err != nil) != tt.wantErr { + t.Errorf("Sparrow.getOpenapi() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Sparrow.getOpenapi() = %v, want %v", got, tt.want) + } + + bgot, err := yaml.Marshal(got) + if err != nil { + t.Errorf("OpenapiFromPerfData() error = %v", err) + return + } + t.Logf("\nGot:\n%s", string(bgot)) + + bwant, err := yaml.Marshal(tt.want) + if err != nil { + t.Errorf("OpenapiFromPerfData() error = %v", err) + return + } + + if !reflect.DeepEqual(bgot, bwant) { + t.Errorf("Sparrow.getOpenapi() = %v, want %v", bgot, bwant) + } + }) + } +}