-
Notifications
You must be signed in to change notification settings - Fork 158
/
apps.go
280 lines (219 loc) · 6.04 KB
/
apps.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
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
package empire
import (
"database/sql/driver"
"encoding/json"
"errors"
"fmt"
"regexp"
"strings"
"time"
"github.com/jinzhu/gorm"
"github.com/remind101/empire/pkg/timex"
"golang.org/x/net/context"
)
const (
exposePrivate = "private"
exposePublic = "public"
)
// NamePattern is a regex pattern that app names must conform to.
var NamePattern = regexp.MustCompile(`^[a-z][a-z0-9-]{2,30}$`)
// appNameFromRepo generates a name from a Repo
//
// remind101/r101-api => r101-api
func appNameFromRepo(repo string) string {
p := strings.Split(repo, "/")
return p[len(p)-1]
}
// Certs maps a process name to a certificate to use for any SSL listeners.
type Certs map[string]string
// Scan implements the sql.Scanner interface.
func (c *Certs) Scan(src interface{}) error {
bytes, ok := src.([]byte)
if !ok {
return error(errors.New("Scan source was not []bytes"))
}
certs := make(Certs)
if err := json.Unmarshal(bytes, &certs); err != nil {
return err
}
*c = certs
return nil
}
// Value implements the driver.Value interface.
func (c Certs) Value() (driver.Value, error) {
if c == nil {
return nil, nil
}
raw, err := json.Marshal(c)
if err != nil {
return nil, err
}
return driver.Value(raw), nil
}
// App represents an Empire application.
type App struct {
// A unique uuid that identifies the application.
ID string
// The name of the application.
Name string
// If provided, the Docker repo that this application is linked to.
// Deployments to Empire, which don't specify an application, will use
// this field to determine what app an image should be deployed to.
Repo *string
// Valid values are exposePrivate and exposePublic.
Exposure string
// Maps a process name to an SSL certificate to use for the SSL listener
// of the load balancer.
Certs Certs
// The time that this application was created.
CreatedAt *time.Time
// If this is non-nil, the app was deleted at this time.
DeletedAt *time.Time
// Maintenance defines whether the app is in maintenance mode or not.
Maintenance bool
}
// IsValid returns an error if the app isn't valid.
func (a *App) IsValid() error {
if !NamePattern.Match([]byte(a.Name)) {
return ErrInvalidName
}
return nil
}
func (a *App) BeforeCreate() error {
t := timex.Now()
a.CreatedAt = &t
if a.Exposure == "" {
a.Exposure = exposePrivate
}
return a.IsValid()
}
// AppsQuery is a scope implementation for common things to filter releases
// by.
type AppsQuery struct {
// If provided, an App ID to find.
ID *string
// If provided, finds apps matching the given name.
Name *string
// If provided, finds apps with the given repo attached.
Repo *string
}
// scope implements the scope interface.
func (q AppsQuery) scope(db *gorm.DB) *gorm.DB {
scope := composedScope{isNull("deleted_at")}
if q.ID != nil {
scope = append(scope, idEquals(*q.ID))
}
if q.Name != nil {
scope = append(scope, fieldEquals("name", *q.Name))
}
if q.Repo != nil {
scope = append(scope, fieldEquals("repo", *q.Repo))
}
return scope.scope(db)
}
type appsService struct {
*Empire
}
// Destroy destroys removes an app from the scheduler, then destroys it here.
func (s *appsService) Destroy(ctx context.Context, db *gorm.DB, app *App) error {
if err := appsDestroy(db, app); err != nil {
return err
}
return s.Scheduler.Remove(ctx, app.ID)
}
func (s *appsService) Restart(ctx context.Context, db *gorm.DB, opts RestartOpts) error {
if opts.PID != "" {
return s.Scheduler.Stop(ctx, opts.PID)
}
return s.releases.Restart(ctx, db, opts.App)
}
func (s *appsService) Scale(ctx context.Context, db *gorm.DB, opts ScaleOpts) ([]*Process, error) {
app := opts.App
release, err := releasesFind(db, ReleasesQuery{App: app})
if err != nil {
return nil, err
}
if release == nil {
return nil, &ValidationError{Err: fmt.Errorf("no releases for %s", app.Name)}
}
event := opts.Event()
var ps []*Process
for i, up := range opts.Updates {
t, q, c := up.Process, up.Quantity, up.Constraints
p, ok := release.Formation[t]
if !ok {
return nil, &ValidationError{Err: fmt.Errorf("no %s process type in release", t)}
}
eventUpdate := event.Updates[i]
eventUpdate.PreviousQuantity = p.Quantity
eventUpdate.PreviousConstraints = p.Constraints()
// Update quantity for this process in the formation
p.Quantity = q
if c != nil {
p.SetConstraints(*c)
}
release.Formation[t] = p
ps = append(ps, &p)
}
// Save the new formation.
if err := releasesUpdate(db, release); err != nil {
return nil, err
}
err = s.releases.Release(ctx, release, nil)
if err != nil {
return ps, err
}
return ps, s.PublishEvent(event)
}
// appsEnsureRepo will set the repo if it's not set.
func appsEnsureRepo(db *gorm.DB, app *App, repo string) error {
if app.Repo != nil {
return nil
}
app.Repo = &repo
return appsUpdate(db, app)
}
// appsFindOrCreateByRepo first attempts to find an app by repo, falling back to
// creating a new app.
func appsFindOrCreateByRepo(db *gorm.DB, repo string) (*App, error) {
n := appNameFromRepo(repo)
a, err := appsFind(db, AppsQuery{Name: &n})
if err != nil && err != gorm.RecordNotFound {
return a, err
}
// If the app wasn't found, create a new app.
if err != gorm.RecordNotFound {
return a, appsEnsureRepo(db, a, repo)
}
a = &App{
Name: n,
Repo: &repo,
}
return appsCreate(db, a)
}
// appsFind finds a single app given the scope.
func appsFind(db *gorm.DB, scope scope) (*App, error) {
var app App
return &app, first(db, scope, &app)
}
// apps finds all apps matching the scope.
func apps(db *gorm.DB, scope scope) ([]*App, error) {
var apps []*App
// Default to ordering by name.
scope = composedScope{order("name"), scope}
return apps, find(db, scope, &apps)
}
// appsCreate inserts the app into the database.
func appsCreate(db *gorm.DB, app *App) (*App, error) {
return app, db.Create(app).Error
}
// appsUpdate updates an app.
func appsUpdate(db *gorm.DB, app *App) error {
return db.Save(app).Error
}
// appsDestroy destroys an app.
func appsDestroy(db *gorm.DB, app *App) error {
now := timex.Now()
app.DeletedAt = &now
return appsUpdate(db, app)
}