Skip to content

Commit bc4dd8e

Browse files
iancoffeyKent Rancourt
authored andcommitted
feature(deisctl) allow for graceful in-place upgrades
1 parent bf94812 commit bc4dd8e

14 files changed

Lines changed: 482 additions & 62 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: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ type DeisCtlClient interface {
2828
Status(argv []string) error
2929
Stop(argv []string) error
3030
Uninstall(argv []string) error
31+
UpgradePrep(argv []string) error
32+
UpgradeTakeover(argv []string) error
33+
RollingRestart(argv []string) error
3134
}
3235

3336
// Client uses a backend to implement the DeisCtlClient interface.
@@ -57,6 +60,49 @@ func NewClient(requestedBackend string) (*Client, error) {
5760
return &Client{Backend: backend}, nil
5861
}
5962

63+
// UpgradePrep prepares a running cluster to be upgraded
64+
func (c *Client) UpgradePrep(argv []string) error {
65+
usage := `Prepare platform for graceful upgrade.
66+
67+
Usage:
68+
deisctl upgrade-prep [options]
69+
`
70+
if _, err := docopt.Parse(usage, argv, true, "", false); err != nil {
71+
return err
72+
}
73+
74+
return cmd.UpgradePrep(c.Backend)
75+
}
76+
77+
// UpgradeTakeover gracefully restarts a cluster prepared with upgrade-prep
78+
func (c *Client) UpgradeTakeover(argv []string) error {
79+
usage := `Complete the upgrade of a prepped cluster.
80+
81+
Usage:
82+
deisctl upgrade-takeover [options]
83+
`
84+
if _, err := docopt.Parse(usage, argv, true, "", false); err != nil {
85+
return err
86+
}
87+
88+
return cmd.UpgradeTakeover(c.Backend)
89+
}
90+
91+
// RollingRestart attempts a rolling restart of an instance unit
92+
func (c *Client) RollingRestart(argv []string) error {
93+
usage := `Perform a rolling restart of an instance unit.
94+
95+
Usage:
96+
deisctl rolling-restart <target>
97+
`
98+
args, err := docopt.Parse(usage, argv, true, "", false)
99+
if err != nil {
100+
return err
101+
}
102+
103+
return cmd.RollingRestart(args["<target>"].(string), c.Backend)
104+
}
105+
60106
// Config gets or sets a configuration value from the cluster.
61107
//
62108
// A configuration value is stored and retrieved from a key/value store (in this case, etcd)

deisctl/cmd/cmd.go

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

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+
100110
// CheckRequiredKeys exist in etcd
101111
func CheckRequiredKeys() error {
102112
if err := config.CheckConfig("/deis/platform/", "domain"); err != nil {

deisctl/cmd/cmd_test.go

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

1515
"github.com/deis/deis/deisctl/backend"
16+
"github.com/deis/deis/deisctl/etcdclient"
17+
"github.com/deis/deis/deisctl/test/mock"
1618
"github.com/deis/deis/deisctl/units"
1719
)
1820

@@ -21,6 +23,7 @@ type backendStub struct {
2123
stoppedUnits []string
2224
installedUnits []string
2325
uninstalledUnits []string
26+
restartedUnits []string
2427
expected bool
2528
}
2629

@@ -46,6 +49,10 @@ func (backend *backendStub) Scale(component string, num int, wg *sync.WaitGroup,
4649
backend.expected = false
4750
}
4851
}
52+
func (backend *backendStub) RollingRestart(target string, wg *sync.WaitGroup, out, ew io.Writer) {
53+
backend.restartedUnits = append(backend.restartedUnits, target)
54+
}
55+
4956
func (backend *backendStub) ListUnits() error {
5057
return nil
5158
}
@@ -270,6 +277,57 @@ func TestStartSwarm(t *testing.T) {
270277
}
271278
}
272279

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

