libnetwork client base infra

This is an experiment by modularizing the client UI handler in libnetwork
while the actual UI hook to the docker chain can come from Docker Project.

Signed-off-by: Madhu Venugopal <madhu@docker.com>
This commit is contained in:
Madhu Venugopal 2015-04-26 00:19:01 -07:00
parent d152bb91c4
commit 263ee2dbbb
3 changed files with 426 additions and 0 deletions

112
libnetwork/client/client.go Normal file
View File

@ -0,0 +1,112 @@
package client
import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"reflect"
"strings"
flag "github.com/docker/docker/pkg/mflag"
)
// CallFunc provides environment specific call utility to invoke backend functions from UI
type CallFunc func(string, string, interface{}, map[string][]string) (io.ReadCloser, int, error)
// NetworkCli is the UI object for network subcmds
type NetworkCli struct {
out io.Writer
err io.Writer
call CallFunc
}
// NewNetworkCli is a conveninent function to create a NetworkCli object
func NewNetworkCli(out, err io.Writer, call CallFunc) *NetworkCli {
return &NetworkCli{
out: out,
err: err,
call: call,
}
}
// getMethod is Borrowed from Docker UI which uses reflection to identify the UI Handler
func (cli *NetworkCli) getMethod(args ...string) (func(string, ...string) error, bool) {
camelArgs := make([]string, len(args))
for i, s := range args {
if len(s) == 0 {
return nil, false
}
camelArgs[i] = strings.ToUpper(s[:1]) + strings.ToLower(s[1:])
}
methodName := "Cmd" + strings.Join(camelArgs, "")
method := reflect.ValueOf(cli).MethodByName(methodName)
if !method.IsValid() {
return nil, false
}
return method.Interface().(func(string, ...string) error), true
}
// Cmd is borrowed from Docker UI and acts as the entry point for network UI commands.
// network UI commands are designed to be invoked from multiple parent chains
func (cli *NetworkCli) Cmd(chain string, args ...string) error {
var errStr string
if len(args) > 1 {
method, exists := cli.getMethod(args[:2]...)
if exists {
return method(chain, args[2:]...)
}
}
if len(args) > 0 {
method, exists := cli.getMethod(args[0])
if !exists {
errStr = fmt.Sprintf("%s: '%s' is not a %s command. See '%s --help'.\n", chain, args[0], chain, chain)
fmt.Fprintf(cli.err, errStr)
return errors.New(errStr)
}
return method(chain, args[1:]...)
}
errStr = fmt.Sprintf("'%s' is not a valid command. See '%s --help'.\n", chain, chain)
fmt.Fprintf(cli.err, errStr)
return errors.New(errStr)
}
// Subcmd is borrowed from Docker UI and performs the same function of configuring the subCmds
func (cli *NetworkCli) Subcmd(chain, name, signature, description string, exitOnError bool) *flag.FlagSet {
var errorHandling flag.ErrorHandling
if exitOnError {
errorHandling = flag.ExitOnError
} else {
errorHandling = flag.ContinueOnError
}
flags := flag.NewFlagSet(name, errorHandling)
flags.Usage = func() {
options := ""
if signature != "" {
signature = " " + signature
}
if flags.FlagCountUndeprecated() > 0 {
options = " [OPTIONS]"
}
fmt.Fprintf(cli.out, "\nUsage: %s %s%s%s\n\n%s\n\n", chain, name, options, signature, description)
flags.SetOutput(cli.out)
flags.PrintDefaults()
os.Exit(0)
}
return flags
}
func readBody(stream io.ReadCloser, statusCode int, err error) ([]byte, int, error) {
if stream != nil {
defer stream.Close()
}
if err != nil {
return nil, statusCode, err
}
body, err := ioutil.ReadAll(stream)
if err != nil {
return nil, -1, err
}
return body, statusCode, nil
}

View File

