Skip to content

Commit

Permalink
Merge pull request #116 from cmars/feat/versionware-validation
Browse files Browse the repository at this point in the history
feat: openapi request and response validation middleware
  • Loading branch information
cmars committed Jan 14, 2022
2 parents 421434d + 1963fca commit 25fb58e
Show file tree
Hide file tree
Showing 31 changed files with 2,732 additions and 19 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ lint-docker:
.PHONY: test
test:
go test ./... -count=1
(cd versionware/example; go generate . && go test ./... -count=1)

.PHONY: test-coverage
test-coverage:
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
Expand Down
34 changes: 34 additions & 0 deletions util.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"bytes"
"encoding/json"
"fmt"
"io/fs"

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

Expand Down Expand Up @@ -40,3 +42,35 @@ func WithGeneratedComment(yamlBuf []byte) ([]byte, error) {
}
return buf.Bytes(), nil
}

// LoadVersions loads all Vervet-compiled and versioned API specs from a
// filesystem root and returns them.
func LoadVersions(root fs.FS) ([]*openapi3.T, error) {
var versions []*openapi3.T
specFiles, err := fs.Glob(root, "*/spec.json")
if err != nil {
return nil, err
}
for _, specFile := range specFiles {
specData, err := fs.ReadFile(root, specFile)
if err != nil {
return nil, err
}
l := openapi3.NewLoader()
t, err := l.LoadFromData(specData)
if err != nil {
return nil, err
}
if _, err := ExtensionString(t.ExtensionProps, ExtSnykApiVersion); IsExtensionNotFound(err) {
// Not a versioned OpenAPI spec, skip it
continue
} else if err != nil {
return nil, err
}
versions = append(versions, t)
}
if len(versions) == 0 {
return nil, ErrNoMatchingVersion
}
return versions, nil
}
19 changes: 19 additions & 0 deletions versionware/example/.vervet.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
generators:
version-spec:
scope: version
filename: "resources/{{ .Resource }}/{{ .Version }}/spec.yaml"
template: "../../testdata/.vervet/resource/version/spec.yaml.tmpl"

apis:
example:
resources:
- path: 'resources'
generators:
- version-spec
overlays:
- inline: |-
servers:
- url: https://example.com/api/v3
description: Test API v3
output:
path: 'releases'
141 changes: 141 additions & 0 deletions versionware/example/chi/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package chi_test

import (
"bytes"
"log"
"net/http"
"net/http/httptest"
"time"

"github.com/getkin/kin-openapi/openapi3filter"
"github.com/go-chi/chi/v5"
chiware "github.com/go-chi/chi/v5/middleware"
"github.com/prometheus/client_golang/prometheus/promhttp"
metrics "github.com/slok/go-http-metrics/metrics/prometheus"
promware "github.com/slok/go-http-metrics/middleware"
promware_std "github.com/slok/go-http-metrics/middleware/std"
"github.com/snyk/vervet"
"github.com/snyk/vervet/versionware"

. "github.com/snyk/vervet/versionware/example"
"github.com/snyk/vervet/versionware/example/releases"
release_2021_11_01 "github.com/snyk/vervet/versionware/example/resources/things/2021-11-01"
release_2021_11_08 "github.com/snyk/vervet/versionware/example/resources/things/2021-11-08"
release_2021_11_20 "github.com/snyk/vervet/versionware/example/resources/things/2021-11-20"
"github.com/snyk/vervet/versionware/example/store"
)

func ExampleChi() {
// Set up a test HTTP server
var h http.Handler
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.ServeHTTP(w, r)
}))
defer srv.Close()

// Top level router for test server
root := chi.NewRouter()
h = root

// Middleware to wrap all requests
root.Use(chiware.RequestID)
root.Use(chiware.RealIP)
root.Use(chiware.Logger)
root.Use(chiware.Recoverer)
root.Use(chiware.Timeout(30 * time.Second))
root.Use(promware_std.HandlerProvider("", promware.New(promware.Config{
Recorder: metrics.NewRecorder(metrics.Config{}),
})))

// Create a router for just the versioned API
apiRouter := chi.NewRouter()

// Load OpenAPI specs for all released API versions.
specs, err := vervet.LoadVersions(releases.Versions)
if err != nil {
log.Fatal(err)
}

// Add request and response validation middleware to the API router
validator, err := versionware.NewValidator(&versionware.ValidatorConfig{
// We're going to mount our API at /api below...
ServerURL: srv.URL + "/api",
Options: []openapi3filter.ValidatorOption{openapi3filter.Strict(true)},
}, specs...)
if err != nil {
log.Fatal(err)
}
// Only validate the API requests (not other top-level stuff)
apiRouter.Use(validator.Middleware)

