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

Swarm flavor revamp #376

Merged
merged 20 commits into from
Feb 1, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
initial work
Signed-off-by: David Chung <david.chung@docker.com>
  • Loading branch information
David Chung committed Jan 22, 2017
commit 974eef9809c857f88a751120ed9e6895d962b174
116 changes: 64 additions & 52 deletions examples/flavor/swarm/flavor.go
Original file line number Diff line number Diff line change
@@ -1,40 +1,27 @@
package main

import (
"bytes"
"encoding/json"
"errors"
"fmt"

log "github.com/Sirupsen/logrus"
docker_types "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
"github.com/docker/infrakit/pkg/plugin/group/types"
"github.com/docker/infrakit/pkg/spi/flavor"
"github.com/docker/infrakit/pkg/spi/instance"
"github.com/docker/infrakit/pkg/template"
"github.com/docker/infrakit/pkg/types"
"golang.org/x/net/context"
)

const (
ebsAttachment string = "ebs"
)

type swarmFlavor struct {
client client.APIClient
initScript *template.Template
}

type schema struct {
Attachments map[instance.LogicalID][]instance.Attachment
DockerRestartCommand string
}

func parseProperties(flavorProperties json.RawMessage) (schema, error) {
s := schema{}
err := json.Unmarshal(flavorProperties, &s)
return s, err
// Spec is the value passed in the `Properties` field of configs
type Spec struct {
Attachments map[instance.LogicalID][]instance.Attachment
}

func validateIDsAndAttachments(logicalIDs []instance.LogicalID,
Expand Down Expand Up @@ -88,54 +75,79 @@ func validateIDsAndAttachments(logicalIDs []instance.LogicalID,
return nil
}

const (
// associationTag is a machine tag added to associate machines with Swarm nodes.
associationTag = "swarm-association-id"
)

func generateInitScript(templ *template.Template,
joinIP, joinToken, associationID, restartCommand string) (string, error) {

var buffer bytes.Buffer
err := templ.Execute(&buffer, map[string]string{
"MY_IP": joinIP,
"JOIN_TOKEN": joinToken,
"ASSOCIATION_ID": associationID,
"RESTART_DOCKER": restartCommand,
})
func swarmState(docker client.APIClient) (status swarm.Swarm, node swarm.Node, err error) {
ctx := context.Background()
info, err := docker.Info(ctx)
if err != nil {
return "", err
return
}
return buffer.String(), nil
}

func (s swarmFlavor) Validate(flavorProperties json.RawMessage, allocation types.AllocationMethod) error {
properties, err := parseProperties(flavorProperties)
node, _, err = docker.NodeInspectWithRaw(ctx, info.Swarm.NodeID)
if err != nil {
return err
}
if properties.DockerRestartCommand == "" {
return errors.New("DockerRestartCommand must be specified")
return
}
if err := validateIDsAndAttachments(allocation.LogicalIDs, properties.Attachments); err != nil {
return err
status, err = docker.SwarmInspect(ctx)
return
}

func exportTemplateFunctions(swarmStatus swarm.Swarm, nodeInfo swarm.Node, link types.Link) []template.Function {

// Get a single consistent view of the data across multiple calls by exporting functions that
// query the input state

return []template.Function{
{
Name: "INFRAKIT_LABELS",
Description: "The label name to use for linking an InfraKit managed resource somewhere else.",
Func: func() []string {
return link.KVPairs()
},
},
{
Name: "SWARM_MANAGER_IP",
Description: "The label name to use for linking an InfraKit managed resource somewhere else.",
Func: func() (string, error) {
if nodeInfo.ManagerStatus == nil {
return "", fmt.Errorf("no manager status")
}
return nodeInfo.ManagerStatus.Addr, nil
},
},
{
Name: "SWARM_INITIALIZED",
Description: "Returns true if the swarm has been initialized.",
Func: func() bool {
return nodeInfo.ManagerStatus != nil
},
},
{
Name: "SWARM_JOIN_TOKENS",
Description: "Returns the swarm JoinTokens object, with either .Manager or .Worker fields",
Func: func() interface{} {
return swarmStatus.JoinTokens
},
},
{
Name: "SWARM_CLUSTER_ID",
Description: "Returns the swarm cluster UUID",
Func: func() interface{} {
return swarmStatus.ID
},
},
}
return nil
}

// Healthy determines whether an instance is healthy. This is determined by whether it has successfully joined the
// Swarm.
func healthy(client client.APIClient,
flavorProperties json.RawMessage, inst instance.Description) (flavor.Health, error) {
func healthy(client client.APIClient, inst instance.Description) (flavor.Health, error) {

associationID, exists := inst.Tags[associationTag]
if !exists {
link := types.NewLinkFromMap(inst.Tags)
if !link.Valid() {
log.Info("Reporting unhealthy for instance without an association tag", inst.ID)
return flavor.Unhealthy, nil
}

filter := filters.NewArgs()
filter.Add("label", fmt.Sprintf("%s=%s", associationTag, associationID))
filter.Add("label", fmt.Sprintf("%s=%s", link.Label(), link.Value()))

nodes, err := client.NodeList(context.Background(), docker_types.NodeListOptions{Filters: filter})
if err != nil {
Expand All @@ -151,7 +163,7 @@ func healthy(client client.APIClient,
return flavor.Healthy, nil

default:
log.Warnf("Expected at most one node with label %s, but found %s", associationID, nodes)
log.Warnf("Expected at most one node with label %s, but found %s", link.Value(), nodes)
return flavor.Healthy, nil
}
}
62 changes: 35 additions & 27 deletions examples/flavor/swarm/flavor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,17 @@ import (
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
mock_client "github.com/docker/infrakit/pkg/mock/docker/docker/client"
"github.com/docker/infrakit/pkg/plugin/group/types"
group_types "github.com/docker/infrakit/pkg/plugin/group/types"
"github.com/docker/infrakit/pkg/spi/flavor"
"github.com/docker/infrakit/pkg/spi/instance"
"github.com/docker/infrakit/pkg/template"
"github.com/docker/infrakit/pkg/types"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
)

func templ() *template.Template {
t, err := template.NewTemplate("str://"+DefaultInitScriptTemplate, template.Options{})
func templ(tpl string) *template.Template {
t, err := template.NewTemplate("str://"+tpl, template.Options{})
if err != nil {
panic(err)
}
Expand All @@ -29,27 +30,27 @@ func TestValidate(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

managerFlavor := NewManagerFlavor(mock_client.NewMockAPIClient(ctrl), templ())
workerFlavor := NewWorkerFlavor(mock_client.NewMockAPIClient(ctrl), templ())
managerFlavor := NewManagerFlavor(mock_client.NewMockAPIClient(ctrl), templ(DefaultManagerInitScriptTemplate))
workerFlavor := NewWorkerFlavor(mock_client.NewMockAPIClient(ctrl), templ(DefaultWorkerInitScriptTemplate))

require.NoError(t, workerFlavor.Validate(
json.RawMessage(`{"DockerRestartCommand": "systemctl restart docker"}`),
types.AllocationMethod{Size: 5}))
group_types.AllocationMethod{Size: 5}))
require.NoError(t, managerFlavor.Validate(
json.RawMessage(`{"DockerRestartCommand": "systemctl restart docker"}`),
types.AllocationMethod{LogicalIDs: []instance.LogicalID{"127.0.0.1"}}))
group_types.AllocationMethod{LogicalIDs: []instance.LogicalID{"127.0.0.1"}}))

// Logical ID with multiple attachments is allowed.
require.NoError(t, managerFlavor.Validate(
json.RawMessage(`{
"DockerRestartCommand": "systemctl restart docker",
"Attachments": {"127.0.0.1": [{"ID": "a", "Type": "ebs"}, {"ID": "b", "Type": "ebs"}]}}`),
types.AllocationMethod{LogicalIDs: []instance.LogicalID{"127.0.0.1"}}))
group_types.AllocationMethod{LogicalIDs: []instance.LogicalID{"127.0.0.1"}}))

// Logical ID used more than once.
err := managerFlavor.Validate(
json.RawMessage(`{"DockerRestartCommand": "systemctl restart docker"}`),
types.AllocationMethod{LogicalIDs: []instance.LogicalID{"127.0.0.1", "127.0.0.1", "127.0.0.2"}})
group_types.AllocationMethod{LogicalIDs: []instance.LogicalID{"127.0.0.1", "127.0.0.1", "127.0.0.2"}})
require.Error(t, err)
require.Equal(t, "LogicalID 127.0.0.1 specified more than once", err.Error())

Expand All @@ -58,7 +59,7 @@ func TestValidate(t *testing.T) {
json.RawMessage(`{
"DockerRestartCommand": "systemctl restart docker",
"Attachments": {"127.0.0.1": [{"ID": "a", "Type": "ebs"}], "127.0.0.2": [{"ID": "a", "Type": "ebs"}]}}`),
types.AllocationMethod{LogicalIDs: []instance.LogicalID{"127.0.0.1", "127.0.0.2", "127.0.0.3"}})
group_types.AllocationMethod{LogicalIDs: []instance.LogicalID{"127.0.0.1", "127.0.0.2", "127.0.0.3"}})
require.Error(t, err)
require.Equal(t, "Attachment a specified more than once", err.Error())

Expand All @@ -67,7 +68,7 @@ func TestValidate(t *testing.T) {
json.RawMessage(`{
"DockerRestartCommand": "systemctl restart docker",
"Attachments": {"127.0.0.1": [{"ID": "a", "Type": "keyboard"}]}}`),
types.AllocationMethod{LogicalIDs: []instance.LogicalID{"127.0.0.1"}})
group_types.AllocationMethod{LogicalIDs: []instance.LogicalID{"127.0.0.1"}})
require.Error(t, err)
require.Equal(t, "Invalid attachment Type 'keyboard', only ebs is supported", err.Error())
}
Expand All @@ -78,7 +79,7 @@ func TestWorker(t *testing.T) {

client := mock_client.NewMockAPIClient(ctrl)

flavorImpl := NewWorkerFlavor(client, templ())
flavorImpl := NewWorkerFlavor(client, templ(DefaultWorkerInitScriptTemplate))

swarmInfo := swarm.Swarm{
ClusterInfo: swarm.ClusterInfo{ID: "ClusterUUID"},
Expand All @@ -87,24 +88,27 @@ func TestWorker(t *testing.T) {
Worker: "WorkerToken",
},
}
client.EXPECT().SwarmInspect(gomock.Any()).Return(swarmInfo, nil)

client.EXPECT().Info(gomock.Any()).Return(infoResponse, nil)

client.EXPECT().SwarmInspect(gomock.Any()).Return(swarmInfo, nil).AnyTimes()
client.EXPECT().Info(gomock.Any()).Return(infoResponse, nil).AnyTimes()
nodeInfo := swarm.Node{ManagerStatus: &swarm.ManagerStatus{Addr: "1.2.3.4"}}
client.EXPECT().NodeInspectWithRaw(gomock.Any(), nodeID).Return(nodeInfo, nil, nil)
client.EXPECT().NodeInspectWithRaw(gomock.Any(), nodeID).Return(nodeInfo, nil, nil).AnyTimes()

details, err := flavorImpl.Prepare(
json.RawMessage(`{}`),
instance.Spec{Tags: map[string]string{"a": "b"}},
types.AllocationMethod{Size: 5})
group_types.AllocationMethod{Size: 5})
require.NoError(t, err)
require.Equal(t, "b", details.Tags["a"])
associationID := details.Tags[associationTag]
require.NotEqual(t, "", associationID)

