/apps.go

https://github.com/remind101/empire · Go · 280 lines · 180 code · 61 blank · 39 comment · 40 complexity · d6ea9d63702b0e403f141f263715c94f MD5 · raw file

  1. package empire
  2. import (
  3. "database/sql/driver"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "regexp"
  8. "strings"
  9. "time"
  10. "github.com/jinzhu/gorm"
  11. "github.com/remind101/empire/pkg/timex"
  12. "golang.org/x/net/context"
  13. )
  14. const (
  15. exposePrivate = "private"
  16. exposePublic = "public"
  17. )
  18. // NamePattern is a regex pattern that app names must conform to.
  19. var NamePattern = regexp.MustCompile(`^[a-z][a-z0-9-]{2,30}$`)
  20. // appNameFromRepo generates a name from a Repo
  21. //
  22. // remind101/r101-api => r101-api
  23. func appNameFromRepo(repo string) string {
  24. p := strings.Split(repo, "/")
  25. return p[len(p)-1]
  26. }
  27. // Certs maps a process name to a certificate to use for any SSL listeners.
  28. type Certs map[string]string
  29. // Scan implements the sql.Scanner interface.
  30. func (c *Certs) Scan(src interface{}) error {
  31. bytes, ok := src.([]byte)
  32. if !ok {
  33. return error(errors.New("Scan source was not []bytes"))
  34. }
  35. certs := make(Certs)
  36. if err := json.Unmarshal(bytes, &certs); err != nil {
  37. return err
  38. }
  39. *c = certs
  40. return nil
  41. }
  42. // Value implements the driver.Value interface.
  43. func (c Certs) Value() (driver.Value, error) {
  44. if c == nil {
  45. return nil, nil
  46. }
  47. raw, err := json.Marshal(c)
  48. if err != nil {
  49. return nil, err
  50. }
  51. return driver.Value(raw), nil
  52. }
  53. // App represents an Empire application.
  54. type App struct {
  55. // A unique uuid that identifies the application.
  56. ID string
  57. // The name of the application.
  58. Name string
  59. // If provided, the Docker repo that this application is linked to.
  60. // Deployments to Empire, which don't specify an application, will use
  61. // this field to determine what app an image should be deployed to.
  62. Repo *string
  63. // Valid values are exposePrivate and exposePublic.
  64. Exposure string
  65. // Maps a process name to an SSL certificate to use for the SSL listener
  66. // of the load balancer.
  67. Certs Certs
  68. // The time that this application was created.
  69. CreatedAt *time.Time
  70. // If this is non-nil, the app was deleted at this time.
  71. DeletedAt *time.Time
  72. // Maintenance defines whether the app is in maintenance mode or not.
  73. Maintenance bool
  74. }
  75. // IsValid returns an error if the app isn't valid.
  76. func (a *App) IsValid() error {
  77. if !NamePattern.Match([]byte(a.Name)) {
  78. return ErrInvalidName
  79. }
  80. return nil
  81. }
  82. func (a *App) BeforeCreate() error {
  83. t := timex.Now()
  84. a.CreatedAt = &t
  85. if a.Exposure == "" {
  86. a.Exposure = exposePrivate
  87. }
  88. return a.IsValid()
  89. }
  90. // AppsQuery is a scope implementation for common things to filter releases
  91. // by.
  92. type AppsQuery struct {
  93. // If provided, an App ID to find.
  94. ID *string
  95. // If provided, finds apps matching the given name.
  96. Name *string
  97. // If provided, finds apps with the given repo attached.
  98. Repo *string
  99. }
  100. // scope implements the scope interface.
  101. func (q AppsQuery) scope(db *gorm.DB) *gorm.DB {
  102. scope := composedScope{isNull("deleted_at")}
  103. if q.ID != nil {
  104. scope = append(scope, idEquals(*q.ID))
  105. }
  106. if q.Name != nil {
  107. scope = append(scope, fieldEquals("name", *q.Name))
  108. }
  109. if q.Repo != nil {
  110. scope = append(scope, fieldEquals("repo", *q.Repo))
  111. }
  112. return scope.scope(db)
  113. }
  114. type appsService struct {
  115. *Empire
  116. }
  117. // Destroy destroys removes an app from the scheduler, then destroys it here.
  118. func (s *appsService) Destroy(ctx context.Context, db *gorm.DB, app *App) error {
  119. if err := appsDestroy(db, app); err != nil {
  120. return err
  121. }
  122. return s.Scheduler.Remove(ctx, app.ID)
  123. }
  124. func (s *appsService) Restart(ctx context.Context, db *gorm.DB, opts RestartOpts) error {
  125. if opts.PID != "" {
  126. return s.Scheduler.Stop(ctx, opts.PID)
  127. }
  128. return s.releases.Restart(ctx, db, opts.App)
  129. }
  130. func (s *appsService) Scale(ctx context.Context, db *gorm.DB, opts ScaleOpts) ([]*Process, error) {
  131. app := opts.App
  132. release, err := releasesFind(db, ReleasesQuery{App: app})
  133. if err != nil {
  134. return nil, err
  135. }
  136. if release == nil {
  137. return nil, &ValidationError{Err: fmt.Errorf("no releases for %s", app.Name)}
  138. }
  139. event := opts.Event()
  140. var ps []*Process
  141. for i, up := range opts.Updates {
  142. t, q, c := up.Process, up.Quantity, up.Constraints
  143. p, ok := release.Formation[t]
  144. if !ok {
  145. return nil, &ValidationError{Err: fmt.Errorf("no %s process type in release", t)}
  146. }
  147. eventUpdate := event.Updates[i]
  148. eventUpdate.PreviousQuantity = p.Quantity
  149. eventUpdate.PreviousConstraints = p.Constraints()
  150. // Update quantity for this process in the formation
  151. p.Quantity = q
  152. if c != nil {
  153. p.SetConstraints(*c)
  154. }
  155. release.Formation[t] = p
  156. ps = append(ps, &p)
  157. }
  158. // Save the new formation.
  159. if err := releasesUpdate(db, release); err != nil {
  160. return nil, err
  161. }
  162. err = s.releases.Release(ctx, release, nil)
  163. if err != nil {
  164. return ps, err
  165. }
  166. return ps, s.PublishEvent(event)
  167. }
  168. // appsEnsureRepo will set the repo if it's not set.
  169. func appsEnsureRepo(db *gorm.DB, app *App, repo string) error {
  170. if app.Repo != nil {
  171. return nil
  172. }
  173. app.Repo = &repo
  174. return appsUpdate(db, app)
  175. }
  176. // appsFindOrCreateByRepo first attempts to find an app by repo, falling back to
  177. // creating a new app.
  178. func appsFindOrCreateByRepo(db *gorm.DB, repo string) (*App, error) {
  179. n := appNameFromRepo(repo)
  180. a, err := appsFind(db, AppsQuery{Name: &n})
  181. if err != nil && err != gorm.RecordNotFound {
  182. return a, err
  183. }
  184. // If the app wasn't found, create a new app.
  185. if err != gorm.RecordNotFound {
  186. return a, appsEnsureRepo(db, a, repo)
  187. }
  188. a = &App{
  189. Name: n,
  190. Repo: &repo,
  191. }
  192. return appsCreate(db, a)
  193. }
  194. // appsFind finds a single app given the scope.
  195. func appsFind(db *gorm.DB, scope scope) (*App, error) {
  196. var app App
  197. return &app, first(db, scope, &app)
  198. }
  199. // apps finds all apps matching the scope.
  200. func apps(db *gorm.DB, scope scope) ([]*App, error) {
  201. var apps []*App
  202. // Default to ordering by name.
  203. scope = composedScope{order("name"), scope}
  204. return apps, find(db, scope, &apps)
  205. }
  206. // appsCreate inserts the app into the database.
  207. func appsCreate(db *gorm.DB, app *App) (*App, error) {
  208. return app, db.Create(app).Error
  209. }
  210. // appsUpdate updates an app.
  211. func appsUpdate(db *gorm.DB, app *App) error {
  212. return db.Save(app).Error
  213. }
  214. // appsDestroy destroys an app.
  215. func appsDestroy(db *gorm.DB, app *App) error {
  216. now := timex.Now()
  217. app.DeletedAt = &now
  218. return appsUpdate(db, app)
  219. }