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:
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
|
||||
}
|
||||
Reference in New Issue
Block a user