deisctl/cmd/upgrade.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"sync"
6+
7+
"github.com/deis/deis/deisctl/backend"
8+
"github.com/deis/deis/deisctl/etcdclient"
9+
)
10+
11+
// UpgradePrep stops and uninstalls all components except router and publisher
12+
func UpgradePrep(b backend.Backend) error {
13+
var wg sync.WaitGroup
14+
15+
b.Stop([]string{"database", "registry@*", "controller", "builder", "logger", "logspout"}, &wg, Stdout, Stderr)
16+
wg.Wait()
17+
b.Destroy([]string{"database", "registry@*", "controller", "builder", "logger", "logspout"}, &wg, Stdout, Stderr)
18+
wg.Wait()
19+
20+
b.Stop([]string{"store-volume", "store-gateway@*"}, &wg, Stdout, Stderr)
21+
wg.Wait()
22+
b.Destroy([]string{"store-volume", "store-gateway@*"}, &wg, Stdout, Stderr)
23+
wg.Wait()
24+
25+
b.Stop([]string{"store-metadata"}, &wg, Stdout, Stderr)
26+
wg.Wait()
27+
b.Destroy([]string{"store-metadata"}, &wg, Stdout, Stderr)
28+
wg.Wait()
29+
30+
b.Stop([]string{"store-daemon"}, &wg, Stdout, Stderr)
31+
wg.Wait()
32+
b.Destroy([]string{"store-daemon"}, &wg, Stdout, Stderr)
33+
wg.Wait()
34+
35+
b.Stop([]string{"store-monitor"}, &wg, Stdout, Stderr)
36+
wg.Wait()
37+
b.Destroy([]string{"store-monitor"}, &wg, Stdout, Stderr)
38+
wg.Wait()
39+
40+
fmt.Fprintln(Stdout, "The platform has been stopped, but applications are still serving traffic as normal.")
41+
fmt.Fprintln(Stdout, "Your cluster is now ready for upgrade. Install a new deisctl version and run `deisctl upgrade-takeover`.")
42+
fmt.Fprintln(Stdout, "For more details, see: http://docs.deis.io/en/latest/managing_deis/upgrading-deis/#graceful-upgrade")
43+
return nil
44+
}
45+
46+
func listPublishedServices(etcdClient etcdclient.Client) ([]*etcdclient.ServiceKey, error) {
47+
nodes, err := etcdClient.GetRecursive("deis/services")
48+
if err != nil {
49+
return nil, err
50+
}
51+
52+
return nodes, nil
53+
}
54+
55+
func republishServices(ttl uint64, nodes []*etcdclient.ServiceKey, etcdClient etcdclient.Client) error {
56+
for _, node := range nodes {
57+
_, err := etcdClient.Update(node.Key, node.Value, ttl)
58+
if err != nil {
59+
return err
60+
}
61+
}
62+
63+
return nil
64+
}
65+
66+
// UpgradeTakeover gracefully starts a platform stopped with UpgradePrep
67+
func UpgradeTakeover(b backend.Backend) error {
68+
etcdClient, err := etcdclient.GetEtcdClient()
69+
if err != nil {
70+
return err
71+
}
72+
73+
if err := doUpgradeTakeOver(b, etcdClient); err != nil {
74+
return err
75+
}
76+
77+
return nil
78+
}
79+
80+
func doUpgradeTakeOver(b backend.Backend, etcdClient etcdclient.Client) error {
81+
var wg sync.WaitGroup
82+
83+
nodes, err := listPublishedServices(etcdClient)
84+
if err != nil {
85+
return err
86+
}
87+
88+
b.Stop([]string{"publisher"}, &wg, Stdout, Stderr)
89+
wg.Wait()
90+
b.Destroy([]string{"publisher"}, &wg, Stdout, Stderr)
91+
wg.Wait()
92+
93+
if err := republishServices(1800, nodes, etcdClient); err != nil {
94+
return err
95+
}
96+
97+
b.RollingRestart("router", &wg, Stdout, Stderr)
98+
wg.Wait()
99+
b.Create([]string{"publisher"}, &wg, Stdout, Stderr)
100+
wg.Wait()
101+
b.Start([]string{"publisher"}, &wg, Stdout, Stderr)
102+
wg.Wait()
103+
104+
installDefaultServices(b, false, &wg, Stdout, Stderr) // @fixme: hax?
105+
wg.Wait()
106+
107+
startDefaultServices(b, false, &wg, Stdout, Stderr) // @fixme: hax?
108+
wg.Wait()
109+
return nil
110+
}

deisctl/config/api.go

Lines changed: 0 additions & 8 deletions
This file was deleted.

deisctl/config/config.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os"
99
"strings"
1010

11+
"github.com/deis/deis/deisctl/etcdclient"
1112
"github.com/deis/deis/deisctl/utils"
1213
)
1314

@@ -23,7 +24,7 @@ var b64Keys = []string{"/deis/platform/sshPrivateKey"}
2324

2425
// Config runs the config subcommand
2526
func Config(target string, action string, key []string) error {
26-
client, err := getEtcdClient()
27+
client, err := etcdclient.GetEtcdClient()
2728
if err != nil {
2829
return err
2930
}
@@ -35,7 +36,7 @@ func Config(target string, action string, key []string) error {
3536
// and returns an error if a value is not found
3637
func CheckConfig(root string, k string) error {
3738

38-
client, err := getEtcdClient()
39+
client, err := etcdclient.GetEtcdClient()
3940
if err != nil {
4041
return err
4142
}
@@ -48,7 +49,7 @@ func CheckConfig(root string, k string) error {
4849
return nil
4950
}
5051

51-
func doConfig(target string, action string, key []string, client Client, w io.Writer) error {
52+
func doConfig(target string, action string, key []string, client etcdclient.Client, w io.Writer) error {
5253
rootPath := "/deis/" + target + "/"
5354

5455
var vals []string
@@ -73,7 +74,7 @@ func doConfig(target string, action string, key []string, client Client, w io.Wr
7374
return nil
7475
}
7576

76-
func doConfigSet(client Client, root string, kvs []string) ([]string, error) {
77+
func doConfigSet(client etcdclient.Client, root string, kvs []string) ([]string, error) {
7778
var result []string
7879

7980
for _, kv := range kvs {
@@ -100,7 +101,7 @@ func doConfigSet(client Client, root string, kvs []string) ([]string, error) {
100101
return result, nil
101102
}
102103

103-
func doConfigGet(client Client, root string, keys []string) ([]string, error) {
104+
func doConfigGet(client etcdclient.Client, root string, keys []string) ([]string, error) {
104105
var result []string
105106
for _, k := range keys {
106107
val, err := client.Get(root + k)
@@ -112,7 +113,7 @@ func doConfigGet(client Client, root string, keys []string) ([]string, error) {
112113
return result, nil
113114
}
114115

115-
func doConfigRm(client Client, root string, keys []string) ([]string, error) {
116+
func doConfigRm(client etcdclient.Client, root string, keys []string) ([]string, error) {
116117
var result []string
117118
for _, k := range keys {
118119
err := client.Delete(root + k)

0 commit comments

Comments
 (0)