Merge pull request #25 from pedronasser/api-fix

Added wrappers; API fixes and swagger
This commit is contained in:
Travis Reeder
2016-07-29 11:41:49 -07:00
committed by GitHub
31 changed files with 694 additions and 409 deletions

View File

@@ -5,13 +5,14 @@ import "errors"
type Apps []*App
var (
ErrAppsCreate = errors.New("Could not create app")
ErrAppsUpdate = errors.New("Could not update app")
ErrAppsRemoving = errors.New("Could not remove app from datastore")
ErrAppsGet = errors.New("Could not get app from datastore")
ErrAppsList = errors.New("Could not list apps from datastore")
ErrAppsNotFound = errors.New("App not found")
ErrAppNothingToUpdate = errors.New("Nothing to update")
ErrAppsCreate = errors.New("Could not create app")
ErrAppsUpdate = errors.New("Could not update app")
ErrAppsRemoving = errors.New("Could not remove app from datastore")
ErrAppsGet = errors.New("Could not get app from datastore")
ErrAppsList = errors.New("Could not list apps from datastore")
ErrAppsNotFound = errors.New("App not found")
ErrAppsNothingToUpdate = errors.New("Nothing to update")
ErrAppsMissingNew = errors.New("Missing new application")
)
type App struct {

32
api/models/app_wrapper.go Normal file
View File

@@ -0,0 +1,32 @@
package models
import "github.com/go-openapi/errors"
type AppWrapper struct {
App *App `json:"app"`
}
func (m *AppWrapper) Validate() error {
var res []error
if err := m.validateApp(); err != nil {
// prop
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *AppWrapper) validateApp() error {
if m.App != nil {
if err := m.App.Validate(); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,34 @@
package models
import (
strfmt "github.com/go-openapi/strfmt"
"github.com/go-openapi/validate"
"github.com/go-openapi/errors"
)
type AppsWrapper struct {
Apps []*App `json:"apps"`
}
func (m *AppsWrapper) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateApps(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *AppsWrapper) validateApps(formats strfmt.Registry) error {
if err := validate.Required("apps", "body", m.Apps); err != nil {
return err
}
return nil
}

View File

@@ -17,6 +17,6 @@ func ApplyAppFilter(app *App, filter *AppFilter) bool {
}
func ApplyRouteFilter(route *Route, filter *RouteFilter) bool {
return (filter.Path != "" && route.Path == filter.Path) &&
(filter.AppName != "" && route.AppName == filter.AppName)
return (filter.Path == "" || route.Path == filter.Path) &&
(filter.AppName == "" || route.AppName == filter.AppName)
}

View File

@@ -11,5 +11,5 @@ func (m *Error) Validate() error {
}
var (
ErrInvalidJSON = errors.New("Could not create app")
ErrInvalidJSON = errors.New("Invalid JSON")
)

View File

@@ -3,27 +3,28 @@ package models
import (
"errors"
"net/http"
apiErrors "github.com/go-openapi/errors"
)
var (
ErrRoutesCreate = errors.New("Could not create route")
ErrRoutesUpdate = errors.New("Could not update route")
ErrRoutesRemoving = errors.New("Could not remove route from datastore")
ErrRoutesGet = errors.New("Could not get route from datastore")
ErrRoutesList = errors.New("Could not list routes from datastore")
ErrRoutesNotFound = errors.New("Route not found")
ErrRoutesCreate = errors.New("Could not create route")
ErrRoutesUpdate = errors.New("Could not update route")
ErrRoutesRemoving = errors.New("Could not remove route from datastore")
ErrRoutesGet = errors.New("Could not get route from datastore")
ErrRoutesList = errors.New("Could not list routes from datastore")
ErrRoutesNotFound = errors.New("Route not found")
ErrRoutesMissingNew = errors.New("Missing new route")
)
type Routes []*Route
type Route struct {
Name string `json:"name"`
AppName string `json:"appname"`
Path string `json:"path"`
Image string `json:"image"`
Type string `json:"type,omitempty"`
ContainerPath string `json:"container_path,omitempty"`
Headers http.Header `json:"headers,omitempty"`
Name string `json:"name"`
AppName string `json:"appname"`
Path string `json:"path"`
Image string `json:"image"`
Headers http.Header `json:"headers,omitempty"`
}
var (
@@ -34,20 +35,26 @@ var (
)
func (r *Route) Validate() error {
var res []error
if r.Name == "" {
return ErrRoutesValidationName
res = append(res, ErrRoutesValidationAppName)
}
if r.Image == "" {
return ErrRoutesValidationImage
res = append(res, ErrRoutesValidationImage)
}
if r.AppName == "" {
return ErrRoutesValidationAppName
res = append(res, ErrRoutesValidationAppName)
}
if r.Path == "" {
return ErrRoutesValidationPath
res = append(res, ErrRoutesValidationPath)
}
if len(res) > 0 {
return apiErrors.CompositeValidationError(res...)
}
return nil

View File

@@ -0,0 +1,31 @@
package models
import "github.com/go-openapi/errors"
type RouteWrapper struct {
Route *Route `json:"route"`
}
func (m *RouteWrapper) Validate() error {
var res []error
if err := m.validateRoute(); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *RouteWrapper) validateRoute() error {
if m.Route != nil {
if err := m.Route.Validate(); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,36 @@
package models
import (
strfmt "github.com/go-openapi/strfmt"
"github.com/go-openapi/validate"
"github.com/go-openapi/errors"
)
type RoutesWrapper struct {
Cursor string `json:"cursor,omitempty"`
Error *ErrorBody `json:"error,omitempty"`
Routes []*Route `json:"routes"`
}
func (m *RoutesWrapper) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateRoutes(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *RoutesWrapper) validateRoutes(formats strfmt.Registry) error {
if err := validate.Required("routes", "body", m.Routes); err != nil {
return err
}
return nil
}

View File

@@ -17,8 +17,6 @@ CREATE TABLE IF NOT EXISTS routes (
path text NOT NULL,
app_name character varying(256) NOT NULL,
image character varying(256) NOT NULL,
type character varying(256) NOT NULL,
container_path text NOT NULL,
headers text NOT NULL
);`
@@ -26,7 +24,7 @@ const appsTableCreate = `CREATE TABLE IF NOT EXISTS apps (
name character varying(256) NOT NULL PRIMARY KEY
);`
const routeSelector = `SELECT name, path, app_name, image, type, container_path, headers FROM routes`
const routeSelector = `SELECT name, path, app_name, image, headers FROM routes`
type rowScanner interface {
Scan(dest ...interface{}) error
@@ -162,22 +160,18 @@ func (ds *PostgresDatastore) StoreRoute(route *models.Route) (*models.Route, err
_, err = ds.db.Exec(`
INSERT INTO routes (
name, app_name, path, image,
type, container_path, headers
headers
)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (name) DO UPDATE SET
path = $3,
image = $4,
type = $5,
container_path = $6,
headers = $7;
headers = $5;
`,
route.Name,
route.AppName,
route.Path,
route.Image,
route.Type,
route.ContainerPath,
headers,
)
@@ -206,8 +200,6 @@ func scanRoute(scanner rowScanner, route *models.Route) error {
&route.Path,
&route.AppName,
&route.Image,
&route.Type,
&route.ContainerPath,
&headerStr,
)

View File

@@ -12,22 +12,28 @@ func handleAppCreate(c *gin.Context) {
store := c.MustGet("store").(models.Datastore)
log := c.MustGet("log").(logrus.FieldLogger)
app := &models.App{}
wapp := &models.AppWrapper{}
err := c.BindJSON(app)
err := c.BindJSON(wapp)
if err != nil {
log.WithError(err).Debug(models.ErrInvalidJSON)
c.JSON(http.StatusBadRequest, simpleError(models.ErrInvalidJSON))
return
}
if err := app.Validate(); err != nil {
if wapp.App == nil {
log.Debug(models.ErrAppsMissingNew)
c.JSON(http.StatusBadRequest, simpleError(models.ErrAppsMissingNew))
return
}
if err := wapp.Validate(); err != nil {
log.Error(err)
c.JSON(http.StatusInternalServerError, simpleError(err))
return
}
app, err = store.StoreApp(app)
app, err := store.StoreApp(wapp.App)
if err != nil {
log.WithError(err).Debug(models.ErrAppsCreate)
c.JSON(http.StatusInternalServerError, simpleError(models.ErrAppsCreate))

View File

@@ -40,5 +40,5 @@ func handleAppGet(c *gin.Context) {
app.Routes = routes
c.JSON(http.StatusOK, app)
c.JSON(http.StatusOK, &models.AppWrapper{app})
}

View File

@@ -21,5 +21,5 @@ func handleAppList(c *gin.Context) {
return
}
c.JSON(http.StatusOK, apps)
c.JSON(http.StatusOK, &models.AppsWrapper{apps})
}

View File

@@ -21,14 +21,5 @@ func handleAppUpdate(c *gin.Context) {
return
}
// app.Name = c.Param("app")
// app, err = store.StoreApp(app)
// if err != nil {
// log.WithError(err).Debug(models.ErrAppsUpdate)
// c.JSON(http.StatusInternalServerError, simpleError(models.ErrAppsUpdate))
// return
// }
c.JSON(http.StatusOK, simpleError(models.ErrAppNothingToUpdate))
c.JSON(http.StatusOK, simpleError(models.ErrAppsNothingToUpdate))
}

View File

@@ -15,7 +15,7 @@ func Start(engine *gin.Engine) {
v1.POST("/apps", handleAppCreate)
v1.GET("/apps/:app", handleAppGet)
v1.POST("/apps/:app", handleAppUpdate)
v1.PUT("/apps/:app", handleAppUpdate)
v1.DELETE("/apps/:app", handleAppDestroy)
apps := v1.Group("/apps/:app")

View File

@@ -16,11 +16,17 @@ func handleRouteCreate(c *gin.Context) {
err := c.BindJSON(route)
if err != nil {
log.WithError(err).Debug(models.ErrInvalidJSON)
log.WithError(err).Error(models.ErrInvalidJSON)
c.JSON(http.StatusBadRequest, simpleError(models.ErrInvalidJSON))
return
}
if route == nil {
log.WithError(err).Error(models.ErrInvalidJSON)
c.JSON(http.StatusBadRequest, simpleError(models.ErrRoutesMissingNew))
return
}
route.AppName = c.Param("app")
if err := route.Validate(); err != nil {
@@ -46,7 +52,7 @@ func handleRouteCreate(c *gin.Context) {
route, err = store.StoreRoute(route)
if err != nil {
log.WithError(err).Debug(models.ErrRoutesCreate)
log.WithError(err).Error(models.ErrRoutesCreate)
c.JSON(http.StatusInternalServerError, simpleError(models.ErrRoutesCreate))
return
}

View File

@@ -24,5 +24,5 @@ func handleRouteGet(c *gin.Context) {
log.WithFields(logrus.Fields{"route": route}).Debug("Got route")
c.JSON(http.StatusOK, route)
c.JSON(http.StatusOK, &models.RouteWrapper{route})
}

View File

@@ -27,5 +27,5 @@ func handleRouteList(c *gin.Context) {
log.WithFields(logrus.Fields{"routes": routes}).Debug("Got routes")
c.JSON(http.StatusOK, routes)
c.JSON(http.StatusOK, &models.RoutesWrapper{Routes: routes})
}

View File

@@ -12,19 +12,19 @@ func handleRouteUpdate(c *gin.Context) {
store := c.MustGet("store").(models.Datastore)
log := c.MustGet("log").(logrus.FieldLogger)
route := &models.Route{}
wroute := &models.RouteWrapper{}
appName := c.Param("app")
err := c.BindJSON(route)
err := c.BindJSON(wroute)
if err != nil {
log.WithError(err).Debug(models.ErrInvalidJSON)
c.JSON(http.StatusBadRequest, simpleError(models.ErrInvalidJSON))
return
}
route.AppName = appName
wroute.Route.AppName = appName
route, err = store.StoreRoute(route)
route, err := store.StoreRoute(wroute.Route)
if err != nil {
log.WithError(err).Debug(models.ErrAppsCreate)
c.JSON(http.StatusInternalServerError, simpleError(models.ErrAppsCreate))

324
api/swagger.yml Normal file
View File

@@ -0,0 +1,324 @@
# This is the IronFunctions API spec
# If you make changes here, remember to run `go generate` in routeserver/ and
# tasker/ to make sure you use the changes.
swagger: '2.0'
info:
title: IronFunctions
description:
version: "0.0.1"
# the domain of the service
host: "localhost:8080"
# array of all schemes that your API supports
schemes:
- https
- http
# will be prefixed to all paths
basePath: /v1
consumes:
- application/json
produces:
- application/json
paths:
/apps:
get:
summary: "Get all app names."
description: "Get a list of all the apps in the system."
tags:
- Apps
responses:
200:
description: List of apps.
schema:
$ref: '#/definitions/AppsWrapper'
default:
description: Unexpected error
schema:
$ref: '#/definitions/Error'
post:
summary: "Post new app"
description: "Insert a new app"
tags:
- Apps
parameters:
- name: body
in: body
description: App to post.
required: true
schema:
$ref: '#/definitions/AppWrapper'
responses:
200:
description: App details and stats.
schema:
$ref: '#/definitions/AppWrapper'
400:
description: Parameters are missing or invalid.
schema:
$ref: '#/definitions/Error'
500:
description: Could not accept app due to internal error.
schema:
$ref: '#/definitions/Error'
default:
description: Unexpected error
schema:
$ref: '#/definitions/Error'
/apps/{app}:
get:
summary: "Get information for a app."
description: "This gives more details about a app, such as statistics."
tags:
- Apps
parameters:
- name: name
in: path
description: name of the app.
required: true
type: string
responses:
200:
description: App details and stats.
schema:
$ref: '#/definitions/AppWrapper'
404:
description: App does not exist.
schema:
$ref: '#/definitions/Error'
default:
description: Unexpected error
schema:
$ref: '#/definitions/Error'
put:
summary: "Create/update a app."
description: "You can set app level settings here. "
tags:
- Apps
parameters:
- name: app
in: path
description: name of the app.
required: true
type: string
- name: body
in: body
description: App to post.
required: true
schema:
$ref: '#/definitions/AppWrapper'
responses:
200:
description: App details and stats.
schema:
$ref: '#/definitions/AppWrapper'
400:
description: Parameters are missing or invalid.
schema:
$ref: '#/definitions/Error'
500:
description: Could not accept app due to internal error.
schema:
$ref: '#/definitions/Error'
default:
description: Unexpected error
schema:
$ref: '#/definitions/Error'
/apps/{app}/routes:
post:
summary: Enqueue Route
description: |
Enqueues route(s). If any of the routes is invalid, none of the routes are enqueued.
tags:
- Routes
parameters:
- name: app
in: path
description: name of the app.
required: true
type: string
- name: body
in: body
description: Array of routes to post.
required: true
schema:
$ref: '#/definitions/NewRoutesWrapper'
responses:
201:
description: Route created
schema:
$ref: '#/definitions/RoutesWrapper'
400:
description: One or more of the routes were invalid due to parameters being missing or invalid.
schema:
$ref: '#/definitions/Error'
500:
description: Could not accept routes due to internal error.
schema:
$ref: '#/definitions/Error'
default:
description: Unexpected error
schema:
$ref: '#/definitions/Error'
get:
summary: Get route list by app name.
description: This will list routes for a particular app.
tags:
- Routes
parameters:
- name: app
in: path
description: Name of app for this set of routes.
required: true
type: string
responses:
200:
description: Route information
schema:
$ref: '#/definitions/RoutesWrapper'
default:
description: Unexpected error
schema:
$ref: '#/definitions/Error'
/apps/{app}/routes/{route}:
get:
summary: Gets route by name
description: Gets a route by name.
tags:
- Routes
parameters:
- name: app
in: path
description: Name of app for this set of routes.
required: true
type: string
- name: route
in: path
description: Route name
required: true
type: string
responses:
200:
description: Route information
schema:
$ref: '#/definitions/RouteWrapper'
404:
description: Route does not exist.
schema:
$ref: '#/definitions/Error'
default:
description: Unexpected error
schema:
$ref: '#/definitions/Error'
delete:
summary: Deletes the route
tags:
- Routes
description: Deletes the route.
parameters:
- name: app
in: path
description: Name of app for this set of routes.
required: true
type: string
- name: route
in: path
description: Route name
required: true
type: string
responses:
200:
description: Route successfully deleted. Deletion succeeds even on routes that do not exist.
definitions:
Route:
allOf:
- type: object
properties:
name:
type: string
description: "Route name"
readOnly: true
app_name:
type: string
description: "App this route belongs to."
readOnly: true
path:
type: string
description: URL path that will be matched to this route
image:
description: Name of Docker image to use in this route. You should include the image tag, which should be a version number, to be more accurate. Can be overridden on a per route basis with route.image.
type: string
headers:
type: string
description: Map of http headers that will be sent with the response
App:
type: object
properties:
name:
type: string
description: "Name of this app. Must be different than the image name. Can ony contain alphanumeric, -, and _."
readOnly: true
RoutesWrapper:
type: object
required:
- routes
properties:
routes:
type: array
items:
$ref: '#/definitions/Route'
cursor:
type: string
description: Used to paginate results. If this is returned, pass it into the same query again to get more results.
error:
$ref: '#/definitions/ErrorBody'
RouteWrapper:
type: object
required:
- route
properties:
route:
$ref: '#/definitions/Route'
AppsWrapper:
type: object
required:
- apps
properties:
apps:
type: array
items:
$ref: '#/definitions/App'
AppWrapper:
type: object
required:
- app
properties:
app:
$ref: '#/definitions/App'
ErrorBody:
type: object
properties:
message:
type: string
readOnly: true
fields:
type: string
readOnly: true
Error:
type: object
properties:
error:
$ref: '#/definitions/ErrorBody'