-
Notifications
You must be signed in to change notification settings - Fork 405
/
trigger.go
245 lines (207 loc) · 7.01 KB
/
trigger.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
package models
import (
"errors"
"fmt"
"net/http"
"strings"
"time"
"unicode"
"github.com/fnproject/fn/api/common"
)
// TriggerHTTPEndpointAnnotation is the annotation that exposes the HTTP trigger endpoint For want of a better place to put this it's here
const TriggerHTTPEndpointAnnotation = "fnproject.io/trigger/httpEndpoint"
// Trigger represents a binding between a Function and an external event source
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"`
}
// Equals compares two triggers for semantic equality it ignores timestamp fields but includes 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
}
// EqualsWithAnnotationSubset is equivalent to Equals except it accepts cases where t's annotations are strict subset of t2
func (t *Trigger) EqualsWithAnnotationSubset(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.Subset(t2.Annotations)
return eq
}
//TriggerTypeHTTP represents an HTTP trigger
const TriggerTypeHTTP = "http"
var triggerTypes = []string{TriggerTypeHTTP}
//ValidTriggerTypes lists the supported trigger types in this service
func ValidTriggerTypes() []string {
return triggerTypes
}
//ValidTriggerType checks that a given trigger type is valid on this service
func ValidTriggerType(a string) bool {
for _, b := range triggerTypes {
if b == a {
return true
}
}
return false
}
var (
//ErrTriggerIDProvided indicates that a trigger ID was specified when it shouldn't have been
ErrTriggerIDProvided = err{
code: http.StatusBadRequest,
error: errors.New("ID cannot be provided for Trigger creation"),
}
//ErrTriggerIDMismatch indicates an ID was provided that did not match the ID of the corresponding operation/call
ErrTriggerIDMismatch = err{
code: http.StatusBadRequest,
error: errors.New("ID in path does not match ID in body"),
}
//ErrTriggerMissingName - name not specified on a trigger object
ErrTriggerMissingName = err{
code: http.StatusBadRequest,
error: errors.New("Missing name on Trigger")}
//ErrTriggerTooLongName - name exceeds maximum permitted name
ErrTriggerTooLongName = err{
code: http.StatusBadRequest,
error: fmt.Errorf("Trigger name must be %v characters or less", MaxLengthTriggerName)}
//ErrTriggerInvalidName - name does not comply with naming spec
ErrTriggerInvalidName = err{
code: http.StatusBadRequest,
error: errors.New("Invalid name for Trigger")}
//ErrTriggerMissingAppID - no API id specified on trigger creation
ErrTriggerMissingAppID = err{
code: http.StatusBadRequest,
error: errors.New("Missing App ID on Trigger")}
//ErrTriggerMissingFnID - no FNID specified on trigger creation
ErrTriggerMissingFnID = err{
code: http.StatusBadRequest,
error: errors.New("Missing Fn ID on Trigger")}
//ErrTriggerFnIDNotSameApp - specified Fn does not belong to the same app as the provided AppID
ErrTriggerFnIDNotSameApp = err{
code: http.StatusBadRequest,
error: errors.New("Invalid Fn ID - not owned by specified app")}
//ErrTriggerTypeUnknown - unsupported trigger type
ErrTriggerTypeUnknown = err{
code: http.StatusBadRequest,
error: errors.New("Trigger Type Not Supported")}
//ErrTriggerMissingSource - no source spceified for trigger
ErrTriggerMissingSource = err{
code: http.StatusBadRequest,
error: errors.New("Missing Trigger Source")}
//ErrTriggerMissingSourcePrefix - source does not have a / prefix
ErrTriggerMissingSourcePrefix = err{
code: http.StatusBadRequest,
error: errors.New("Missing Trigger Source Prefix '/'")}
//ErrTriggerNotFound - trigger not found
ErrTriggerNotFound = err{
code: http.StatusNotFound,
error: errors.New("Trigger not found")}
//ErrTriggerExists - a trigger with the specified name already exists
ErrTriggerExists = err{
code: http.StatusConflict,
error: errors.New("Trigger already exists")}
//ErrTriggerSourceExists - another trigger on the same app has the same source and type
ErrTriggerSourceExists = err{
code: http.StatusConflict,
error: errors.New("Trigger with the same type and source exists on this app")}
)
//Validate checks that trigger has valid data for inserting into a store
func (t *Trigger) Validate() error {
if t.AppID == "" {
return ErrTriggerMissingAppID
}
if err := t.ValidateName(); err != nil {
return err
}
if t.FnID == "" {
return ErrTriggerMissingFnID
}
if !ValidTriggerType(t.Type) {
return ErrTriggerTypeUnknown
}
if t.Source == "" {
return ErrTriggerMissingSource
}
if !strings.HasPrefix(t.Source, "/") {
return ErrTriggerMissingSourcePrefix
}
err := t.Annotations.Validate()
if err != nil {
return err
}
return nil
}
func (t *Trigger) ValidateName() error {
if t.Name == "" {
return ErrTriggerMissingName
}
if len(t.Name) > MaxLengthTriggerName {
return ErrTriggerTooLongName
}
for _, c := range t.Name {
if !(unicode.IsLetter(c) || unicode.IsNumber(c) || c == '_' || c == '-') {
return ErrTriggerInvalidName
}
}
return nil
}
// Clone creates a deep copy of a trigger
func (t *Trigger) Clone() *Trigger {
clone := new(Trigger)
*clone = *t // shallow copy
// annotations are immutable via their interface so can be shallow copied
return clone
}
// Update applies a change to a trigger
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())
}
}
//TriggerFilter is a search criteria on triggers
type TriggerFilter struct {
//AppID searches for triggers in APP - mandatory
AppID string // this is exact match mandatory
//FNID searches for triggers belonging to a specific function
FnID string // this is exact match
//Name is the name of the trigger
Name string // exact match
Cursor string
PerPage int
}
//TriggerList is a container of triggers returned by search, optionally indicating the next page cursor
type TriggerList struct {
NextCursor string `json:"next_cursor,omitempty"`
Items []*Trigger `json:"items"`
}