Skip to content

Commit

Permalink
Merge pull request #25 from scraperwiki/add-port-options
Browse files Browse the repository at this point in the history
Add -p option to publish ports
  • Loading branch information
pwaller committed Dec 10, 2014
2 parents f30fced + 8e48e78 commit aa0ba81
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 35 deletions.
1 change: 0 additions & 1 deletion container.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,6 @@ func (c *Container) Run(event UpdateEvent) (int, error) {
return
}
c.Ready.Fall()
log.Println("Listening on", c.container.NetworkSettings.PortMappingAPI())
}()

return c.Wait()
Expand Down
44 changes: 27 additions & 17 deletions iptables.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"fmt"
"log"
"os"
"os/exec"
)

Expand All @@ -26,43 +27,52 @@ func CheckIPTables() error {

// Invoke one iptables command.
// Expects "iptables" in the path to be runnable with reasonable permissions.
func iptables(action Action, chain string, source, target int64) *exec.Cmd {
func iptables(action Action, chain string, source, target int, ipAddress string) *exec.Cmd {
var cmd *exec.Cmd

switch action {
case INSERT:
return exec.Command(
"iptables", "-I", chain, "1",
"-t", "nat", "-j", "REDIRECT",
"-p", "tcp", "-m", "tcp",
"--dport", fmt.Sprint(source), "--to-ports", fmt.Sprint(target))
cmd = exec.Command(
"iptables", "--insert", chain, "1",
"--table", "nat",
"--protocol", "tcp", "--match", "tcp",
"!", "--destination", ipAddress,
"--dport", fmt.Sprint(source),
"--jump", "REDIRECT",
"--to-ports", fmt.Sprint(target))
case DELETE:
return exec.Command(
"iptables", "-D", chain,
"-t", "nat", "-j", "REDIRECT",
"-p", "tcp", "-m", "tcp",
"--dport", fmt.Sprint(source), "--to-ports", fmt.Sprint(target))
cmd = exec.Command(
"iptables", "--delete", chain,
"--table", "nat",
"--protocol", "tcp", "--match", "tcp",
"!", "--destination", ipAddress,
"--dport", fmt.Sprint(source),
"--jump", "REDIRECT",
"--to-ports", fmt.Sprint(target))
}
panic("unreachable")
cmd.Stderr = os.Stderr
return cmd
}

// Configure one port redirect from `source` to `target` using iptables.
// Returns an error and a function which undoes the change to the firewall.
func ConfigureRedirect(source, target int64) (func(), error) {
func ConfigureRedirect(source, target int, ipAddress string) (func(), error) {

err := iptables(INSERT, "PREROUTING", source, target).Run()
err := iptables(INSERT, "PREROUTING", source, target, ipAddress).Run()
if err != nil {
return nil, err
}
err = iptables(INSERT, "OUTPUT", source, target).Run()
err = iptables(INSERT, "OUTPUT", source, target, ipAddress).Run()
if err != nil {
return nil, err
}

remove := func() {
err := iptables(DELETE, "PREROUTING", source, target).Run()
err := iptables(DELETE, "PREROUTING", source, target, ipAddress).Run()
if err != nil {
log.Println("Failed to remove iptables rule:", source, target)
}
err = iptables(DELETE, "OUTPUT", source, target).Run()
err = iptables(DELETE, "OUTPUT", source, target, ipAddress).Run()
if err != nil {
log.Println("Failed to remove iptables rule:", source, target)
}
Expand Down
90 changes: 73 additions & 17 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"sync"
"sync/atomic"

"github.com/docker/docker/nat"
"github.com/docker/docker/opts"
"github.com/docker/docker/pkg/mflag"
"github.com/fsouza/go-dockerclient"
Expand All @@ -39,6 +40,8 @@ type Options struct {
env, publish opts.ListOpts
source ContainerSource
containerArgs []string
ports nat.PortSet
portBindings nat.PortMap
}

type UpdateEvent struct {
Expand All @@ -48,6 +51,7 @@ type UpdateEvent struct {
}

func main() {
var err error

options := Options{
env: opts.NewListOpts(nil),
Expand Down Expand Up @@ -78,6 +82,11 @@ func main() {
log.Fatal("Unable to run `iptables -L`, see README (", err, ")")
}

options.ports, options.portBindings, err = nat.ParsePortSpecs(options.publish.GetAll())
if err != nil {
log.Fatalln("--publish:", err)
}

log.Println("Hanoverd")

var wg sync.WaitGroup
Expand Down Expand Up @@ -205,7 +214,7 @@ func dockerConnect() (*docker.Client, error) {
}

// Main loop managing the lifecycle of all containers.
func loop(wg *sync.WaitGroup, dying *barrier.Barrier, options Options, events <-chan UpdateEvent) {
func loop(wg *sync.WaitGroup, dying *barrier.Barrier, options Options, events chan UpdateEvent) {
client, err := dockerConnect()
if err != nil {
dying.Fall()
Expand Down Expand Up @@ -256,6 +265,17 @@ func loop(wg *sync.WaitGroup, dying *barrier.Barrier, options Options, events <-

status, err := c.Run(lastEvent)
if err != nil {
switch err := err.(type) {
case *docker.Error:
// (name) Conflict
if err.Status == 409 {
// retry
log.Printf("Container with name %q exists, using a new name...", c.Name)
events <- lastEvent
c.Failed.Fall()
return
}
}
log.Println("Container run failed:", strings.TrimSpace(err.Error()))
c.Failed.Fall()
return
Expand Down Expand Up @@ -288,31 +308,67 @@ func loop(wg *sync.WaitGroup, dying *barrier.Barrier, options Options, events <-
defer liveMutex.Unlock()
previousLive := live

// Block main exit until the firewall rule has been removed
// Block main exit until the firewall rule has been placed
// (and removed)
wg.Add(1)
defer wg.Done()

target := c.container.NetworkSettings.PortMappingAPI()[0].PublicPort
remove, err := ConfigureRedirect(5555, target)
if err != nil {
// Firewall rule didn't get applied.
log.Println("Firewall rule application failed:", err)
wg.Done()
c.err(err)
return
// get the public port for an internal one
getMappedPort := func(p int) (int, bool) {
for _, m := range c.container.NetworkSettings.PortMappingAPI() {
if int(m.PrivatePort) == p {
return int(m.PublicPort), true
}
}
return -1, false
}

removal := []func(){}

defer func() {
// Block main exit until firewall rule has been removed
wg.Add(1)
go func() {
defer wg.Done()

<-c.Closing.Barrier()
for _, remove := range removal {
remove()
}
}()
}()

for internalPort, bindings := range options.portBindings {
if mappedPort, ok := getMappedPort(internalPort.Int()); ok {
for _, binding := range bindings {
var public int
_, err := fmt.Sscan(binding.HostPort, &public)
if err != nil {
// If no public port specified, use same port as internal port
public = internalPort.Int()
}

ipAddress := c.container.NetworkSettings.IPAddress
remove, err := ConfigureRedirect(public, mappedPort, ipAddress)
if err != nil {
// Firewall rule didn't get applied.
c.err(fmt.Errorf("Firewall rule application failed: %q (public: %v, private: %v)", err, public, internalPort))
return
}

removal = append(removal, remove)
}
} else {
c.err(fmt.Errorf("Docker image not exposing port %v!", internalPort))
return
}
}

live = c
if previousLive != nil {
previousLive.Closing.Fall()
}

// Networking
go func() {
defer wg.Done()

<-c.Closing.Barrier()
remove()
}()
}(c)

lastEvent = <-events
Expand Down

0 comments on commit aa0ba81

Please sign in to comment.