@ -0,0 +1,178 @@
package client
import (
"bytes"
"io"
"testing"
)
// nopCloser is used to provide a dummy CallFunc for Cmd()
type nopCloser struct {
io.Reader
}
func (nopCloser) Close() error { return nil }
func TestClientDummyCommand(t *testing.T) {
var out, errOut bytes.Buffer
cFunc := func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, int, error) {
return nopCloser{bytes.NewBufferString("")}, 200, nil
}
cli := NewNetworkCli(&out, &errOut, cFunc)
err := cli.Cmd("docker", "dummy")
if err == nil {
t.Fatalf("Incorrect Command must fail")
}
}
func TestClientNoCommand(t *testing.T) {
var out, errOut bytes.Buffer
cFunc := func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, int, error) {
return nopCloser{bytes.NewBufferString("")}, 200, nil
}
cli := NewNetworkCli(&out, &errOut, cFunc)
err := cli.Cmd("docker")
if err == nil {
t.Fatalf("Incorrect Command must fail")
}
}
func TestClientNetworkCreate(t *testing.T) {
var out, errOut bytes.Buffer
cFunc := func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, int, error) {
return nopCloser{bytes.NewBufferString("")}, 200, nil
}
cli := NewNetworkCli(&out, &errOut, cFunc)
err := cli.Cmd("docker", "network", "create", "test")
if err != nil {
t.Fatal(err.Error())
}
}
func TestClientNetworkCreateWithDriver(t *testing.T) {
var out, errOut bytes.Buffer
cFunc := func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, int, error) {
return nopCloser{bytes.NewBufferString("")}, 200, nil
}
cli := NewNetworkCli(&out, &errOut, cFunc)
err := cli.Cmd("docker", "network", "create", "-f=dummy", "test")
if err == nil {
t.Fatalf("Passing incorrect flags to the create command must fail")
}
err = cli.Cmd("docker", "network", "create", "-d=dummy", "test")
if err != nil {
t.Fatalf(err.Error())
}
}
func TestClientNetworkRm(t *testing.T) {
var out, errOut bytes.Buffer
cFunc := func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, int, error) {
return nopCloser{bytes.NewBufferString("")}, 200, nil
}
cli := NewNetworkCli(&out, &errOut, cFunc)
err := cli.Cmd("docker", "network", "rm", "test")
if err != nil {
t.Fatal(err.Error())
}
}
func TestClientNetworkLs(t *testing.T) {
var out, errOut bytes.Buffer
networks := "db,web,test"
cFunc := func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, int, error) {
return nopCloser{bytes.NewBufferString(networks)}, 200, nil
}
cli := NewNetworkCli(&out, &errOut, cFunc)
err := cli.Cmd("docker", "network", "ls")
if err != nil {
t.Fatal(err.Error())
}
if out.String() != networks {
t.Fatal("Network List command fail to return the intended list")
}
}
func TestClientNetworkInfo(t *testing.T) {
var out, errOut bytes.Buffer
info := "dummy info"
cFunc := func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, int, error) {
return nopCloser{bytes.NewBufferString(info)}, 200, nil
}
cli := NewNetworkCli(&out, &errOut, cFunc)
err := cli.Cmd("docker", "network", "info", "test")
if err != nil {
t.Fatal(err.Error())
}
if out.String() != info {
t.Fatal("Network List command fail to return the intended list")
}
}
func TestClientNetworkJoin(t *testing.T) {
var out, errOut bytes.Buffer
cFunc := func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, int, error) {
return nopCloser{bytes.NewBufferString("")}, 200, nil
}
cli := NewNetworkCli(&out, &errOut, cFunc)
err := cli.Cmd("docker", "network", "join", "db1", "dbnet", "db1-ep")
if err != nil {
t.Fatal(err.Error())
}
}
func TestClientNetworkLeave(t *testing.T) {
var out, errOut bytes.Buffer
cFunc := func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, int, error) {
return nopCloser{bytes.NewBufferString("")}, 200, nil
}
cli := NewNetworkCli(&out, &errOut, cFunc)
err := cli.Cmd("docker", "network", "leave", "db1", "dbnet")
if err != nil {
t.Fatal(err.Error())
}
}
// Docker Flag processing in flag.go uses os.Exit(0) for --help
// TODO : Handle the --help test-case in the IT when CLI is available
/*
func TestClientNetworkCreateHelp(t *testing.T) {
var out, errOut bytes.Buffer
cFunc := func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, int, error) {
return nil, 0, nil
}
cli := NewNetworkCli(&out, &errOut, cFunc)
err := cli.Cmd("docker", "network", "create", "--help")
if err != nil {
t.Fatalf(err.Error())
}
}
*/
// Docker flag processing in flag.go uses os.Exit(1) for incorrect paramater case.
// TODO : Handle the missing argument case in the IT when CLI is available
/*
func TestClientNetworkCreateMissingArgument(t *testing.T) {
var out, errOut bytes.Buffer
cFunc := func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, int, error) {
return nil, 0, nil
}
cli := NewNetworkCli(&out, &errOut, cFunc)
err := cli.Cmd("docker", "network", "create")
if err != nil {
t.Fatal(err.Error())
}
}
*/

View File

