-
Notifications
You must be signed in to change notification settings - Fork 21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support Docker mode #63
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,8 @@ import ( | |
"github.com/dachad/tcpgoon/debugging" | ||
"github.com/dachad/tcpgoon/mtcpclient" | ||
"github.com/dachad/tcpgoon/tcpclient" | ||
"github.com/dachad/tcpgoon/docker" | ||
|
||
"github.com/spf13/cobra" | ||
) | ||
|
||
|
@@ -24,6 +26,8 @@ type tcpgoonParams struct { | |
debug bool | ||
reportingInterval int | ||
assumeyes bool | ||
docker bool | ||
containerID string | ||
} | ||
|
||
var params tcpgoonParams | ||
|
@@ -33,12 +37,12 @@ var runCmd = &cobra.Command{ | |
Short: "Run tcpgoon test", | ||
Long: ``, | ||
PreRun: func(cmd *cobra.Command, args []string) { | ||
enableDebuggingIfFlagSet(params) | ||
if err := validateRequiredArgs(¶ms, args); err != nil { | ||
cmd.Println(err) | ||
cmd.Println(cmd.UsageString()) | ||
os.Exit(1) | ||
} | ||
enableDebuggingIfFlagSet(params) | ||
autorunValidation(params) | ||
}, | ||
Run: func(cmd *cobra.Command, args []string) { | ||
|
@@ -53,26 +57,65 @@ func init() { | |
runCmd.Flags().BoolVarP(¶ms.debug, "debug", "d", false, "Print debugging information to the standard error") | ||
runCmd.Flags().IntVarP(¶ms.reportingInterval, "interval", "i", 1, "Interval, in seconds, between stats updates") | ||
runCmd.Flags().BoolVarP(¶ms.assumeyes, "assume-yes", "y", false, "Force execution without asking for confirmation") | ||
runCmd.Flags().BoolVarP(¶ms.docker, "docker", "", false, "Running against a docker image. " + | ||
"Use the image reference instead of the <host>, and a custom mapped port if the first exposed one is not ok (optional") | ||
} | ||
|
||
func validateRequiredArgs(params *tcpgoonParams, args []string) error { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we must redo how we are validating parms. Relying on a max/min num of required params looks ugly: we should just check which parm is required and which are not in each execution mode. If execution modes parameters differ a lot in the end, maybe we should recover your idea of using different commands There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, the execution mode is the first question to answer |
||
if len(args) != 2 { | ||
return errors.New("Number of required parameters doesn't match") | ||
var minNumberRequiredArgs int | ||
var maxNumberRequiredArgs int | ||
|
||
if params.docker { | ||
minNumberRequiredArgs = 1 | ||
maxNumberRequiredArgs = 2 | ||
} else { | ||
minNumberRequiredArgs = 2 | ||
maxNumberRequiredArgs = 2 | ||
} | ||
params.target = args[0] | ||
addrs, err := net.LookupIP(params.target) | ||
if err != nil || len(addrs) == 0 { | ||
return errors.New("Domain name not resolvable") | ||
|
||
if len(args) < minNumberRequiredArgs || len(args) > maxNumberRequiredArgs{ | ||
return errors.New("Number of required parameters doesn't match") | ||
} | ||
|
||
params.targetip = addrs[0].String() | ||
fmt.Fprintln(debugging.DebugOut, "TCPGOON target: Hostname(", params.target, "), IP (", params.targetip, ")") | ||
if params.docker { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this massive if/else is... :) . That smells to two different validation functions depending on the "mode", but again, we should rethink the parms in general |
||
dockerImage := args[0] | ||
|
||
fmt.Println("Starting Docker test for", dockerImage) | ||
|
||
var port int | ||
if len(args) == 2 { | ||
|
||
portInt, err := strconv.Atoi(args[1]) | ||
port = portInt | ||
|
||
port, err := strconv.Atoi(args[1]) | ||
if err != nil && port <= 0 { | ||
return errors.New("Port argument is not a valid integer") | ||
if err != nil && port <= 0 { | ||
return errors.New("Port argument is not a valid integer") | ||
} | ||
} else { | ||
port = 0 | ||
} | ||
|
||
target, containerID := docker.DownloadAndRun(dockerImage, port) | ||
params.containerID = containerID | ||
params.target = target.IP | ||
params.port = target.Port | ||
|
||
} else { | ||
params.target = args[0] | ||
addrs, err := net.LookupIP(params.target) | ||
if err != nil || len(addrs) == 0 { | ||
return errors.New("Domain name not resolvable") | ||
} | ||
|
||
params.targetip = addrs[0].String() | ||
fmt.Fprintln(debugging.DebugOut, "TCPGOON target: Hostname(", params.target, "), IP (", params.targetip, ")") | ||
|
||
port, err := strconv.Atoi(args[1]) | ||
if err != nil && port <= 0 { | ||
return errors.New("Port argument is not a valid integer") | ||
} | ||
params.port = port | ||
} | ||
params.port = port | ||
|
||
return nil | ||
} | ||
|
@@ -86,18 +129,28 @@ func enableDebuggingIfFlagSet(params tcpgoonParams) { | |
func autorunValidation(params tcpgoonParams) { | ||
if !(params.assumeyes || cmdutil.AskForUserConfirmation(params.target, params.port, params.numberConnections)) { | ||
fmt.Fprintln(debugging.DebugOut, "Execution not approved by the user") | ||
if params.docker{ | ||
docker.Stop(params.containerID) | ||
} | ||
cmdutil.CloseAbruptly() | ||
} | ||
} | ||
|
||
func run(params tcpgoonParams) { | ||
|
||
tcpclient.DefaultDialTimeoutInMs = params.connDialTimeout | ||
|
||
// TODO: we should decouple the caller from the mtcpclient package (too many structures being moved from | ||
// one side to the other.. everything in a single structure, or applying something like the builder pattern, | ||
// may help | ||
connStatusCh, connStatusTracker := mtcpclient.StartBackgroundReporting(params.numberConnections, params.reportingInterval) | ||
closureCh := mtcpclient.StartBackgroundClosureTrigger(*connStatusTracker) | ||
if params.docker{ | ||
go func() { | ||
<-closureCh | ||
docker.Stop(params.containerID) | ||
}() | ||
} | ||
mtcpclient.MultiTCPConnect(params.numberConnections, params.delay, params.target, params.port, connStatusCh, closureCh) | ||
fmt.Fprintln(debugging.DebugOut, "Tests execution completed") | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
package docker | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"io" | ||
"math/rand" | ||
"net" | ||
|
||
//"os" | ||
"strconv" | ||
|
||
"github.com/docker/docker/api/types" | ||
"github.com/docker/docker/api/types/container" | ||
"github.com/docker/docker/client" | ||
"github.com/docker/go-connections/nat" | ||
|
||
"github.com/dachad/tcpgoon/debugging" | ||
) | ||
|
||
type IPandPort struct { | ||
IP string | ||
Port int | ||
} | ||
|
||
func lookForAvailableLocalPort() (availablePort int){ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. up to you, but i would try the same the docker is exposing, and if that fails, require the user to be the one that chooses one |
||
minPort := 1025 | ||
maxPort := 65535 | ||
|
||
for { | ||
availablePort = rand.Intn(maxPort - minPort) + minPort | ||
ln, err := net.Listen("tcp", ":" + strconv.Itoa(availablePort)) | ||
ln.Close() | ||
if err == nil { | ||
break | ||
} | ||
} | ||
return availablePort | ||
} | ||
|
||
func DownloadAndRun(image string, port int) (targetinfo IPandPort, containerID string) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i guess this requires refactor / better encapsulation... long and with too much statements inside if/elses. I would also not panic in this function, but return an error and make the caller to manage the errors accordingly There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sure, do not take this PR as ready to review. The point here is, having a first working version, understand if this docker mode should be part of the regular command or have a custom one. |
||
ctx := context.Background() | ||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
fmt.Fprintln(debugging.DebugOut,"Pulling Docker image", image) | ||
|
||
out, err := cli.ImagePull(ctx, image, types.ImagePullOptions{}) | ||
if err != nil { | ||
panic(err) | ||
} | ||
io.Copy(debugging.DebugOut, out) | ||
|
||
defer out.Close() | ||
|
||
fmt.Fprintln(debugging.DebugOut, "Docker image", image, "downloaded") | ||
|
||
mappedPort := "" | ||
mapperProto := "" | ||
if port == 0 { | ||
// if not specific mapping defined we look for exposed ports | ||
// getting the exposed port before decide the mapping | ||
|
||
// TODO support mapping volumes, some containers required them | ||
respCreate, err := cli.ContainerCreate(ctx, &container.Config{ | ||
Image: image, | ||
}, nil, nil, "") | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
if err := cli.ContainerStart(ctx, respCreate.ID, types.ContainerStartOptions{}); err != nil { | ||
panic(err) | ||
} | ||
|
||
respInspect := inspect(respCreate.ID) | ||
natPorts := respInspect.NetworkSettings.NetworkSettingsBase.Ports | ||
|
||
for k, _ := range natPorts { | ||
mappedPort = k.Port() | ||
mapperProto = k.Proto() | ||
break | ||
} | ||
// Stopping container used to get the port mapping | ||
Stop(respCreate.ID) | ||
} else { | ||
mappedPort = strconv.Itoa(port) | ||
mapperProto = "tcp" | ||
} | ||
|
||
if mappedPort == "" || mapperProto != "tcp" { | ||
panic("Not found any TCP port mapping") | ||
} else { | ||
fmt.Println("Internal Docker port binding:", mapperProto, mappedPort) | ||
} | ||
|
||
// hack https://stackoverflow.com/questions/47395973/issue-while-using-docker-api-for-go-cannot-import-nat | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we need to nat / expose the port in the host 0.0.0.0? Maybe we can target the container IP if an internal subnet/bridge network is being used There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. changing the network defaults is another option, but I considered this simpler |
||
|
||
hostBinding := nat.PortBinding{ | ||
HostIP: "0.0.0.0", | ||
HostPort: strconv.Itoa(lookForAvailableLocalPort()), | ||
} | ||
containerPort, err := nat.NewPort(mapperProto, mappedPort) | ||
if err != nil { | ||
panic("Unable to get the port") | ||
} | ||
|
||
portBinding := nat.PortMap{containerPort: []nat.PortBinding{hostBinding}} | ||
resp, err := cli.ContainerCreate( | ||
context.Background(), | ||
&container.Config{ | ||
Image: image, | ||
}, | ||
&container.HostConfig{ | ||
PortBindings: portBinding, | ||
}, nil, "") | ||
if err != nil { | ||
panic(err) | ||
} | ||
fmt.Fprintln(debugging.DebugOut, "Docker container built") | ||
|
||
if err := cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil { | ||
panic(err) | ||
} | ||
fmt.Fprintln(debugging.DebugOut, "Started docker container", resp.ID) | ||
|
||
targetinfo.IP = "127.0.0.1" | ||
targetinfo.Port, _ = strconv.Atoi(hostBinding.HostPort) | ||
|
||
return targetinfo, resp.ID | ||
} | ||
|
||
func Stop(containerID string) { | ||
ctx := context.Background() | ||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
fmt.Fprintln(debugging.DebugOut, "Stopping container", containerID, "...") | ||
if err := cli.ContainerStop(ctx, containerID, nil); err != nil { | ||
panic(err) | ||
} | ||
fmt.Fprintln(debugging.DebugOut, "Container stopped successfully") | ||
} | ||
|
||
func inspect(containerID string) types.ContainerJSON { | ||
ctx := context.Background() | ||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
fmt.Fprintln(debugging.DebugOut, "Getting container info", containerID, "...") | ||
resp, err := cli.ContainerInspect(ctx, containerID) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
return resp | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
re-using things like the "host" to inject the image name is a bit hacky. But that is a minor thing