// A new storage backend
s := store.New()

// Router for the "things" resource
// As the service grows, these could be pulled out into per-resource sub-packages...
thingsRouter := chi.NewRouter()
thingsRouter.Get("/{id}", versionware.NewHandler([]versionware.VersionHandler{{
Version: release_2021_11_01.Version,
Handler: http.HandlerFunc(release_2021_11_01.GetThing(s)),
}}...).ServeHTTP)
thingsRouter.Get("/", versionware.NewHandler([]versionware.VersionHandler{{
Version: release_2021_11_08.Version,
Handler: release_2021_11_08.ListThings(s),
}}...).ServeHTTP)
thingsRouter.Post("/", versionware.NewHandler([]versionware.VersionHandler{{
Version: release_2021_11_01.Version,
Handler: http.HandlerFunc(release_2021_11_01.CreateThing(s)),
}}...).ServeHTTP)
thingsRouter.Delete("/{id}", versionware.NewHandler([]versionware.VersionHandler{{
Version: release_2021_11_20.Version,
Handler: release_2021_11_20.DeleteThing(s),
}}...).ServeHTTP)

// Mount the "things" resource router at /things in the API
apiRouter.Mount("/things", thingsRouter)

// Mount the entire API at /api
root.Mount("/api", apiRouter)

// Observability stuff at the top-level, not part of the API
root.Get("/metrics", promhttp.Handler().ServeHTTP)
root.Get("/healthcheck", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
})

// Do a health check
PrintResp(srv.Client().Get(srv.URL + "/healthcheck"))

// Create some things
PrintResp(srv.Client().Post(
srv.URL+"/api/things?version=2021-11-01~experimental", "application/json",
bytes.NewBufferString(`{"name":"foo","color":"blue","strangeness":32}`)))
PrintResp(srv.Client().Post(
srv.URL+"/api/things?version=2021-11-01~experimental", "application/json",
bytes.NewBufferString(`{"name":"shiny","color":"green","strangeness":99}`)))
PrintResp(srv.Client().Post(
srv.URL+"/api/things?version=2021-11-01~experimental", "application/json",
bytes.NewBufferString(`{"name":"cochineal","color":"red","strangeness":5}`)))

// 404: no matching version
PrintResp(srv.Client().Post(
srv.URL+"/api/things?version=2021-10-01~experimental", "application/json",
bytes.NewBufferString(`{"name":"cochineal","color":"red","strangeness":5}`)))

// 400: create an invalid thing
PrintResp(srv.Client().Post(
srv.URL+"/api/things?version=2021-11-01~experimental", "application/json",
bytes.NewBufferString(`{"name":"eggplant","color":"purple","banality":17}`)))

// 200: get a thing
PrintResp(srv.Client().Get(srv.URL + "/api/things/1?version=2021-11-10~experimental"))

// Output:
// 200 OK
// 200 {"id":"1","created":"2022-01-14T00:23:50Z","attributes":{"name":"foo","color":"blue","strangeness":32}}
// 200 {"id":"2","created":"2022-01-14T00:23:50Z","attributes":{"name":"shiny","color":"green","strangeness":99}}
// 200 {"id":"3","created":"2022-01-14T00:23:50Z","attributes":{"name":"cochineal","color":"red","strangeness":5}}
// 404 Not Found
// 400 bad request
// 200 {"id":"1","created":"2022-01-14T00:23:50Z","attributes":{"name":"foo","color":"blue","strangeness":32}}
}
22 changes: 22 additions & 0 deletions versionware/example/example.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package example

import (
"fmt"
"io/ioutil"
"net/http"
"strings"
)

// PrintResp prints the response and error from an http client request. This
// is used in example tests.
func PrintResp(resp *http.Response, err error) {
if err != nil {
panic(err)
}
defer resp.Body.Close()
contents, err := ioutil.ReadAll(resp.Body)
if err != nil {
panic(err)
}
fmt.Println(resp.StatusCode, strings.TrimSpace(string(contents)))
}
4 changes: 4 additions & 0 deletions versionware/example/gen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package example

//go:generate make -C ../.. build
//go:generate ../../vervet compile
14 changes: 14 additions & 0 deletions versionware/example/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module github.com/snyk/vervet/versionware/example

go 1.16

require (
github.com/getkin/kin-openapi v0.87.0
github.com/go-chi/chi/v5 v5.0.7
github.com/gorilla/mux v1.8.0
github.com/prometheus/client_golang v1.11.0
github.com/slok/go-http-metrics v0.10.0
github.com/snyk/vervet v1.5.1
)

replace github.com/snyk/vervet => ../..
Loading

0 comments on commit 25fb58e

Please sign in to comment.