Skip to content

Commit

Permalink
Merge pull request #1086 from remind101/maintenance
Browse files Browse the repository at this point in the history
Maintenance mode
  • Loading branch information
ejholmes authored Jun 9, 2017
2 parents 53fce11 + 979174e commit df1b2bd
Show file tree
Hide file tree
Showing 19 changed files with 322 additions and 15 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
**Features**

* Empire now supports a new (experimental) feature to enable attached processes to be ran with ECS. [#1043](https://github.com/remind101/empire/pull/1043)
* Empire now supports "maintenance mode" for applications. [#1086](https://github.com/remind101/empire/pull/1086)

**Bugs**

Expand Down
3 changes: 3 additions & 0 deletions apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ type App struct {

// The time that this application was created.
CreatedAt *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.
Expand Down
2 changes: 1 addition & 1 deletion certs.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func (s *certsService) CertsAttach(ctx context.Context, db *gorm.DB, opts CertsA
return err
}

if err := s.releases.Restart(ctx, db, app); err != nil {
if err := s.releases.ReleaseApp(ctx, db, app, nil); err != nil {
if err == ErrNoReleases {
return nil
}
Expand Down
3 changes: 2 additions & 1 deletion cmd/emp/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func runInfo(cmd *Command, args []string) {
app, err := client.AppInfo(mustApp())
must(err)
fmt.Printf("Name: %s\n", app.Name)
fmt.Printf("ID: %s\n", app.Id)
fmt.Printf("ID: %s\n", app.Id)
fmt.Printf("Maintenance: %s\n", fmtMaintenance(app.Maintenance))
fmt.Printf("Cert: %s\n", app.Cert)
}
3 changes: 3 additions & 0 deletions cmd/emp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ var commands = []*Command{
cmdGet,
cmdLogin,
cmdLogout,
cmdMaintenance,
cmdMaintenanceEnable,
cmdMaintenanceDisable,
cmdSSL,
cmdSSLCertAdd,
cmdSSLCertRollback,
Expand Down
94 changes: 94 additions & 0 deletions cmd/emp/maintenance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package main

import (
"fmt"
"log"
"os"

"github.com/remind101/empire/pkg/heroku"
)

var cmdMaintenance = &Command{
Run: runMaintenance,
Usage: "maintenance",
NeedsApp: true,
Category: "app",
Short: "show app maintenance mode" + extra,
Long: `
Maintenance shows the current maintenance mode state of an app.
Example:
$ emp maintenance -a <myapp>
enabled
`,
}

func runMaintenance(cmd *Command, args []string) {
if len(args) != 0 {
cmd.PrintUsage()
os.Exit(2)
}
app, err := client.AppInfo(mustApp())
must(err)
fmt.Println(fmtMaintenance(app.Maintenance))
}

var cmdMaintenanceEnable = &Command{
Run: maybeMessage(runMaintenanceEnable),
Usage: "maintenance-enable",
NeedsApp: true,
Category: "app",
Short: "enable maintenance mode" + extra,
Long: `
Enables maintenance mode on an app.
Example:
$ emp maintenance-enable -a <myapp>
Enabled maintenance mode on myapp.
`,
}

func runMaintenanceEnable(cmd *Command, args []string) {
message := getMessage()
if len(args) != 0 {
cmd.PrintUsage()
os.Exit(2)
}
newmode := true
app, err := client.AppUpdate(mustApp(), &heroku.AppUpdateOpts{Maintenance: &newmode}, message)
must(err)
log.Printf("Enabled maintenance mode on %s.", app.Name)
}

var cmdMaintenanceDisable = &Command{
Run: maybeMessage(runMaintenanceDisable),
Usage: "maintenance-disable",
NeedsApp: true,
Category: "app",
Short: "disable maintenance mode" + extra,
Long: `
Disables maintenance mode on an app.
Example:
$ emp maintenance-disable -a <myapp>
Disabled maintenance mode on myapp.
`,
}

func runMaintenanceDisable(cmd *Command, args []string) {
message := getMessage()
if len(args) != 0 {
cmd.PrintUsage()
os.Exit(2)
}
newmode := false
app, err := client.AppUpdate(mustApp(), &heroku.AppUpdateOpts{Maintenance: &newmode}, message)
must(err)
log.Printf("Disabled maintenance mode on %s.", app.Name)
}

type fmtMaintenance bool

func (f fmtMaintenance) String() string {
if f {
return "enabled"
}
return "disabled"
}
3 changes: 2 additions & 1 deletion cmd/emp/rename.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ Example:
}

func runRename(cmd *Command, args []string) {
message := getMessage()
cmd.AssertNumArgsCorrect(args)

oldname, newname := args[0], args[1]
app, err := client.AppUpdate(oldname, &heroku.AppUpdateOpts{Name: &newname})
app, err := client.AppUpdate(oldname, &heroku.AppUpdateOpts{Name: &newname}, message)
must(err)
log.Printf("Renamed %s to %s.", oldname, app.Name)
log.Println("Ensure you update your git remote URL.")
Expand Down
63 changes: 63 additions & 0 deletions empire.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,69 @@ func (e *Empire) Config(app *App) (*Config, error) {
return c, nil
}

type SetMaintenanceModeOpts struct {
// User performing the action.
User *User

// The associated app.
App *App

// Wheather maintenance mode should be enabled or not.
Maintenance bool

// Commit message
Message string
}

func (opts SetMaintenanceModeOpts) Event() MaintenanceEvent {
return MaintenanceEvent{
User: opts.User.Name,
App: opts.App.Name,
Maintenance: opts.Maintenance,
Message: opts.Message,
}
}

func (opts SetMaintenanceModeOpts) Validate(e *Empire) error {
return e.requireMessages(opts.Message)
}

// SetMaintenanceMode enables or disables "maintenance mode" on the app. When an
// app is in maintenance mode, all processes will be scaled down to 0. When
// taken out of maintenance mode, all processes will be scaled up back to their
// existing values.
func (e *Empire) SetMaintenanceMode(ctx context.Context, opts SetMaintenanceModeOpts) error {
if err := opts.Validate(e); err != nil {
return err
}

tx := e.db.Begin()

app := opts.App

app.Maintenance = opts.Maintenance

if err := appsUpdate(tx, app); err != nil {
tx.Rollback()
return err
}

if err := e.releases.ReleaseApp(ctx, tx, app, nil); err != nil {
tx.Rollback()
if err == ErrNoReleases {
return nil
}

return err
}

if err := tx.Commit().Error; err != nil {
return err
}

return e.PublishEvent(opts.Event())
}

// SetOpts are options provided when setting new config vars on an app.
type SetOpts struct {
// User performing the action.
Expand Down
26 changes: 26 additions & 0 deletions events.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,32 @@ func (e RestartEvent) GetApp() *App {
return e.app
}

type MaintenanceEvent struct {
User string
App string
Maintenance bool
Message string

app *App
}

func (e MaintenanceEvent) Event() string {
return "maintenance"
}

func (e MaintenanceEvent) String() string {
state := "disabled"
if e.Maintenance {
state = "enabled"
}
msg := fmt.Sprintf("%s %s maintenance mode on %s", e.User, state, e.App)
return appendCommitMessage(msg, e.Message)
}

func (e MaintenanceEvent) GetApp() *App {
return e.app
}

type ScaleEventUpdate struct {
Process string
Quantity int
Expand Down
5 changes: 5 additions & 0 deletions events_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ func TestEvents_String(t *testing.T) {
{RestartEvent{User: "ejholmes", App: "acme-inc", Message: "commit message"}, "ejholmes restarted acme-inc: 'commit message'"},
{RestartEvent{User: "ejholmes", App: "acme-inc", PID: "abcd", Message: "commit message"}, "ejholmes restarted `abcd` on acme-inc: 'commit message'"},

// MaintenanceEvent
{MaintenanceEvent{User: "ejholmes", App: "acme-inc", Maintenance: false}, "ejholmes disabled maintenance mode on acme-inc"},
{MaintenanceEvent{User: "ejholmes", App: "acme-inc", Maintenance: true}, "ejholmes enabled maintenance mode on acme-inc"},
{MaintenanceEvent{User: "ejholmes", App: "acme-inc", Maintenance: true, Message: "upgrading db"}, "ejholmes enabled maintenance mode on acme-inc: 'upgrading db'"},

// ScaleEvent
{ScaleEvent{
User: "ejholmes",
Expand Down
17 changes: 17 additions & 0 deletions migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,23 @@ ALTER TABLE apps ADD COLUMN exposure TEXT NOT NULL default 'private'`,
`ALTER TABLE apps ADD COLUMN cert text`,
}),
},

// This migration migrates the cert storage from a single string column
// to a mapping of process name to cert name.
{
ID: 20,
Up: func(tx *sql.Tx) error {
_, err := tx.Exec(`ALTER TABLE apps ADD COLUMN maintenance bool NOT NULL DEFAULT false`)
if err != nil {
return fmt.Errorf("error adding maintenance column: %v", err)
}

return nil
},
Down: migrate.Queries([]string{
`ALTER TABLE apps DROP COLUMN maintenance`,
}),
},
}

// latestSchema returns the schema version that this version of Empire should be
Expand Down
2 changes: 1 addition & 1 deletion migrations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func TestMigrations(t *testing.T) {
}

func TestLatestSchema(t *testing.T) {
assert.Equal(t, 19, latestSchema())
assert.Equal(t, 20, latestSchema())
}

func TestNoDuplicateMigrations(t *testing.T) {
Expand Down
5 changes: 3 additions & 2 deletions pkg/heroku/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,10 @@ func (c *Client) AppList(lr *ListRange) ([]App, error) {
//
// appIdentity is the unique identifier of the App. options is the struct of
// optional parameters for this action.
func (c *Client) AppUpdate(appIdentity string, options *AppUpdateOpts) (*App, error) {
func (c *Client) AppUpdate(appIdentity string, options *AppUpdateOpts, message string) (*App, error) {
rh := RequestHeaders{CommitMessage: message}
var appRes App
return &appRes, c.Patch(&appRes, "/apps/"+appIdentity, options)
return &appRes, c.PatchWithHeaders(&appRes, "/apps/"+appIdentity, options, rh.Headers())
}

// AppUpdateOpts holds the optional parameters for AppUpdate
Expand Down
2 changes: 1 addition & 1 deletion pkg/heroku/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ func TestAppUpdateSuccess(t *testing.T) {
ts, handler, c := newTestServerAndClient(t, appUpdateRequest)
defer ts.Close()

app, err := c.AppUpdate("example", &appUpdateRequestOptions)
app, err := c.AppUpdate("example", &appUpdateRequestOptions, "message")
if err != nil {
t.Fatal(err)
}
Expand Down
24 changes: 23 additions & 1 deletion releases.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,23 @@ func (s *releasesService) Release(ctx context.Context, release *Release, ss twel
return s.Scheduler.Submit(ctx, a, ss)
}

func (s *releasesService) ReleaseApp(ctx context.Context, db *gorm.DB, app *App, ss twelvefactor.StatusStream) error {
release, err := releasesFind(db, ReleasesQuery{App: app})
if err != nil {
if err == gorm.RecordNotFound {
return ErrNoReleases
}

return err
}

if release == nil {
return nil
}

return s.Release(ctx, release, ss)
}

// Restart will find the last release for an app and submit it to the scheduler
// to restart the app.
func (s *releasesService) Restart(ctx context.Context, db *gorm.DB, app *App) error {
Expand Down Expand Up @@ -361,13 +378,18 @@ func newSchedulerProcess(release *Release, name string, p Process) (*twelvefacto
}
}

quantity := p.Quantity
if release.App.Maintenance {
quantity = 0
}

return &twelvefactor.Process{
Type: name,
Env: env,
Labels: labels,
Command: []string(p.Command),
Image: release.Slug.Image,
Quantity: p.Quantity,
Quantity: quantity,
Memory: uint(p.Memory),
CPUShares: uint(p.CPUShare),
Nproc: uint(p.Nproc),
Expand Down
Loading

0 comments on commit df1b2bd

Please sign in to comment.