@ -0,0 +1,136 @@
package client
import (
"bytes"
"fmt"
"io"
flag "github.com/docker/docker/pkg/mflag"
)
// CmdNetworkCreate handles Network Create UI
func (cli *NetworkCli) CmdNetworkCreate(chain string, args ...string) error {
cmd := cli.Subcmd(chain, "create", "NETWORK-NAME", chain+" create", false)
flDriver := cmd.String([]string{"d", "-driver"}, "", "Driver to manage the Network")
cmd.Require(flag.Min, 1)
err := cmd.ParseFlags(args, true)
if err != nil {
return err
}
if *flDriver == "" {
*flDriver = "bridge"
}
// TODO : Proper Backend handling
obj, _, err := readBody(cli.call("POST", "/networks/"+args[0], nil, nil))
if err != nil {
fmt.Fprintf(cli.err, "%s", err.Error())
return err
}
if _, err := io.Copy(cli.out, bytes.NewReader(obj)); err != nil {
return err
}
return nil
}
// CmdNetworkRm handles Network Delete UI
func (cli *NetworkCli) CmdNetworkRm(chain string, args ...string) error {
cmd := cli.Subcmd(chain, "rm", "NETWORK-NAME", chain+" rm", false)
cmd.Require(flag.Min, 1)
err := cmd.ParseFlags(args, true)
if err != nil {
return err
}
// TODO : Proper Backend handling
obj, _, err := readBody(cli.call("DELETE", "/networks/"+args[0], nil, nil))
if err != nil {
fmt.Fprintf(cli.err, "%s", err.Error())
return err
}
if _, err := io.Copy(cli.out, bytes.NewReader(obj)); err != nil {
return err
}
return nil
}
// CmdNetworkLs handles Network List UI
func (cli *NetworkCli) CmdNetworkLs(chain string, args ...string) error {
cmd := cli.Subcmd(chain, "ls", "", chain+" ls", false)
err := cmd.ParseFlags(args, true)
if err != nil {
return err
}
// TODO : Proper Backend handling
obj, _, err := readBody(cli.call("GET", "/networks", nil, nil))
if err != nil {
fmt.Fprintf(cli.err, "%s", err.Error())
return err
}
if _, err := io.Copy(cli.out, bytes.NewReader(obj)); err != nil {
return err
}
return nil
}
// CmdNetworkInfo handles Network Info UI
func (cli *NetworkCli) CmdNetworkInfo(chain string, args ...string) error {
cmd := cli.Subcmd(chain, "info", "NETWORK-NAME", chain+" info", false)
cmd.Require(flag.Min, 1)
err := cmd.ParseFlags(args, true)
if err != nil {
return err
}
// TODO : Proper Backend handling
obj, _, err := readBody(cli.call("GET", "/networks/"+args[0], nil, nil))
if err != nil {
fmt.Fprintf(cli.err, "%s", err.Error())
return err
}
if _, err := io.Copy(cli.out, bytes.NewReader(obj)); err != nil {
return err
}
return nil
}
// CmdNetworkJoin handles the UI to let a Container join a Network via an endpoint
// Sample UI : <chain> network join <container-name/id> <network-name/id> [<endpoint-name>]
func (cli *NetworkCli) CmdNetworkJoin(chain string, args ...string) error {
cmd := cli.Subcmd(chain, "join", "CONTAINER-NAME/ID NETWORK-NAME/ID [ENDPOINT-NAME]",
chain+" join", false)
cmd.Require(flag.Min, 2)
err := cmd.ParseFlags(args, true)
if err != nil {
return err
}
// TODO : Proper Backend handling
obj, _, err := readBody(cli.call("POST", "/endpoints/", nil, nil))
if err != nil {
fmt.Fprintf(cli.err, "%s", err.Error())
return err
}
if _, err := io.Copy(cli.out, bytes.NewReader(obj)); err != nil {
return err
}
return nil
}
// CmdNetworkLeave handles the UI to let a Container disconnect from a Network
// Sample UI : <chain> network leave <container-name/id> <network-name/id>
func (cli *NetworkCli) CmdNetworkLeave(chain string, args ...string) error {
cmd := cli.Subcmd(chain, "leave", "CONTAINER-NAME/ID NETWORK-NAME/ID",
chain+" leave", false)
cmd.Require(flag.Min, 2)
err := cmd.ParseFlags(args, true)
if err != nil {
return err
}
// TODO : Proper Backend handling
obj, _, err := readBody(cli.call("PUT", "/endpoints/", nil, nil))
if err != nil {
fmt.Fprintf(cli.err, "%s", err.Error())
return err
}
if _, err := io.Copy(cli.out, bytes.NewReader(obj)); err != nil {
return err
}
return nil
}