Skip to content

Commit 02830a6

Browse files
committed
Merge pull request #4175 from krancour/graceful-upgrade
feat(deisctl): add graceful upgrade
2 parents 134c51a + 16c2bd6 commit 02830a6

17 files changed

Lines changed: 660 additions & 199 deletions

File tree

deisctl/backend/backend.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type Backend interface {
1212
Start([]string, *sync.WaitGroup, io.Writer, io.Writer)
1313
Stop([]string, *sync.WaitGroup, io.Writer, io.Writer)
1414
Scale(string, int, *sync.WaitGroup, io.Writer, io.Writer)
15+
RollingRestart(string, *sync.WaitGroup, io.Writer, io.Writer)
1516
SSH(string) error
1617
SSHExec(string, string) error
1718
ListUnits() error
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package fleet
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"sync"
7+
)
8+
9+
// RollingRestart for instance units
10+
func (c *FleetClient) RollingRestart(component string, wg *sync.WaitGroup, out, ew io.Writer) {
11+
if component != "router" {
12+
fmt.Fprint(ew, "invalid component. supported for: router")
13+
return
14+
}
15+
16+
components, err := c.Units(component)
17+
if err != nil {
18+
io.WriteString(ew, err.Error())
19+
return
20+
}
21+
if len(components) < 1 {
22+
fmt.Fprint(ew, "rolling restart requires at least 1 component")
23+
return
24+
}
25+
for num := range components {
26+
unitName := fmt.Sprintf("%s@%v", component, num+1)
27+
28+
c.Stop([]string{unitName}, wg, out, ew)
29+
wg.Wait()
30+
c.Destroy([]string{unitName}, wg, out, ew)
31+
wg.Wait()
32+
c.Create([]string{unitName}, wg, out, ew)
33+
wg.Wait()
34+
c.Start([]string{unitName}, wg, out, ew)
35+
wg.Wait()
36+
}
37+
}

deisctl/client/client.go

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"github.com/deis/deis/deisctl/backend"
1010
"github.com/deis/deis/deisctl/backend/fleet"
1111
"github.com/deis/deis/deisctl/cmd"
12+
"github.com/deis/deis/deisctl/config"
13+
"github.com/deis/deis/deisctl/config/etcd"
1214
"github.com/deis/deis/deisctl/units"
1315

1416
docopt "github.com/docopt/docopt-go"
@@ -28,11 +30,15 @@ type DeisCtlClient interface {
2830
Status(argv []string) error
2931
Stop(argv []string) error
3032
Uninstall(argv []string) error
33+
UpgradePrep(argv []string) error
34+
UpgradeTakeover(argv []string) error
35+
RollingRestart(argv []string) error
3136
}
3237

3338
// Client uses a backend to implement the DeisCtlClient interface.
3439
type Client struct {
35-
Backend backend.Backend
40+
Backend backend.Backend
41+
configBackend config.Backend
3642
}
3743

3844
// NewClient returns a Client using the requested backend.
@@ -54,7 +60,56 @@ func NewClient(requestedBackend string) (*Client, error) {
5460
default:
5561
return nil, errors.New("invalid backend")
5662
}
57-
return &Client{Backend: backend}, nil
63+
64+
cb, err := etcd.NewConfigBackend()
65+
if err != nil {
66+
return nil, err
67+
}
68+
69+
return &Client{Backend: backend, configBackend: cb}, nil
70+
}
71+
72+
// UpgradePrep prepares a running cluster to be upgraded
73+
func (c *Client) UpgradePrep(argv []string) error {
74+
usage := `Prepare platform for graceful upgrade.
75+
76+
Usage:
77+
deisctl upgrade-prep [options]
78+
`
79+
if _, err := docopt.Parse(usage, argv, true, "", false); err != nil {
80+
return err
81+
}
82+
83+
return cmd.UpgradePrep(c.Backend)
84+
}
85+
86+
// UpgradeTakeover gracefully restarts a cluster prepared with upgrade-prep
87+
func (c *Client) UpgradeTakeover(argv []string) error {
88+
usage := `Complete the upgrade of a prepped cluster.
89+
90+
Usage:
91+
deisctl upgrade-takeover [options]
92+
`
93+
if _, err := docopt.Parse(usage, argv, true, "", false); err != nil {
94+
return err
95+
}
96+
97+
return cmd.UpgradeTakeover(c.Backend, c.configBackend)
98+
}
99+
100+
// RollingRestart attempts a rolling restart of an instance unit
101+
func (c *Client) RollingRestart(argv []string) error {
102+
usage := `Perform a rolling restart of an instance unit.
103+
104+
Usage:
105+
deisctl rolling-restart <target>
106+
`
107+
args, err := docopt.Parse(usage, argv, true, "", false)
108+
if err != nil {
109+
return err
110+
}
111+
112+
return cmd.RollingRestart(args["<target>"].(string), c.Backend)
58113
}
59114

