Skip to content

Commit

Permalink
Merge pull request #415 from hashicorp/f-port-labels
Browse files Browse the repository at this point in the history
Implemented Port Labeling and Driver Configurations
  • Loading branch information
diptanu committed Nov 17, 2015
2 parents aef7396 + b8d468f commit a17ce1e
Show file tree
Hide file tree
Showing 38 changed files with 462 additions and 410 deletions.
10 changes: 5 additions & 5 deletions api/compose_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func TestCompose(t *testing.T) {
&NetworkResource{
CIDR: "0.0.0.0/0",
MBits: 100,
ReservedPorts: []int{80, 443},
ReservedPorts: []Port{{"", 80}, {"", 443}},
},
},
})
Expand Down Expand Up @@ -83,9 +83,9 @@ func TestCompose(t *testing.T) {
&NetworkResource{
CIDR: "0.0.0.0/0",
MBits: 100,
ReservedPorts: []int{
80,
443,
ReservedPorts: []Port{
{"", 80},
{"", 443},
},
},
},
Expand All @@ -97,7 +97,7 @@ func TestCompose(t *testing.T) {
Operand: "=",
},
},
Config: map[string]string{
Config: map[string]interface{}{
"foo": "bar",
},
Meta: map[string]string{
Expand Down
9 changes: 7 additions & 2 deletions api/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@ type Resources struct {
Networks []*NetworkResource
}

type Port struct {
Label string
Value int
}

// NetworkResource is used to describe required network
// resources of a given task.
type NetworkResource struct {
Public bool
CIDR string
ReservedPorts []int
DynamicPorts []string
ReservedPorts []Port
DynamicPorts []Port
MBits int
}
4 changes: 2 additions & 2 deletions api/tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func (g *TaskGroup) AddTask(t *Task) *TaskGroup {
type Task struct {
Name string
Driver string
Config map[string]string
Config map[string]interface{}
Constraints []*Constraint
Env map[string]string
Resources *Resources
Expand All @@ -84,7 +84,7 @@ func NewTask(name, driver string) *Task {
// the task.
func (t *Task) SetConfig(key, val string) *Task {
if t.Config == nil {
t.Config = make(map[string]string)
t.Config = make(map[string]interface{})
}
t.Config[key] = val
return t
Expand Down
4 changes: 2 additions & 2 deletions api/tasks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func TestTask_SetConfig(t *testing.T) {

// Set another config value
task.SetConfig("baz", "zip")
expect := map[string]string{"foo": "bar", "baz": "zip"}
expect := map[string]interface{}{"foo": "bar", "baz": "zip"}
if !reflect.DeepEqual(task.Config, expect) {
t.Fatalf("expect: %#v, got: %#v", expect, task.Config)
}
Expand Down Expand Up @@ -171,7 +171,7 @@ func TestTask_Require(t *testing.T) {
&NetworkResource{
CIDR: "0.0.0.0/0",
MBits: 100,
ReservedPorts: []int{80, 443},
ReservedPorts: []Port{{"", 80}, {"", 443}},
},
},
}
Expand Down
4 changes: 2 additions & 2 deletions client/allocdir/alloc_dir_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ var (
t1 = &structs.Task{
Name: "web",
Driver: "exec",
Config: map[string]string{
Config: map[string]interface{}{
"command": "/bin/date",
"args": "+%s",
},
Expand All @@ -27,7 +27,7 @@ var (
t2 = &structs.Task{
Name: "web2",
Driver: "exec",
Config: map[string]string{
Config: map[string]interface{}{
"command": "/bin/date",
"args": "+%s",
},
Expand Down
150 changes: 87 additions & 63 deletions client/driver/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,53 @@ import (
"strconv"
"strings"

docker "github.com/fsouza/go-dockerclient"

"github.com/hashicorp/nomad/client/allocdir"
"github.com/hashicorp/nomad/client/config"
"github.com/hashicorp/nomad/client/driver/args"
cstructs "github.com/hashicorp/nomad/client/driver/structs"
"github.com/hashicorp/nomad/client/fingerprint"
"github.com/hashicorp/nomad/nomad/structs"

docker "github.com/fsouza/go-dockerclient"
cstructs "github.com/hashicorp/nomad/client/driver/structs"
"github.com/mitchellh/mapstructure"
)

type DockerDriver struct {
DriverContext
fingerprint.StaticFingerprinter
}

type DockerAuthConfig struct {
UserName string `mapstructure:"auth.username"` // user name of the registry
Password string `mapstructure:"auth.password"` // password to access the registry
Email string `mapstructure:"auth.email"` // email address of the user who is allowed to access the registry
ServerAddress string `mapstructure:"auth.server_address"` // server address of the registry

}

type DockerDriverConfig struct {
DockerAuthConfig
ImageName string `mapstructure:"image"` // Container's Image Name
Command string `mapstructure:"command"` // The Command/Entrypoint to run when the container starts up
Args string `mapstructure:"args"` // The arguments to the Command/Entrypoint
NetworkMode string `mapstructure:"network_mode"` // The network mode of the container - host, net and none
PortMap []map[string]int `mapstructure:"port_map"` // A map of host port labels and the ports exposed on the container
Privileged bool `mapstructure:"privileged"` // Flag to run the container in priviledged mode
DNS string `mapstructure:"dns_server"` // DNS Server for containers
SearchDomains string `mapstructure:"search_domains"` // DNS Search domains for containers
}

func (c *DockerDriverConfig) Validate() error {
if c.ImageName == "" {
return fmt.Errorf("Docker Driver needs an image name")
}

if len(c.PortMap) > 1 {
return fmt.Errorf("Only one port_map block is allowed in the docker driver config")
}
return nil
}

type dockerPID struct {
ImageID string
ContainerID string
Expand Down Expand Up @@ -117,7 +149,7 @@ func (d *DockerDriver) containerBinds(alloc *allocdir.AllocDir, task *structs.Ta
}

// createContainer initializes a struct needed to call docker.client.CreateContainer()
func (d *DockerDriver) createContainer(ctx *ExecContext, task *structs.Task) (docker.CreateContainerOptions, error) {
func (d *DockerDriver) createContainer(ctx *ExecContext, task *structs.Task, driverConfig *DockerDriverConfig) (docker.CreateContainerOptions, error) {
var c docker.CreateContainerOptions
if task.Resources == nil {
d.logger.Printf("[ERR] driver.docker: task.Resources is empty")
Expand All @@ -135,8 +167,7 @@ func (d *DockerDriver) createContainer(ctx *ExecContext, task *structs.Task) (do
env.SetTaskLocalDir(filepath.Join("/", allocdir.TaskLocal))

config := &docker.Config{
Env: env.List(),
Image: task.Config["image"],
Image: driverConfig.ImageName,
}

hostConfig := &docker.HostConfig{
Expand Down Expand Up @@ -185,22 +216,18 @@ func (d *DockerDriver) createContainer(ctx *ExecContext, task *structs.Task) (do
return c, fmt.Errorf("Unable to parse docker.privileged.enabled: %s", err)
}

if v, ok := task.Config["privileged"]; ok {
taskPrivileged, err := strconv.ParseBool(v)
if err != nil {
return c, fmt.Errorf("Unable to parse boolean value from task config option 'privileged': %v", err)
}
if taskPrivileged && !hostPrivileged {
if driverConfig.Privileged {
if !hostPrivileged {
return c, fmt.Errorf(`Unable to set privileged flag since "docker.privileged.enabled" is false`)
}

hostConfig.Privileged = taskPrivileged
hostConfig.Privileged = driverConfig.Privileged
}

// set DNS servers
dns, ok := task.Config["dns-servers"]
dns := driverConfig.DNS

if ok && dns != "" {
if dns != "" {
for _, v := range strings.Split(dns, ",") {
ip := strings.TrimSpace(v)
if net.ParseIP(ip) != nil {
Expand All @@ -212,16 +239,16 @@ func (d *DockerDriver) createContainer(ctx *ExecContext, task *structs.Task) (do
}

// set DNS search domains
dnsSearch, ok := task.Config["search-domains"]
dnsSearch := driverConfig.SearchDomains

if ok && dnsSearch != "" {
if dnsSearch != "" {
for _, v := range strings.Split(dnsSearch, ",") {
hostConfig.DNSSearch = append(hostConfig.DNSSearch, strings.TrimSpace(v))
}
}

mode, ok := task.Config["network_mode"]
if !ok || mode == "" {
mode := driverConfig.NetworkMode
if mode == "" {
// docker default
d.logger.Printf("[WARN] driver.docker: no mode specified for networking, defaulting to bridge")
mode = "bridge"
Expand All @@ -246,73 +273,70 @@ func (d *DockerDriver) createContainer(ctx *ExecContext, task *structs.Task) (do
publishedPorts := map[docker.Port][]docker.PortBinding{}
exposedPorts := map[docker.Port]struct{}{}

for _, port := range network.ListStaticPorts() {
publishedPorts[docker.Port(strconv.Itoa(port)+"/tcp")] = []docker.PortBinding{docker.PortBinding{HostIP: network.IP, HostPort: strconv.Itoa(port)}}
publishedPorts[docker.Port(strconv.Itoa(port)+"/udp")] = []docker.PortBinding{docker.PortBinding{HostIP: network.IP, HostPort: strconv.Itoa(port)}}
d.logger.Printf("[DEBUG] driver.docker: allocated port %s:%d -> %d (static)\n", network.IP, port, port)
exposedPorts[docker.Port(strconv.Itoa(port)+"/tcp")] = struct{}{}
exposedPorts[docker.Port(strconv.Itoa(port)+"/udp")] = struct{}{}
d.logger.Printf("[DEBUG] driver.docker: exposed port %d\n", port)
for _, port := range network.ReservedPorts {
publishedPorts[docker.Port(strconv.Itoa(port.Value)+"/tcp")] = []docker.PortBinding{docker.PortBinding{HostIP: network.IP, HostPort: strconv.Itoa(port.Value)}}
publishedPorts[docker.Port(strconv.Itoa(port.Value)+"/udp")] = []docker.PortBinding{docker.PortBinding{HostIP: network.IP, HostPort: strconv.Itoa(port.Value)}}
d.logger.Printf("[DEBUG] driver.docker: allocated port %s:%d -> %d (static)\n", network.IP, port.Value, port.Value)
exposedPorts[docker.Port(strconv.Itoa(port.Value)+"/tcp")] = struct{}{}
exposedPorts[docker.Port(strconv.Itoa(port.Value)+"/udp")] = struct{}{}
d.logger.Printf("[DEBUG] driver.docker: exposed port %d\n", port.Value)
}

for label, port := range network.MapDynamicPorts() {
// If the label is numeric we expect that there is a service
// listening on that port inside the container. In this case we'll
// setup a mapping from our random host port to the label port.
//
// Otherwise we'll setup a direct 1:1 mapping from the host port to
// the container, and assume that the process inside will read the
// environment variable and bind to the correct port.
if _, err := strconv.Atoi(label); err == nil {
publishedPorts[docker.Port(label+"/tcp")] = []docker.PortBinding{docker.PortBinding{HostIP: network.IP, HostPort: strconv.Itoa(port)}}
publishedPorts[docker.Port(label+"/udp")] = []docker.PortBinding{docker.PortBinding{HostIP: network.IP, HostPort: strconv.Itoa(port)}}
d.logger.Printf("[DEBUG] driver.docker: allocated port %s:%d -> %s (mapped)", network.IP, port, label)
exposedPorts[docker.Port(label+"/tcp")] = struct{}{}
exposedPorts[docker.Port(label+"/udp")] = struct{}{}
d.logger.Printf("[DEBUG] driver.docker: exposed port %d\n", port)
} else {
publishedPorts[docker.Port(strconv.Itoa(port)+"/tcp")] = []docker.PortBinding{docker.PortBinding{HostIP: network.IP, HostPort: strconv.Itoa(port)}}
publishedPorts[docker.Port(strconv.Itoa(port)+"/udp")] = []docker.PortBinding{docker.PortBinding{HostIP: network.IP, HostPort: strconv.Itoa(port)}}
d.logger.Printf("[DEBUG] driver.docker: allocated port %s:%d -> %d for label %s\n", network.IP, port, port, label)
exposedPorts[docker.Port(strconv.Itoa(port)+"/tcp")] = struct{}{}
exposedPorts[docker.Port(strconv.Itoa(port)+"/udp")] = struct{}{}
d.logger.Printf("[DEBUG] driver.docker: exposed port %d\n", port)
containerToHostPortMap := make(map[string]int)
for _, port := range network.DynamicPorts {
containerPort, ok := driverConfig.PortMap[0][port.Label]
if !ok {
containerPort = port.Value
}
cp := strconv.Itoa(containerPort)
hostPort := strconv.Itoa(port.Value)
publishedPorts[docker.Port(cp+"/tcp")] = []docker.PortBinding{docker.PortBinding{HostIP: network.IP, HostPort: hostPort}}
publishedPorts[docker.Port(cp+"/udp")] = []docker.PortBinding{docker.PortBinding{HostIP: network.IP, HostPort: hostPort}}
d.logger.Printf("[DEBUG] driver.docker: allocated port %s:%d -> %d (mapped)", network.IP, port.Value, containerPort)
exposedPorts[docker.Port(cp+"/tcp")] = struct{}{}
exposedPorts[docker.Port(cp+"/udp")] = struct{}{}
d.logger.Printf("[DEBUG] driver.docker: exposed port %s\n", hostPort)
containerToHostPortMap[cp] = port.Value
}

env.SetPorts(containerToHostPortMap)
hostConfig.PortBindings = publishedPorts
config.ExposedPorts = exposedPorts
}

rawArgs, hasArgs := task.Config["args"]
parsedArgs, err := args.ParseAndReplace(rawArgs, env.Map())
parsedArgs, err := args.ParseAndReplace(driverConfig.Args, env.Map())
if err != nil {
return c, err
}

// If the user specified a custom command to run as their entrypoint, we'll
// inject it here.
if command, ok := task.Config["command"]; ok {
cmd := []string{command}
if hasArgs {
if driverConfig.Command != "" {
cmd := []string{driverConfig.Command}
if driverConfig.Args != "" {
cmd = append(cmd, parsedArgs...)
}
config.Cmd = cmd
} else if hasArgs {
} else if driverConfig.Args != "" {
d.logger.Println("[DEBUG] driver.docker: ignoring args because command not specified")
}

config.Env = env.List()
return docker.CreateContainerOptions{
Config: config,
HostConfig: hostConfig,
}, nil
}

func (d *DockerDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, error) {
// Get the image from config
image, ok := task.Config["image"]
if !ok || image == "" {
return nil, fmt.Errorf("Image not specified")
var driverConfig DockerDriverConfig
if err := mapstructure.WeakDecode(task.Config, &driverConfig); err != nil {
return nil, err
}
image := driverConfig.ImageName

if err := driverConfig.Validate(); err != nil {
return nil, err
}
if task.Resources == nil {
return nil, fmt.Errorf("Resources are not specified")
Expand Down Expand Up @@ -362,10 +386,10 @@ func (d *DockerDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle
}

authOptions := docker.AuthConfiguration{
Username: task.Config["auth.username"],
Password: task.Config["auth.password"],
Email: task.Config["auth.email"],
ServerAddress: task.Config["auth.server-address"],
Username: driverConfig.UserName,
Password: driverConfig.Password,
Email: driverConfig.Email,
ServerAddress: driverConfig.ServerAddress,
}

err = client.PullImage(pullOptions, authOptions)
Expand All @@ -385,7 +409,7 @@ func (d *DockerDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle
d.logger.Printf("[DEBUG] driver.docker: using image %s", dockerImage.ID)
d.logger.Printf("[INFO] driver.docker: identified image %s as %s", image, dockerImage.ID)

config, err := d.createContainer(ctx, task)
config, err := d.createContainer(ctx, task, &driverConfig)
if err != nil {
d.logger.Printf("[ERR] driver.docker: %s", err)
return nil, fmt.Errorf("Failed to create container config for image %s", image)
Expand Down
Loading

0 comments on commit a17ce1e

Please sign in to comment.