Skip to content
This repository was archived by the owner on Jan 21, 2020. It is now read-only.

Commit 89b031a

Browse files
author
David Chung
authored
Multiplex flavor plugins by type in a single RPC endpoint. (#364)
Signed-off-by: David Chung <[email protected]>
1 parent 3113b14 commit 89b031a

File tree

17 files changed

+822
-229
lines changed

17 files changed

+822
-229
lines changed

cmd/cli/flavor.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func flavorPluginCommand(plugins func() discovery.Plugins) *cobra.Command {
3434
return err
3535
}
3636

37-
flavorPlugin = flavor_plugin.NewClient(endpoint.Address)
37+
flavorPlugin = flavor_plugin.NewClient(plugin.Name(*name), endpoint.Address)
3838

3939
return nil
4040
}

cmd/group/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,15 @@ func main() {
4141
if err != nil {
4242
return nil, err
4343
}
44-
return instance_client.NewClient(plugin.Name(n), endpoint.Address), nil
44+
return instance_client.NewClient(n, endpoint.Address), nil
4545
}
4646

4747
flavorPluginLookup := func(n plugin.Name) (flavor.Plugin, error) {
4848
endpoint, err := plugins.Find(n)
4949
if err != nil {
5050
return nil, err
5151
}
52-
return flavor_client.NewClient(endpoint.Address), nil
52+
return flavor_client.NewClient(n, endpoint.Address), nil
5353
}
5454