60115
// Config gets or sets a configuration value from the cluster.
@@ -105,7 +160,7 @@ Examples:
105160
key = args["<key>"].([]string)
106161
}
107162

108-
return cmd.Config(args["<target>"].(string), action, key)
163+
return cmd.Config(args["<target>"].(string), action, key, c.configBackend)
109164
}
110165

111166
// Install loads the definitions of components from local unit files.
@@ -140,7 +195,7 @@ Options:
140195
}
141196
cmd.RouterMeshSize = uint8(parsedValue)
142197

143-
return cmd.Install(args["<target>"].([]string), c.Backend, cmd.CheckRequiredKeys)
198+
return cmd.Install(args["<target>"].([]string), c.Backend, c.configBackend, cmd.CheckRequiredKeys)
144199
}
145200

146201
// Journal prints log output for the specified components.

deisctl/cmd/cmd.go

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -97,14 +97,24 @@ func Start(targets []string, b backend.Backend) error {
9797
return nil
9898
}
9999

100-
// CheckRequiredKeys exist in etcd
101-
func CheckRequiredKeys() error {
102-
if err := config.CheckConfig("/deis/platform/", "domain"); err != nil {
100+
// RollingRestart restart instance unit in a rolling manner
101+
func RollingRestart(target string, b backend.Backend) error {
102+
var wg sync.WaitGroup
103+
104+
b.RollingRestart(target, &wg, Stdout, Stderr)
105+
wg.Wait()
106+
107+
return nil
108+
}
109+
110+
// CheckRequiredKeys exist in config backend
111+
func CheckRequiredKeys(cb config.Backend) error {
112+
if err := config.CheckConfig("/deis/platform/", "domain", cb); err != nil {
103113
return fmt.Errorf(`Missing platform domain, use:
104114
deisctl config platform set domain=<your-domain>`)
105115
}
106116

107-
if err := config.CheckConfig("/deis/platform/", "sshPrivateKey"); err != nil {
117+
if err := config.CheckConfig("/deis/platform/", "sshPrivateKey", cb); err != nil {
108118
fmt.Printf(`Warning: Missing sshPrivateKey, "deis run" will be unavailable. Use:
109119
deisctl config platform set sshPrivateKey=<path-to-key>
110120
`)
@@ -276,15 +286,15 @@ func Journal(targets []string, b backend.Backend) error {
276286

277287
// Install loads the definitions of components from local unit files.
278288
// After Install, the components will be available to Start.
279-
func Install(targets []string, b backend.Backend, checkKeys func() error) error {
289+
func Install(targets []string, b backend.Backend, cb config.Backend, checkKeys func(config.Backend) error) error {
280290

281291
// if target is platform, install all services
282292
if len(targets) == 1 {
283293
switch targets[0] {
284294
case PlatformCommand:
285-
return InstallPlatform(b, checkKeys, false)
295+
return InstallPlatform(b, cb, checkKeys, false)
286296
case StatelessPlatformCommand:
287-
return InstallPlatform(b, checkKeys, true)
297+
return InstallPlatform(b, cb, checkKeys, true)
288298
case mesos:
289299
return InstallMesos(b)
290300
case swarm:
@@ -429,11 +439,11 @@ func splitScaleTarget(target string) (c string, num int, err error) {
429439

430440
// Config gets or sets a configuration value from the cluster.
431441
//
432-
// A configuration value is stored and retrieved from a key/value store (in this case, etcd)
442+
// A configuration value is stored and retrieved from a key/value store
433443
// at /deis/<component>/<config>. Configuration values are typically used for component-level
434444
// configuration, such as enabling TLS for the routers.
435-
func Config(target string, action string, key []string) error {
436-
if err := config.Config(target, action, key); err != nil {
445+
func Config(target string, action string, key []string, cb config.Backend) error {
446+
if err := config.Config(target, action, key, cb); err != nil {
437447
return err
438448
}
439449
return nil

deisctl/cmd/cmd_test.go

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import (
1313
"testing"
1414

1515
"github.com/deis/deis/deisctl/backend"
16+
"github.com/deis/deis/deisctl/config"
17+
"github.com/deis/deis/deisctl/config/model"
18+
"github.com/deis/deis/deisctl/test/mock"
1619
"github.com/deis/deis/deisctl/units"
1720
)
1821

@@ -21,6 +24,7 @@ type backendStub struct {
2124
stoppedUnits []string
2225
installedUnits []string
2326
uninstalledUnits []string
27+
restartedUnits []string
2428
expected bool
2529
}
2630

@@ -46,6 +50,10 @@ func (backend *backendStub) Scale(component string, num int, wg *sync.WaitGroup,
4650
backend.expected = false
4751
}
4852
}
53+
func (backend *backendStub) RollingRestart(target string, wg *sync.WaitGroup, out, ew io.Writer) {
54+
backend.restartedUnits = append(backend.restartedUnits, target)
55+
}
56+
4957
func (backend *backendStub) ListUnits() error {
5058
return nil
5159
}
@@ -79,7 +87,7 @@ func (backend *backendStub) SSHExec(target, command string) error {
7987

8088
var _ backend.Backend = &backendStub{}
8189

82-
func fakeCheckKeys() error {
90+
func fakeCheckKeys(cb config.Backend) error {
8391
return nil
8492
}
8593

@@ -270,6 +278,57 @@ func TestStartSwarm(t *testing.T) {
270278
}
271279
}
272280

281+
func TestRollingRestart(t *testing.T) {
282+
t.Parallel()
283+
284+
b := backendStub{}
285+
expected := []string{"router"}
286+
287+
RollingRestart("router", &b)
288+
289+
if !reflect.DeepEqual(b.restartedUnits, expected) {
290+
t.Error(fmt.Errorf("Expected %v, Got %v", expected, b.restartedUnits))
291+
}
292+
}
293+
294+
func TestUpgradePrep(t *testing.T) {
295+
t.Parallel()
296+
297+
b := backendStub{}
298+
expected := []string{"database", "registry@*", "controller", "builder", "logger", "logspout", "store-volume",
299+
"store-gateway@*", "store-metadata", "store-daemon", "store-monitor"}
300+
301+
UpgradePrep(&b)
302+
303+
if !reflect.DeepEqual(b.stoppedUnits, expected) {
304+
t.Error(fmt.Errorf("Expected %v, Got %v", expected, b.stoppedUnits))
305+
}
306+
}
307+
308+
func TestUpgradeTakeover(t *testing.T) {
309+
t.Parallel()
310+
testMock := mock.ConfigBackend{Expected: []*model.ConfigNode{{Key: "/deis/services/app1", Value: "foo", TTL: 10},
311+
{Key: "/deis/services/app2", Value: "8000", TTL: 10}}}
312+
313+
b := backendStub{}
314+
expectedRestarted := []string{"router"}
315+
expectedStarted := []string{"publisher", "store-monitor", "store-daemon", "store-metadata",
316+
"store-gateway@*", "store-volume", "logger", "logspout", "database", "registry@*",
317+
"controller", "builder", "publisher", "router@*", "database", "registry@*",
318+
"controller", "builder", "publisher", "router@*"}
319+
320+
if err := doUpgradeTakeOver(&b, testMock); err != nil {
321+
t.Error(fmt.Errorf("Takeover failed: %v", err))
322+
}
323+
324+
if !reflect.DeepEqual(b.restartedUnits, expectedRestarted) {
325+
t.Error(fmt.Errorf("Expected %v, Got %v", expectedRestarted, b.restartedUnits))
326+
}
327+
if !reflect.DeepEqual(b.startedUnits, expectedStarted) {
328+
t.Error(fmt.Errorf("Expected %v, Got %v", expectedStarted, b.startedUnits))
329+
}
330+
}
331+
273332
func TestStop(t *testing.T) {
274333
t.Parallel()
275334

@@ -419,9 +478,11 @@ func TestInstall(t *testing.T) {
419478
t.Parallel()
420479

421480
b := backendStub{}
481+
cb := mock.ConfigBackend{}
482+
422483
expected := []string{"router@1", "router@2"}
423484

424-
Install(expected, &b, fakeCheckKeys)
485+
Install(expected, &b, &cb, fakeCheckKeys)
425486

426487
if !reflect.DeepEqual(b.installedUnits, expected) {
427488
t.Error(fmt.Errorf("Expected %v, Got %v", expected, b.installedUnits))
@@ -432,11 +493,13 @@ func TestInstallPlatform(t *testing.T) {
432493
t.Parallel()
433494

434495
b := backendStub{}
496+
cb := mock.ConfigBackend{}
497+
435498
expected := []string{"store-daemon", "store-monitor", "store-metadata", "store-volume",
436499
"store-gateway@1", "logger", "logspout", "database", "registry@1",
437500
"controller", "builder", "publisher", "router@1", "router@2", "router@3"}
438501

439-
Install([]string{"platform"}, &b, fakeCheckKeys)
502+
Install([]string{"platform"}, &b, &cb, fakeCheckKeys)
440503

441504
if !reflect.DeepEqual(b.installedUnits, expected) {
442505
t.Error(fmt.Errorf("Expected %v, Got %v", expected, b.installedUnits))
@@ -447,12 +510,14 @@ func TestInstallPlatformWithCustomRouterMeshSize(t *testing.T) {
447510
t.Parallel()
448511

449512
b := backendStub{}
513+
cb := mock.ConfigBackend{}
514+
450515
expected := []string{"store-daemon", "store-monitor", "store-metadata", "store-volume",
451516
"store-gateway@1", "logger", "logspout", "database", "registry@1",
452517
"controller", "builder", "publisher", "router@1", "router@2", "router@3", "router@4", "router@5"}
453518
RouterMeshSize = 5
454519

455-
Install([]string{"platform"}, &b, fakeCheckKeys)
520+
Install([]string{"platform"}, &b, &cb, fakeCheckKeys)
456521
RouterMeshSize = DefaultRouterMeshSize
457522

458523
if !reflect.DeepEqual(b.installedUnits, expected) {
@@ -464,10 +529,12 @@ func TestInstallStatelessPlatform(t *testing.T) {
464529
t.Parallel()
465530

466531
b := backendStub{}
532+
cb := mock.ConfigBackend{}
533+
467534
expected := []string{"logspout", "registry@1",
468535
"controller", "builder", "publisher", "router@1", "router@2", "router@3"}
469536

470-
Install([]string{"stateless-platform"}, &b, fakeCheckKeys)
537+
Install([]string{"stateless-platform"}, &b, &cb, fakeCheckKeys)
471538

472539
if !reflect.DeepEqual(b.installedUnits, expected) {
473540
t.Error(fmt.Errorf("Expected %v, Got %v", expected, b.installedUnits))
@@ -478,9 +545,11 @@ func TestInstallSwarm(t *testing.T) {
478545
t.Parallel()
479546

480547
b := backendStub{}
548+
cb := mock.ConfigBackend{}
549+
481550
expected := []string{"swarm-node", "swarm-manager"}
482551

483-
Install([]string{"swarm"}, &b, fakeCheckKeys)
552+
Install([]string{"swarm"}, &b, &cb, fakeCheckKeys)
484553

485554
if !reflect.DeepEqual(b.installedUnits, expected) {
486555
t.Error(fmt.Errorf("Expected %v, Got %v", expected, b.installedUnits))

deisctl/cmd/platform.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ import (
66
"sync"
77

88
"github.com/deis/deis/deisctl/backend"
9+
"github.com/deis/deis/deisctl/config"
910
"github.com/deis/deis/pkg/prettyprint"
1011
)
1112

1213
// InstallPlatform loads all components' definitions from local unit files.
1314
// After InstallPlatform, all components will be available for StartPlatform.
14-
func InstallPlatform(b backend.Backend, checkKeys func() error, stateless bool) error {
15+
func InstallPlatform(b backend.Backend, cb config.Backend, checkKeys func(config.Backend) error, stateless bool) error {
1516

16-
if err := checkKeys(); err != nil {
17+
if err := checkKeys(cb); err != nil {
1718
return err
1819
}
1920

0 commit comments

Comments
 (0)