link := types.NewLinkFromMap(details.Tags)
require.True(t, link.Valid())
require.True(t, len(link.KVPairs()) > 0)

// Perform a rudimentary check to ensure that the expected fields are in the InitScript, without having any
// other knowledge about the script structure.
associationID := link.Value()
associationTag := link.Label()
require.Contains(t, details.Init, associationID)
require.Contains(t, details.Init, swarmInfo.JoinTokens.Worker)
require.NotContains(t, details.Init, swarmInfo.JoinTokens.Manager)
Expand Down Expand Up @@ -140,7 +144,7 @@ func TestManager(t *testing.T) {

client := mock_client.NewMockAPIClient(ctrl)

flavorImpl := NewManagerFlavor(client, templ())
flavorImpl := NewManagerFlavor(client, templ(DefaultManagerInitScriptTemplate))

swarmInfo := swarm.Swarm{
ClusterInfo: swarm.ClusterInfo{ID: "ClusterUUID"},
Expand All @@ -149,25 +153,29 @@ func TestManager(t *testing.T) {
Worker: "WorkerToken",
},
}
client.EXPECT().SwarmInspect(gomock.Any()).Return(swarmInfo, nil)

client.EXPECT().Info(gomock.Any()).Return(infoResponse, nil)

client.EXPECT().SwarmInspect(gomock.Any()).Return(swarmInfo, nil).AnyTimes()
client.EXPECT().Info(gomock.Any()).Return(infoResponse, nil).AnyTimes()
nodeInfo := swarm.Node{ManagerStatus: &swarm.ManagerStatus{Addr: "1.2.3.4"}}
client.EXPECT().NodeInspectWithRaw(gomock.Any(), nodeID).Return(nodeInfo, nil, nil)
client.EXPECT().NodeInspectWithRaw(gomock.Any(), nodeID).Return(nodeInfo, nil, nil).AnyTimes()

id := instance.LogicalID("127.0.0.1")
details, err := flavorImpl.Prepare(
json.RawMessage(`{"Attachments": {"127.0.0.1": [{"ID": "a", "Type": "gpu"}]}}`),
instance.Spec{Tags: map[string]string{"a": "b"}, LogicalID: &id},
types.AllocationMethod{LogicalIDs: []instance.LogicalID{"127.0.0.1"}})
group_types.AllocationMethod{LogicalIDs: []instance.LogicalID{"127.0.0.1"}})
require.NoError(t, err)
require.Equal(t, "b", details.Tags["a"])
associationID := details.Tags[associationTag]
require.NotEqual(t, "", associationID)

link := types.NewLinkFromMap(details.Tags)
require.True(t, link.Valid())
require.True(t, len(link.KVPairs()) > 0)

// Perform a rudimentary check to ensure that the expected fields are in the InitScript, without having any
// other knowledge about the script structure.

associationID := link.Value()
associationTag := link.Label()
require.Contains(t, details.Init, associationID)
require.Contains(t, details.Init, swarmInfo.JoinTokens.Manager)
require.NotContains(t, details.Init, swarmInfo.JoinTokens.Worker)
Expand Down
Loading