forked from remind101/empire
-
Notifications
You must be signed in to change notification settings - Fork 0
/
apps.go
244 lines (190 loc) · 5.25 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
package empire
import (
"errors"
"fmt"
"regexp"
"strings"
"time"
"github.com/jinzhu/gorm"
"github.com/remind101/pkg/timex"
"golang.org/x/net/context"
)
const (
ExposePrivate = "private"
ExposePublic = "public"
)
var (
// ErrInvalidName is used to indicate that the app name is not valid.
ErrInvalidName = &ValidationError{
errors.New("An app name must be alphanumeric and dashes only, 3-30 chars in length."),
}
)
// 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]
}
// App represents an app.
type App struct {
ID string
Name string
Repo *string
// Valid values are empire.ExposePrivate and empire.ExposePublic.
Exposure string
// The name of an SSL cert for the web process of this app.
Cert string
CreatedAt *time.Time
}
// 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 {
var scope ComposedScope
if q.ID != nil {
scope = append(scope, ID(*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)
}
// AppID returns a scope to find an app by id.
func AppID(id string) func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Where("id = ?", id)
}
}
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.ReleaseApp(ctx, db, opts.App)
}
func (s *appsService) Scale(ctx context.Context, db *gorm.DB, opts ScaleOpts) (*Process, error) {
app, t, quantity, c := opts.App, opts.Process, opts.Quantity, opts.Constraints
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)}
}
p := release.Process(t)
if p == nil {
return nil, &ValidationError{Err: fmt.Errorf("no %s process type in release", t)}
}
if err := s.Scheduler.Scale(ctx, release.AppID, string(p.Type), uint(quantity)); err != nil {
return nil, err
}
event := opts.Event()
event.PreviousQuantity = p.Quantity
// Update quantity for this process in the formation
p.Quantity = quantity
if c != nil {
p.Constraints = *c
}
if err := processesUpdate(db, p); err != nil {
return nil, err
}
// If there are no changes to the process size, we can do a quick scale
// up, otherwise, we will resubmit the release to the scheduler.
if c == nil {
err = s.Scheduler.Scale(ctx, release.AppID, string(p.Type), uint(quantity))
} else {
err = s.releases.Release(ctx, release)
}
if err != nil {
return p, err
}
return p, 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 {
return db.Delete(app).Error
}