5555
cli.RunPlugin(*name, group_server.PluginServer(

pkg/example/flavor/combo/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func main() {
3333
if err != nil {
3434
return nil, err
3535
}
36-
return flavor_rpc.NewClient(endpoint.Address), nil
36+
return flavor_rpc.NewClient(n, endpoint.Address), nil
3737
}
3838

3939
cli.SetLogLevel(*logLevel)

pkg/example/flavor/swarm/flavor.go

Lines changed: 25 additions & 178 deletions
Original file line numberDiff line numberDiff line change
@@ -11,33 +11,22 @@ import (
1111
"github.com/docker/docker/api/types/filters"
1212
"github.com/docker/docker/client"
1313
"github.com/docker/infrakit/pkg/plugin/group/types"
14-
"github.com/docker/infrakit/pkg/plugin/group/util"
1514
"github.com/docker/infrakit/pkg/spi/flavor"
1615
"github.com/docker/infrakit/pkg/spi/instance"
1716
"github.com/docker/infrakit/pkg/template"
1817
"golang.org/x/net/context"
1918
)
2019

21-
type nodeType string
22-
2320
const (
24-
worker nodeType = "worker"
25-
manager nodeType = "manager"
26-
ebsAttachment string = "ebs"
21+
ebsAttachment string = "ebs"
2722
)
2823

29-
// NewSwarmFlavor creates a flavor.Plugin that creates manager and worker nodes connected in a swarm.
30-
func NewSwarmFlavor(dockerClient client.APIClient, templ *template.Template) flavor.Plugin {
31-
return &swarmFlavor{client: dockerClient, initScript: templ}
32-
}
33-
3424
type swarmFlavor struct {
3525
client client.APIClient
3626
initScript *template.Template
3727
}
3828

3929
type schema struct {
40-
Type nodeType
4130
Attachments map[instance.LogicalID][]instance.Attachment
4231
DockerRestartCommand string
4332
}
@@ -48,7 +37,9 @@ func parseProperties(flavorProperties json.RawMessage) (schema, error) {
4837
return s, err
4938
}
5039

51-
func validateIDsAndAttachments(logicalIDs []instance.LogicalID, attachments map[instance.LogicalID][]instance.Attachment) error {
40+
func validateIDsAndAttachments(logicalIDs []instance.LogicalID,
41+
attachments map[instance.LogicalID][]instance.Attachment) error {
42+
5243
// Each attachment association must be represented by a logical ID.
5344
idsMap := map[instance.LogicalID]bool{}
5445
for _, id := range logicalIDs {
@@ -97,49 +88,14 @@ func validateIDsAndAttachments(logicalIDs []instance.LogicalID, attachments map[
9788
return nil
9889
}
9990

100-
func (s swarmFlavor) Validate(flavorProperties json.RawMessage, allocation types.AllocationMethod) error {
101-
properties, err := parseProperties(flavorProperties)
102-
if err != nil {
103-
return err
104-
}
105-
106-
if properties.DockerRestartCommand == "" {
107-
return errors.New("DockerRestartCommand must be specified")
108-
}
109-
110-
switch properties.Type {
111-
case manager:
112-
numIDs := len(allocation.LogicalIDs)
113-
if numIDs != 1 && numIDs != 3 && numIDs != 5 {
114-
return errors.New("Must have 1, 3, or 5 manager logical IDs")
115-
}
116-
case worker:
117-
118-
default:
119-
return errors.New("Unrecognized node Type")
120-
}
121-
122-
if properties.Type == manager {
123-
for _, id := range allocation.LogicalIDs {
124-
if att, exists := properties.Attachments[id]; !exists || len(att) == 0 {
125-
log.Warnf("LogicalID %s has no attachments, which is needed for durability", id)
126-
}
127-
}
128-
}
129-
130-
if err := validateIDsAndAttachments(allocation.LogicalIDs, properties.Attachments); err != nil {
131-
return err
132-
}
133-
134-
return nil
135-
}
136-
13791
const (
13892
// associationTag is a machine tag added to associate machines with Swarm nodes.
13993
associationTag = "swarm-association-id"
14094
)
14195

142-
func generateInitScript(templ *template.Template, joinIP, joinToken, associationID, restartCommand string) (string, error) {
96+
func generateInitScript(templ *template.Template,
97+
joinIP, joinToken, associationID, restartCommand string) (string, error) {
98+
14399
var buffer bytes.Buffer
144100
err := templ.Execute(&buffer, map[string]string{
145101
"MY_IP": joinIP,
@@ -153,9 +109,25 @@ func generateInitScript(templ *template.Template, joinIP, joinToken, association
153109
return buffer.String(), nil
154110
}
155111

112+
func (s swarmFlavor) Validate(flavorProperties json.RawMessage, allocation types.AllocationMethod) error {
113+
properties, err := parseProperties(flavorProperties)
114+
if err != nil {
115+
return err
116+
}
117+
if properties.DockerRestartCommand == "" {
118+
return errors.New("DockerRestartCommand must be specified")
119+
}
120+
if err := validateIDsAndAttachments(allocation.LogicalIDs, properties.Attachments); err != nil {
121+
return err
122+
}
123+
return nil
124+
}
125+
156126
// Healthy determines whether an instance is healthy. This is determined by whether it has successfully joined the
157127
// Swarm.
158-
func (s swarmFlavor) Healthy(flavorProperties json.RawMessage, inst instance.Description) (flavor.Health, error) {
128+
func healthy(client client.APIClient,
129+
flavorProperties json.RawMessage, inst instance.Description) (flavor.Health, error) {
130+
159131
associationID, exists := inst.Tags[associationTag]
160132
if !exists {
161133
log.Info("Reporting unhealthy for instance without an association tag", inst.ID)
@@ -165,7 +137,7 @@ func (s swarmFlavor) Healthy(flavorProperties json.RawMessage, inst instance.Des
165137
filter := filters.NewArgs()
166138
filter.Add("label", fmt.Sprintf("%s=%s", associationTag, associationID))
167139

168-
nodes, err := s.client.NodeList(context.Background(), docker_types.NodeListOptions{Filters: filter})
140+
nodes, err := client.NodeList(context.Background(), docker_types.NodeListOptions{Filters: filter})
169141
if err != nil {
170142
return flavor.Unknown, err
171143
}
@@ -183,128 +155,3 @@ func (s swarmFlavor) Healthy(flavorProperties json.RawMessage, inst instance.Des
183155
return flavor.Healthy, nil
184156
}
185157
}
186-
187-
func (s swarmFlavor) Drain(flavorProperties json.RawMessage, inst instance.Description) error {
188-
properties, err := parseProperties(flavorProperties)
189-
if err != nil {
190-
return err
191-
}
192-
193-
// Only explicitly remove worker nodes, not manager nodes. Manager nodes are assumed to have an
194-
// attached volume for state, and fixed IP addresses. This allows them to rejoin as the same node.
195-
if properties.Type != worker {
196-
return nil
197-
}
198-
199-
associationID, exists := inst.Tags[associationTag]
200-
if !exists {
201-
return fmt.Errorf("Unable to drain %s without an association tag", inst.ID)
202-
}
203-
204-
filter := filters.NewArgs()
205-
filter.Add("label", fmt.Sprintf("%s=%s", associationTag, associationID))
206-
207-
nodes, err := s.client.NodeList(context.Background(), docker_types.NodeListOptions{Filters: filter})
208-
if err != nil {
209-
return err
210-
}
211-
212-
switch {
213-
case len(nodes) == 0:
214-
return fmt.Errorf("Unable to drain %s, not found in swarm", inst.ID)
215-
216-
case len(nodes) == 1:
217-
err := s.client.NodeRemove(
218-
context.Background(),
219-
nodes[0].ID,
220-
docker_types.NodeRemoveOptions{Force: true})
221-
if err != nil {
222-
return err
223-
}
224-
225-
return nil
226-
227-
default:
228-
return fmt.Errorf("Expected at most one node with label %s, but found %s", associationID, nodes)
229-
}
230-
}
231-
232-
func (s *swarmFlavor) Prepare(
233-
flavorProperties json.RawMessage,
234-
spec instance.Spec,
235-
allocation types.AllocationMethod) (instance.Spec, error) {
236-
237-
properties, err := parseProperties(flavorProperties)
238-
if err != nil {
239-
return spec, err
240-
}
241-
242-
swarmStatus, err := s.client.SwarmInspect(context.Background())
243-
if err != nil {
244-
return spec, fmt.Errorf("Failed to fetch Swarm join tokens: %s", err)
245-
}
246-
247-
nodeInfo, err := s.client.Info(context.Background())
248-
if err != nil {
249-
return spec, fmt.Errorf("Failed to fetch node self info: %s", err)
250-
}
251-
252-
self, _, err := s.client.NodeInspectWithRaw(context.Background(), nodeInfo.Swarm.NodeID)
253-
if err != nil {
254-
return spec, fmt.Errorf("Failed to fetch Swarm node status: %s", err)
255-
}
256-
257-
if self.ManagerStatus == nil {
258-
return spec, errors.New(
259-
"Swarm node status did not include manager status. Need to run 'docker swarm init`?")
260-
}
261-
262-
associationID := util.RandomAlphaNumericString(8)
263-
spec.Tags[associationTag] = associationID
264-
265-
switch properties.Type {
266-
case worker:
267-
initScript, err :=
268-
generateInitScript(
269-
s.initScript,
270-
self.ManagerStatus.Addr,
271-
swarmStatus.JoinTokens.Worker,
272-
associationID,
273-
properties.DockerRestartCommand)
274-
if err != nil {
275-
return spec, err
276-
}
277-
spec.Init = initScript
278-
279-
case manager:
280-
if spec.LogicalID == nil {
281-
return spec, errors.New("Manager nodes require a LogicalID, " +
282-
"which will be used as an assigned private IP address")
283-
}
284-
285-
initScript, err := generateInitScript(
286-
s.initScript,
287-
self.ManagerStatus.Addr,
288-
swarmStatus.JoinTokens.Manager,
289-
associationID,
290-
properties.DockerRestartCommand)
291-
if err != nil {
292-
return spec, err
293-
}
294-
spec.Init = initScript
295-
default:
296-
return spec, errors.New("Unsupported node type")
297-
}
298-
299-
if spec.LogicalID != nil {
300-
if attachments, exists := properties.Attachments[*spec.LogicalID]; exists {
301-
spec.Attachments = append(spec.Attachments, attachments...)
302-
}
303-
}
304-
305-
// TODO(wfarner): Use the cluster UUID to scope instances for this swarm separately from instances in another
306-
// swarm. This will require plumbing back to Scaled (membership tags).
307-
spec.Tags["swarm-id"] = swarmStatus.ID
308-
309-
return spec, nil
310-
}

pkg/example/flavor/swarm/flavor_test.go

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,46 +29,42 @@ func TestValidate(t *testing.T) {
2929
ctrl := gomock.NewController(t)
3030
defer ctrl.Finish()
3131

32-
swarmFlavor := NewSwarmFlavor(mock_client.NewMockAPIClient(ctrl), templ())
32+
managerFlavor := NewManagerFlavor(mock_client.NewMockAPIClient(ctrl), templ())
33+
workerFlavor := NewWorkerFlavor(mock_client.NewMockAPIClient(ctrl), templ())
3334

34-
require.NoError(t, swarmFlavor.Validate(
35-
json.RawMessage(`{"Type": "worker", "DockerRestartCommand": "systemctl restart docker"}`),
35+
require.NoError(t, workerFlavor.Validate(
36+
json.RawMessage(`{"DockerRestartCommand": "systemctl restart docker"}`),
3637
types.AllocationMethod{Size: 5}))
37-
require.NoError(t, swarmFlavor.Validate(
38-
json.RawMessage(`{"Type": "manager", "DockerRestartCommand": "systemctl restart docker"}`),
38+
require.NoError(t, managerFlavor.Validate(
39+
json.RawMessage(`{"DockerRestartCommand": "systemctl restart docker"}`),
3940
types.AllocationMethod{LogicalIDs: []instance.LogicalID{"127.0.0.1"}}))
4041

4142
// Logical ID with multiple attachments is allowed.
42-
require.NoError(t, swarmFlavor.Validate(
43+
require.NoError(t, managerFlavor.Validate(
4344
json.RawMessage(`{
44-
"Type": "manager",
4545
"DockerRestartCommand": "systemctl restart docker",
4646
"Attachments": {"127.0.0.1": [{"ID": "a", "Type": "ebs"}, {"ID": "b", "Type": "ebs"}]}}`),
4747
types.AllocationMethod{LogicalIDs: []instance.LogicalID{"127.0.0.1"}}))
4848

49-
require.Error(t, swarmFlavor.Validate(json.RawMessage(`{"type": "other"}`), types.AllocationMethod{Size: 5}))
50-
5149
// Logical ID used more than once.
52-
err := swarmFlavor.Validate(
53-
json.RawMessage(`{"Type": "manager", "DockerRestartCommand": "systemctl restart docker"}`),
50+
err := managerFlavor.Validate(
51+
json.RawMessage(`{"DockerRestartCommand": "systemctl restart docker"}`),
5452
types.AllocationMethod{LogicalIDs: []instance.LogicalID{"127.0.0.1", "127.0.0.1", "127.0.0.2"}})
5553
require.Error(t, err)
5654
require.Equal(t, "LogicalID 127.0.0.1 specified more than once", err.Error())
5755

5856
// Attachment cannot be associated with multiple Logical IDs.
59-
err = swarmFlavor.Validate(
57+
err = managerFlavor.Validate(
6058
json.RawMessage(`{
61-
"Type": "manager",
6259
"DockerRestartCommand": "systemctl restart docker",
6360
"Attachments": {"127.0.0.1": [{"ID": "a", "Type": "ebs"}], "127.0.0.2": [{"ID": "a", "Type": "ebs"}]}}`),
6461
types.AllocationMethod{LogicalIDs: []instance.LogicalID{"127.0.0.1", "127.0.0.2", "127.0.0.3"}})
6562
require.Error(t, err)
6663
require.Equal(t, "Attachment a specified more than once", err.Error())
6764

6865
// Unsupported Attachment Type.
69-
err = swarmFlavor.Validate(
66+
err = managerFlavor.Validate(
7067
json.RawMessage(`{
71-
"Type": "manager",
7268
"DockerRestartCommand": "systemctl restart docker",
7369
"Attachments": {"127.0.0.1": [{"ID": "a", "Type": "keyboard"}]}}`),
7470
types.AllocationMethod{LogicalIDs: []instance.LogicalID{"127.0.0.1"}})
@@ -82,7 +78,7 @@ func TestWorker(t *testing.T) {
8278

8379
client := mock_client.NewMockAPIClient(ctrl)
8480

85-
flavorImpl := NewSwarmFlavor(client, templ())
81+
flavorImpl := NewWorkerFlavor(client, templ())
8682

8783
swarmInfo := swarm.Swarm{
8884
ClusterInfo: swarm.ClusterInfo{ID: "ClusterUUID"},
@@ -99,7 +95,7 @@ func TestWorker(t *testing.T) {
9995
client.EXPECT().NodeInspectWithRaw(gomock.Any(), nodeID).Return(nodeInfo, nil, nil)
10096

10197
details, err := flavorImpl.Prepare(
102-
json.RawMessage(`{"Type": "worker"}`),
98+
json.RawMessage(`{}`),
10399
instance.Spec{Tags: map[string]string{"a": "b"}},
104100
types.AllocationMethod{Size: 5})
105101
require.NoError(t, err)
@@ -144,7 +140,7 @@ func TestManager(t *testing.T) {
144140

145141
client := mock_client.NewMockAPIClient(ctrl)
146142

147-
flavorImpl := NewSwarmFlavor(client, templ())
143+
flavorImpl := NewManagerFlavor(client, templ())
148144

149145
swarmInfo := swarm.Swarm{
150146
ClusterInfo: swarm.ClusterInfo{ID: "ClusterUUID"},
@@ -162,7 +158,7 @@ func TestManager(t *testing.T) {
162158

163159
id := instance.LogicalID("127.0.0.1")
164160
details, err := flavorImpl.Prepare(
165-
json.RawMessage(`{"Type": "manager", "Attachments": {"127.0.0.1": [{"ID": "a", "Type": "gpu"}]}}`),
161+
json.RawMessage(`{"Attachments": {"127.0.0.1": [{"ID": "a", "Type": "gpu"}]}}`),
166162
instance.Spec{Tags: map[string]string{"a": "b"}, LogicalID: &id},
167163
types.AllocationMethod{LogicalIDs: []instance.LogicalID{"127.0.0.1"}})
168164
require.NoError(t, err)

0 commit comments

Comments
 (0)