mirror of
https://github.com/fnproject/fn.git
synced 2022-10-28 21:29:17 +03:00
Add support for Function and Trigger domain objects (#1060)
Vast commit, includes: * Introduces the Trigger domain entity. * Introduces the Fns domain entity. * V2 of the API for interacting with the new entities in swaggerv2.yml * Adds v2 end points for Apps to support PUT updates. * Rewrites the datastore level tests into a new pattern. * V2 routes use entity ID over name as the path parameter.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -9,7 +10,50 @@ import (
|
||||
"unicode"
|
||||
|
||||
"github.com/fnproject/fn/api/common"
|
||||
"github.com/fnproject/fn/api/id"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrAppsMissingID = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Missing app ID"),
|
||||
}
|
||||
ErrAppIDProvided = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("App ID cannot be supplied on create"),
|
||||
}
|
||||
ErrAppsIDMismatch = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("App ID in path does not match ID in body"),
|
||||
}
|
||||
ErrAppsMissingName = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Missing app name"),
|
||||
}
|
||||
ErrAppsTooLongName = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: fmt.Errorf("App name must be %v characters or less", maxAppName),
|
||||
}
|
||||
ErrAppsInvalidName = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Invalid app name"),
|
||||
}
|
||||
ErrAppsAlreadyExists = err{
|
||||
code: http.StatusConflict,
|
||||
error: errors.New("App already exists"),
|
||||
}
|
||||
ErrAppsMissingNew = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Missing new application"),
|
||||
}
|
||||
ErrAppsNameImmutable = err{
|
||||
code: http.StatusConflict,
|
||||
error: errors.New("Could not update - name is immutable"),
|
||||
}
|
||||
|
||||
ErrAppsNotFound = err{
|
||||
code: http.StatusNotFound,
|
||||
error: errors.New("App not found"),
|
||||
}
|
||||
)
|
||||
|
||||
type App struct {
|
||||
@@ -22,25 +66,10 @@ type App struct {
|
||||
UpdatedAt common.DateTime `json:"updated_at,omitempty" db:"updated_at"`
|
||||
}
|
||||
|
||||
func (a *App) SetDefaults() {
|
||||
if time.Time(a.CreatedAt).IsZero() {
|
||||
a.CreatedAt = common.DateTime(time.Now())
|
||||
}
|
||||
if time.Time(a.UpdatedAt).IsZero() {
|
||||
a.UpdatedAt = common.DateTime(time.Now())
|
||||
}
|
||||
if a.Config == nil {
|
||||
// keeps the json from being nil
|
||||
a.Config = map[string]string{}
|
||||
}
|
||||
if a.ID == "" {
|
||||
a.ID = id.New().String()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) Validate() error {
|
||||
|
||||
if a.Name == "" {
|
||||
return ErrAppsMissingName
|
||||
return ErrMissingName
|
||||
}
|
||||
if len(a.Name) > maxAppName {
|
||||
return ErrAppsTooLongName
|
||||
@@ -147,7 +176,6 @@ func (e ErrInvalidSyslog) Error() string { return string(e) }
|
||||
|
||||
// AppFilter is the filter used for querying apps
|
||||
type AppFilter struct {
|
||||
Name string
|
||||
// NameIn will filter by all names in the list (IN query)
|
||||
NameIn []string
|
||||
PerPage int
|
||||
|
||||
@@ -3,13 +3,10 @@ package models
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type Datastore interface {
|
||||
// GetAppByID gets an App by ID.
|
||||
// Returns ErrDatastoreEmptyAppID for empty appID.
|
||||
// Returns ErrAppsNotFound if no app is found.
|
||||
GetAppByID(ctx context.Context, appID string) (*App, error)
|
||||
|
||||
@@ -59,8 +56,42 @@ type Datastore interface {
|
||||
// ErrDatastoreEmptyRoutePath when routePath is empty. Returns ErrRoutesNotFound when no route exists.
|
||||
RemoveRoute(ctx context.Context, appID, routePath string) error
|
||||
|
||||
// GetDatabase returns the underlying sqlx database implementation
|
||||
GetDatabase() *sqlx.DB
|
||||
// InsertFn inserts a new function if one does not exist, applying any defaults necessary,
|
||||
InsertFn(ctx context.Context, fn *Fn) (*Fn, error)
|
||||
|
||||
// UpdateFn updates a function that exists under the same id.
|
||||
// ErrMissingName is func.Name is empty.
|
||||
UpdateFn(ctx context.Context, fn *Fn) (*Fn, error)
|
||||
|
||||
// GetFns returns a list of funcs, applying any additional filters provided.
|
||||
GetFns(ctx context.Context, filter *FnFilter) ([]*Fn, error)
|
||||
|
||||
// GetFnByID returns a function by ID. Returns ErrDatastoreEmptyFnID if fnID is empty.
|
||||
// Returns ErrFnsNotFound if a fn is not found.
|
||||
GetFnByID(ctx context.Context, fnID string) (*Fn, error)
|
||||
|
||||
// RemoveFn removes a function. Returns ErrDatastoreEmptyFnID if fnID is empty.
|
||||
// Returns ErrFnsNotFound if a func is not found.
|
||||
RemoveFn(ctx context.Context, fnID string) error
|
||||
|
||||
// InsertTrigger inserts a trigger. Returns ErrDatastoreEmptyTrigger when trigger is nil, and specific errors for each field
|
||||
// Returns ErrTriggerAlreadyExists if the exact apiID, fnID, source, type combination already exists
|
||||
InsertTrigger(ctx context.Context, trigger *Trigger) (*Trigger, error)
|
||||
|
||||
//UpdateTrigger updates a trigger object in the data store
|
||||
UpdateTrigger(ctx context.Context, trigger *Trigger) (*Trigger, error)
|
||||
|
||||
// Removes a Trigger. Returns field specific errors if they are empty.
|
||||
// Returns nil if successful
|
||||
RemoveTrigger(ctx context.Context, triggerID string) error
|
||||
|
||||
// GetTriggerByID gets a trigger by it's id.
|
||||
// Returns ErrTriggerNotFound when no matching trigger is found
|
||||
GetTriggerByID(ctx context.Context, triggerID string) (*Trigger, error)
|
||||
|
||||
// GetTriggers gets a list of triggers that match the specified filter
|
||||
// Return ErrDatastoreEmptyAppId if no AppID set in the filter
|
||||
GetTriggers(ctx context.Context, filter *TriggerFilter) ([]*Trigger, error)
|
||||
|
||||
// implements io.Closer to shutdown
|
||||
io.Closer
|
||||
|
||||
@@ -8,7 +8,9 @@ import (
|
||||
|
||||
// TODO we can put constants all in this file too
|
||||
const (
|
||||
maxAppName = 30
|
||||
maxAppName = 30
|
||||
maxFnName = 30
|
||||
MaxTriggerName = 30
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -24,62 +26,49 @@ var (
|
||||
code: http.StatusServiceUnavailable,
|
||||
error: errors.New("Timed out - server too busy"),
|
||||
}
|
||||
ErrAppsMissingName = err{
|
||||
|
||||
ErrMissingID = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Missing app name"),
|
||||
}
|
||||
ErrAppsTooLongName = err{
|
||||
error: errors.New("Missing ID")}
|
||||
|
||||
ErrMissingAppID = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: fmt.Errorf("App name must be %v characters or less", maxAppName),
|
||||
}
|
||||
ErrAppsInvalidName = err{
|
||||
error: errors.New("Missing App ID")}
|
||||
ErrMissingName = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Invalid app name"),
|
||||
}
|
||||
ErrAppsAlreadyExists = err{
|
||||
code: http.StatusConflict,
|
||||
error: errors.New("App already exists"),
|
||||
}
|
||||
ErrAppsMissingNew = err{
|
||||
error: errors.New("Missing Name")}
|
||||
|
||||
ErrCreatedAtProvided = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Missing new application"),
|
||||
}
|
||||
ErrAppsNameImmutable = err{
|
||||
code: http.StatusConflict,
|
||||
error: errors.New("Could not update - name is immutable"),
|
||||
}
|
||||
ErrAppsNotFound = err{
|
||||
code: http.StatusNotFound,
|
||||
error: errors.New("App not found"),
|
||||
}
|
||||
ErrDeleteAppsWithRoutes = err{
|
||||
code: http.StatusConflict,
|
||||
error: errors.New("Cannot remove apps with routes"),
|
||||
}
|
||||
error: errors.New("Trigger Created At Provided for Create")}
|
||||
ErrUpdatedAtProvided = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Trigger ID Provided for Create")}
|
||||
|
||||
ErrDatastoreEmptyApp = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Missing app"),
|
||||
}
|
||||
ErrDatastoreEmptyAppID = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Missing app ID"),
|
||||
}
|
||||
ErrDatastoreEmptyRoute = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Missing route"),
|
||||
}
|
||||
ErrDatastoreEmptyKey = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Missing key"),
|
||||
}
|
||||
ErrDatastoreEmptyCallID = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Missing call ID"),
|
||||
}
|
||||
ErrDatastoreEmptyFn = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Missing Fn"),
|
||||
}
|
||||
ErrDatastoreEmptyFnID = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Missing Fn ID"),
|
||||
}
|
||||
ErrInvalidPayload = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Invalid payload"),
|
||||
}
|
||||
ErrDatastoreEmptyRoute = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Missing route"),
|
||||
}
|
||||
ErrRoutesAlreadyExists = err{
|
||||
code: http.StatusConflict,
|
||||
error: errors.New("Route already exists"),
|
||||
@@ -128,10 +117,6 @@ var (
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Missing route Path"),
|
||||
}
|
||||
ErrRoutesMissingType = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Missing route Type"),
|
||||
}
|
||||
ErrPathMalformed = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Path malformed"),
|
||||
@@ -156,10 +141,19 @@ var (
|
||||
code: http.StatusBadRequest,
|
||||
error: fmt.Errorf("memory value is out of range. It should be between 0 and %d", RouteMaxMemory),
|
||||
}
|
||||
ErrInvalidMemory = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: fmt.Errorf("memory value is out of range. It should be between 0 and %d", RouteMaxMemory),
|
||||
}
|
||||
ErrCallNotFound = err{
|
||||
code: http.StatusNotFound,
|
||||
error: errors.New("Call not found"),
|
||||
}
|
||||
ErrInvalidCPUs = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: fmt.Errorf("Cpus is invalid. Value should be either between [%.3f and %.3f] or [%dm and %dm] milliCPU units",
|
||||
float64(MinMilliCPUs)/1000.0, float64(MaxMilliCPUs)/1000.0, MinMilliCPUs, MaxMilliCPUs),
|
||||
}
|
||||
ErrCallLogNotFound = err{
|
||||
code: http.StatusNotFound,
|
||||
error: errors.New("Call log not found"),
|
||||
@@ -176,11 +170,6 @@ var (
|
||||
code: http.StatusNotFound,
|
||||
error: errors.New("Path not found"),
|
||||
}
|
||||
ErrInvalidCPUs = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: fmt.Errorf("Cpus is invalid. Value should be either between [%.3f and %.3f] or [%dm and %dm] milliCPU units",
|
||||
float64(MinMilliCPUs)/1000.0, float64(MaxMilliCPUs)/1000.0, MinMilliCPUs, MaxMilliCPUs),
|
||||
}
|
||||
ErrFunctionResponseTooBig = err{
|
||||
code: http.StatusBadGateway,
|
||||
error: fmt.Errorf("function response too large"),
|
||||
@@ -240,11 +229,11 @@ func GetAPIErrorCode(e error) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Error uniform error output
|
||||
type Error struct {
|
||||
Error *ErrorBody `json:"error,omitempty"`
|
||||
// ErrorWrapper uniform error output (v1) only
|
||||
type ErrorWrapper struct {
|
||||
Error *Error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Error) Validate() error {
|
||||
func (m *ErrorWrapper) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package models
|
||||
|
||||
type ErrorBody struct {
|
||||
type Error struct {
|
||||
Message string `json:"message,omitempty"`
|
||||
Fields string `json:"fields,omitempty"`
|
||||
}
|
||||
|
||||
// Validate validates this error body
|
||||
func (m *ErrorBody) Validate() error {
|
||||
func (m *Error) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
281
api/models/fn.go
Normal file
281
api/models/fn.go
Normal file
@@ -0,0 +1,281 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/fnproject/fn/api/common"
|
||||
)
|
||||
|
||||
var (
|
||||
// these are vars so that they can be configured. these apply
|
||||
// across function & trigger (resource config)
|
||||
|
||||
MaxMemory uint64 = 8 * 1024 // 8GB
|
||||
MaxTimeout int32 = 300 // 5m
|
||||
MaxIdleTimeout int32 = 3600 // 1h
|
||||
|
||||
ErrFnsIDMismatch = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Fn ID in path does not match that in body"),
|
||||
}
|
||||
ErrFnsIDProvided = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("ID cannot be provided for Fn creation"),
|
||||
}
|
||||
ErrFnsMissingID = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Missing Fn ID"),
|
||||
}
|
||||
ErrFnsMissingName = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Missing Fn name"),
|
||||
}
|
||||
ErrFnsInvalidName = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("name must be a valid string"),
|
||||
}
|
||||
ErrFnsTooLongName = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: fmt.Errorf("Fn name must be %v characters or less", maxFnName),
|
||||
}
|
||||
ErrFnsMissingAppID = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Missing AppID on Fn"),
|
||||
}
|
||||
ErrFnsMissingImage = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Missing image on Fn"),
|
||||
}
|
||||
ErrFnsInvalidFormat = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Invalid format on Fn"),
|
||||
}
|
||||
ErrFnsInvalidTimeout = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: fmt.Errorf("timeout value is out of range, must be between 0 and %d", MaxTimeout),
|
||||
}
|
||||
ErrFnsInvalidIdleTimeout = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: fmt.Errorf("idle_timeout value is out of range, must be between 0 and %d", MaxIdleTimeout),
|
||||
}
|
||||
ErrFnsNotFound = err{
|
||||
code: http.StatusNotFound,
|
||||
error: errors.New("Fn not found"),
|
||||
}
|
||||
ErrFnsExists = err{
|
||||
code: http.StatusConflict,
|
||||
error: errors.New("Fn with specified name already exists"),
|
||||
}
|
||||
)
|
||||
|
||||
// Fn contains information about a function configuration.
|
||||
type Fn struct {
|
||||
// ID is the generated resource id.
|
||||
ID string `json:"id" db:"id"`
|
||||
// Name is a user provided name for this fn.
|
||||
Name string `json:"name" db:"name"`
|
||||
// AppID is the name of the app this fn belongs to.
|
||||
AppID string `json:"app_id" db:"app_id"`
|
||||
// Image is the fully qualified container registry address to execute.
|
||||
// examples: hub.docker.io/me/myfunc, me/myfunc, me/func:0.0.1
|
||||
Image string `json:"image" db:"image"`
|
||||
// ResourceConfig specifies resource constraints.
|
||||
ResourceConfig // embed (TODO or not?)
|
||||
// Config is the configuration passed to a function at execution time.
|
||||
Config Config `json:"config" db:"config"`
|
||||
// Annotations allow additional configuration of a function, these are not passed to the function.
|
||||
Annotations Annotations `json:"annotations,omitempty" db:"annotations"`
|
||||
// CreatedAt is the UTC timestamp when this function was created.
|
||||
CreatedAt common.DateTime `json:"created_at,omitempty" db:"created_at"`
|
||||
// UpdatedAt is the UTC timestamp of the last time this func was modified.
|
||||
UpdatedAt common.DateTime `json:"updated_at,omitempty" db:"updated_at"`
|
||||
|
||||
// TODO wish to kill but not yet ?
|
||||
// Format is the container protocol the function will accept,
|
||||
// may be one of: json | http | cloudevent | default
|
||||
Format string `json:"format" db:"format"`
|
||||
}
|
||||
|
||||
// ResourceConfig specified resource constraints imposed on a function execution.
|
||||
type ResourceConfig struct {
|
||||
// Memory is the amount of memory allotted, in MB.
|
||||
Memory uint64 `json:"memory,omitempty" db:"memory"`
|
||||
// Timeout is the max execution time for a function, in seconds.
|
||||
// TODO this should probably be milliseconds?
|
||||
Timeout int32 `json:"timeout,omitempty" db:"timeout"`
|
||||
// IdleTimeout is the
|
||||
// TODO this should probably be milliseconds
|
||||
IdleTimeout int32 `json:"idle_timeout,omitempty" db:"idle_timeout"`
|
||||
}
|
||||
|
||||
// SetCreated sets zeroed field to defaults.
|
||||
func (f *Fn) SetDefaults() {
|
||||
|
||||
if f.Memory == 0 {
|
||||
f.Memory = DefaultMemory
|
||||
}
|
||||
|
||||
if f.Format == "" {
|
||||
f.Format = FormatDefault
|
||||
}
|
||||
|
||||
if f.Config == nil {
|
||||
// keeps the json from being nil
|
||||
f.Config = map[string]string{}
|
||||
}
|
||||
|
||||
if f.Timeout == 0 {
|
||||
f.Timeout = DefaultTimeout
|
||||
}
|
||||
|
||||
if f.IdleTimeout == 0 {
|
||||
f.IdleTimeout = DefaultIdleTimeout
|
||||
}
|
||||
|
||||
if time.Time(f.CreatedAt).IsZero() {
|
||||
f.CreatedAt = common.DateTime(time.Now())
|
||||
}
|
||||
|
||||
if time.Time(f.UpdatedAt).IsZero() {
|
||||
f.UpdatedAt = common.DateTime(time.Now())
|
||||
}
|
||||
}
|
||||
|
||||
// Validate validates all field values, returning the first error, if any.
|
||||
func (f *Fn) Validate() error {
|
||||
|
||||
if f.Name == "" {
|
||||
return ErrFnsMissingName
|
||||
}
|
||||
if len(f.Name) > maxFnName {
|
||||
return ErrFnsTooLongName
|
||||
}
|
||||
|
||||
if url.PathEscape(f.Name) != f.Name {
|
||||
return ErrFnsInvalidName
|
||||
}
|
||||
|
||||
if f.AppID == "" {
|
||||
return ErrFnsMissingAppID
|
||||
}
|
||||
|
||||
if f.Image == "" {
|
||||
return ErrFnsMissingImage
|
||||
}
|
||||
|
||||
switch f.Format {
|
||||
case FormatDefault, FormatHTTP, FormatJSON, FormatCloudEvent:
|
||||
default:
|
||||
return ErrFnsInvalidFormat
|
||||
}
|
||||
|
||||
if f.Timeout <= 0 || f.Timeout > MaxTimeout {
|
||||
return ErrFnsInvalidTimeout
|
||||
}
|
||||
|
||||
if f.IdleTimeout <= 0 || f.IdleTimeout > MaxIdleTimeout {
|
||||
return ErrFnsInvalidIdleTimeout
|
||||
}
|
||||
|
||||
if f.Memory < 1 || f.Memory > MaxMemory {
|
||||
return ErrInvalidMemory
|
||||
}
|
||||
|
||||
return f.Annotations.Validate()
|
||||
}
|
||||
|
||||
func (f *Fn) Clone() *Fn {
|
||||
clone := new(Fn)
|
||||
*clone = *f // shallow copy
|
||||
|
||||
// now deep copy the maps
|
||||
if f.Config != nil {
|
||||
clone.Config = make(Config, len(f.Config))
|
||||
for k, v := range f.Config {
|
||||
clone.Config[k] = v
|
||||
}
|
||||
}
|
||||
if f.Annotations != nil {
|
||||
clone.Annotations = make(Annotations, len(f.Annotations))
|
||||
for k, v := range f.Annotations {
|
||||
// TODO technically, we need to deep copy the bytes
|
||||
clone.Annotations[k] = v
|
||||
}
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
func (f1 *Fn) Equals(f2 *Fn) bool {
|
||||
// start off equal, check equivalence of each field.
|
||||
// the RHS of && won't eval if eq==false so config/headers checking is lazy
|
||||
|
||||
eq := true
|
||||
eq = eq && f1.ID == f2.ID
|
||||
eq = eq && f1.Name == f2.Name
|
||||
eq = eq && f1.AppID == f2.AppID
|
||||
eq = eq && f1.Image == f2.Image
|
||||
eq = eq && f1.Memory == f2.Memory
|
||||
eq = eq && f1.Format == f2.Format
|
||||
eq = eq && f1.Timeout == f2.Timeout
|
||||
eq = eq && f1.IdleTimeout == f2.IdleTimeout
|
||||
eq = eq && f1.Config.Equals(f2.Config)
|
||||
eq = eq && f1.Annotations.Equals(f2.Annotations)
|
||||
// NOTE: datastore tests are not very fun to write with timestamp checks,
|
||||
// and these are not values the user may set so we kind of don't care.
|
||||
//eq = eq && time.Time(f1.CreatedAt).Equal(time.Time(f2.CreatedAt))
|
||||
//eq = eq && time.Time(f2.UpdatedAt).Equal(time.Time(f2.UpdatedAt))
|
||||
return eq
|
||||
}
|
||||
|
||||
// Update updates fields in f with non-zero field values from new, and sets
|
||||
// updated_at if any of the fields change. 0-length slice Header values, and
|
||||
// empty-string Config values trigger removal of map entry.
|
||||
func (f *Fn) Update(patch *Fn) {
|
||||
original := f.Clone()
|
||||
|
||||
if patch.Image != "" {
|
||||
f.Image = patch.Image
|
||||
}
|
||||
if patch.Memory != 0 {
|
||||
f.Memory = patch.Memory
|
||||
}
|
||||
|
||||
if patch.Timeout != 0 {
|
||||
f.Timeout = patch.Timeout
|
||||
}
|
||||
if patch.IdleTimeout != 0 {
|
||||
f.IdleTimeout = patch.IdleTimeout
|
||||
}
|
||||
if patch.Format != "" {
|
||||
f.Format = patch.Format
|
||||
}
|
||||
if patch.Config != nil {
|
||||
if f.Config == nil {
|
||||
f.Config = make(Config)
|
||||
}
|
||||
for k, v := range patch.Config {
|
||||
if v == "" {
|
||||
delete(f.Config, k)
|
||||
} else {
|
||||
f.Config[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
f.Annotations = f.Annotations.MergeChange(patch.Annotations)
|
||||
|
||||
if !f.Equals(original) {
|
||||
f.UpdatedAt = common.DateTime(time.Now())
|
||||
}
|
||||
}
|
||||
|
||||
type FnFilter struct {
|
||||
AppID string // this is exact match
|
||||
Name string //exact match
|
||||
Cursor string
|
||||
PerPage int
|
||||
}
|
||||
@@ -25,7 +25,7 @@ type LogStore interface {
|
||||
InsertCall(ctx context.Context, call *Call) error
|
||||
|
||||
// GetCall returns a call at a certain id and app name.
|
||||
GetCall(ctx context.Context, appName, callID string) (*Call, error)
|
||||
GetCall(ctx context.Context, appId, callID string) (*Call, error)
|
||||
|
||||
// GetCalls returns a list of calls that satisfy the given CallFilter. If no
|
||||
// calls exist, an empty list and a nil error are returned.
|
||||
|
||||
@@ -17,7 +17,6 @@ const (
|
||||
|
||||
MaxSyncTimeout = 120 // 2 minutes
|
||||
MaxAsyncTimeout = 3600 // 1 hour
|
||||
MaxIdleTimeout = MaxAsyncTimeout
|
||||
)
|
||||
|
||||
var RouteMaxMemory = uint64(8 * 1024)
|
||||
@@ -73,13 +72,6 @@ func (r *Route) SetDefaults() {
|
||||
r.IdleTimeout = DefaultIdleTimeout
|
||||
}
|
||||
|
||||
if time.Time(r.CreatedAt).IsZero() {
|
||||
r.CreatedAt = common.DateTime(time.Now())
|
||||
}
|
||||
|
||||
if time.Time(r.UpdatedAt).IsZero() {
|
||||
r.UpdatedAt = common.DateTime(time.Now())
|
||||
}
|
||||
}
|
||||
|
||||
// Validate validates all field values, returning the first error, if any.
|
||||
|
||||
180
api/models/trigger.go
Normal file
180
api/models/trigger.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/fnproject/fn/api/common"
|
||||
)
|
||||
|
||||
type Trigger struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
AppID string `json:"app_id" db:"app_id"`
|
||||
FnID string `json:"fn_id" db:"fn_id"`
|
||||
CreatedAt common.DateTime `json:"created_at,omitempty" db:"created_at"`
|
||||
UpdatedAt common.DateTime `json:"updated_at,omitempty" db:"updated_at"`
|
||||
Type string `json:"type" db:"type"`
|
||||
Source string `json:"source" db:"source"`
|
||||
Annotations Annotations `json:"annotations,omitempty" db:"annotations"`
|
||||
}
|
||||
|
||||
func (t *Trigger) Equals(t2 *Trigger) bool {
|
||||
eq := true
|
||||
eq = eq && t.ID == t2.ID
|
||||
eq = eq && t.Name == t2.Name
|
||||
eq = eq && t.AppID == t2.AppID
|
||||
eq = eq && t.FnID == t2.FnID
|
||||
|
||||
eq = eq && t.Type == t2.Type
|
||||
eq = eq && t.Source == t2.Source
|
||||
eq = eq && t.Annotations.Equals(t2.Annotations)
|
||||
|
||||
return eq
|
||||
}
|
||||
|
||||
var triggerTypes = []string{"http"}
|
||||
|
||||
func ValidTriggerTypes() []string {
|
||||
return triggerTypes
|
||||
}
|
||||
|
||||
func ValidTriggerType(a string) bool {
|
||||
for _, b := range triggerTypes {
|
||||
if b == a {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var (
|
||||
ErrTriggerIDProvided = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("ID cannot be provided for Trigger creation"),
|
||||
}
|
||||
ErrTriggerIDMismatch = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("ID in path does not match ID in body"),
|
||||
}
|
||||
ErrTriggerMissingName = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Missing name on Trigger")}
|
||||
ErrTriggerTooLongName = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: fmt.Errorf("Trigger name must be %v characters or less", MaxTriggerName)}
|
||||
ErrTriggerInvalidName = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Invalid name for Trigger")}
|
||||
ErrTriggerMissingAppID = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Missing App ID on Trigger")}
|
||||
ErrTriggerMissingFnID = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Missing Fn ID on Trigger")}
|
||||
ErrTriggerFnIDNotSameApp = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Invalid Fn ID - not owned by specified app")}
|
||||
ErrTriggerTypeUnknown = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Trigger Type Not Supported")}
|
||||
ErrTriggerMissingSource = err{
|
||||
code: http.StatusBadRequest,
|
||||
error: errors.New("Missing Trigger Source")}
|
||||
ErrTriggerNotFound = err{
|
||||
code: http.StatusNotFound,
|
||||
error: errors.New("Trigger not found")}
|
||||
ErrTriggerExists = err{
|
||||
code: http.StatusConflict,
|
||||
error: errors.New("Trigger already exists")}
|
||||
)
|
||||
|
||||
func (t *Trigger) Validate() error {
|
||||
if t.Name == "" {
|
||||
return ErrTriggerMissingName
|
||||
}
|
||||
|
||||
if t.AppID == "" {
|
||||
return ErrTriggerMissingAppID
|
||||
}
|
||||
|
||||
if len(t.Name) > MaxTriggerName {
|
||||
return ErrTriggerTooLongName
|
||||
}
|
||||
for _, c := range t.Name {
|
||||
if !(unicode.IsLetter(c) || unicode.IsNumber(c) || c == '_' || c == '-') {
|
||||
return ErrTriggerInvalidName
|
||||
}
|
||||
}
|
||||
|
||||
if t.FnID == "" {
|
||||
return ErrTriggerMissingFnID
|
||||
}
|
||||
|
||||
if !ValidTriggerType(t.Type) {
|
||||
return ErrTriggerTypeUnknown
|
||||
}
|
||||
|
||||
if t.Source == "" {
|
||||
return ErrTriggerMissingSource
|
||||
}
|
||||
|
||||
err := t.Annotations.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Trigger) Clone() *Trigger {
|
||||
clone := new(Trigger)
|
||||
*clone = *t // shallow copy
|
||||
|
||||
if t.Annotations != nil {
|
||||
clone.Annotations = make(Annotations, len(t.Annotations))
|
||||
for k, v := range t.Annotations {
|
||||
// TODO technically, we need to deep copy the bytes
|
||||
clone.Annotations[k] = v
|
||||
}
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
func (t *Trigger) Update(patch *Trigger) {
|
||||
|
||||
original := t.Clone()
|
||||
if patch.AppID != "" {
|
||||
t.AppID = patch.AppID
|
||||
}
|
||||
|
||||
if patch.FnID != "" {
|
||||
t.FnID = patch.FnID
|
||||
}
|
||||
|
||||
if patch.Name != "" {
|
||||
t.Name = patch.Name
|
||||
}
|
||||
|
||||
if patch.Source != "" {
|
||||
t.Source = patch.Source
|
||||
}
|
||||
|
||||
t.Annotations = t.Annotations.MergeChange(patch.Annotations)
|
||||
|
||||
if !t.Equals(original) {
|
||||
t.UpdatedAt = common.DateTime(time.Now())
|
||||
}
|
||||
}
|
||||
|
||||
type TriggerFilter struct {
|
||||
AppID string // this is exact match
|
||||
FnID string // this is exact match
|
||||
Name string // exact match
|
||||
|
||||
Cursor string
|
||||
PerPage int
|
||||
}
|
||||
51
api/models/trigger_test.go
Normal file
51
api/models/trigger_test.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var openEmptyJson = `{"id":"","name":"","app_id":"","fn_id":"","created_at":"0001-01-01T00:00:00.000Z","updated_at":"0001-01-01T00:00:00.000Z","type":"","source":""`
|
||||
|
||||
var triggerJsonCases = []struct {
|
||||
val *Trigger
|
||||
valString string
|
||||
}{
|
||||
{val: &Trigger{}, valString: openEmptyJson + "}"},
|
||||
}
|
||||
|
||||
func TestTriggerJsonMarshalling(t *testing.T) {
|
||||
for _, tc := range triggerJsonCases {
|
||||
v, err := json.Marshal(tc.val)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal json into %s: %v", tc.valString, err)
|
||||
}
|
||||
if string(v) != tc.valString {
|
||||
t.Errorf("Invalid trigger value, expected %s, got %s", tc.valString, string(v))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var httpTrigger = &Trigger{Name: "name", AppID: "foo", FnID: "bar", Type: "http", Source: "baz"}
|
||||
var invalidTrigger = &Trigger{Name: "name", AppID: "foo", FnID: "bar", Type: "error", Source: "baz"}
|
||||
|
||||
var triggerValidateCases = []struct {
|
||||
val *Trigger
|
||||
valid bool
|
||||
}{
|
||||
{val: &Trigger{}, valid: false},
|
||||
{val: invalidTrigger, valid: false},
|
||||
{val: httpTrigger, valid: true},
|
||||
}
|
||||
|
||||
func TestTriggerValidate(t *testing.T) {
|
||||
for _, tc := range triggerValidateCases {
|
||||
v := tc.val.Validate()
|
||||
if v != nil && tc.valid {
|
||||
t.Errorf("Expected Trigger to be valid, but err (%s) returned. Trigger: %#v", v, tc.val)
|
||||
}
|
||||
if v == nil && !tc.valid {
|
||||
t.Errorf("Expected Trigger to be invalid, but no err returned. Trigger: %#v", tc.val)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user