Skip to content

Commit

Permalink
Feat/openapi generation (#4)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
niklastreml authored Oct 25, 2023
1 parent 2306ef9 commit a8b4655
Show file tree
Hide file tree
Showing 10 changed files with 265 additions and 10 deletions.
12 changes: 12 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/cmd/run"
}
]
}
13 changes: 13 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -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
)
45 changes: 45 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
10 changes: 7 additions & 3 deletions pkg/checks/checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package checks
import (
"context"
"time"

"github.com/getkin/kin-openapi/openapi3"
)

// Available Checks will be registered in this map
Expand All @@ -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"`
}
3 changes: 1 addition & 2 deletions pkg/checks/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@ package checks

import "errors"


var ErrInvalidConfig = errors.New("invalid config")
var ErrInvalidConfig = errors.New("invalid config")
22 changes: 22 additions & 0 deletions pkg/checks/oapi.go
Original file line number Diff line number Diff line change
@@ -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
}
37 changes: 37 additions & 0 deletions pkg/checks/oapi_test.go
Original file line number Diff line number Diff line change
@@ -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)
}

})
}
}
15 changes: 12 additions & 3 deletions pkg/checks/roundtrip.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -52,11 +57,15 @@ 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
}
rt.config = checkConfig
return nil
}

func (rt *RoundTrip) Schema() (*openapi3.SchemaRef, error) {
return OpenapiFromPerfData[roundTripData](roundTripData{})

}
56 changes: 54 additions & 2 deletions pkg/sparrow/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"

"github.com/caas-team/sparrow/pkg/checks"
"github.com/getkin/kin-openapi/openapi3"
)

type Sparrow struct {
Expand All @@ -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
Expand Down Expand Up @@ -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: "[email protected]",
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
}
62 changes: 62 additions & 0 deletions pkg/sparrow/run_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}

0 comments on commit a8b4655

Please sign in to comment.