From a9f58fba7cb1ac736b5046c122c5d54215e18f35 Mon Sep 17 00:00:00 2001 From: vfarcic Date: Sat, 21 May 2016 00:22:00 +0200 Subject: [PATCH 1/5] Started templates --- README.md | 41 ++++++++------- articles/templates.md | 5 ++ consul.go | 1 - docker-compose-setup.yml | 22 +++++++- docker-compose.yml | 9 +--- docker-flow.yml | 2 +- flow.go | 1 + flow_test.go | 3 ++ ha_proxy.go | 43 +++++++++++++-- ha_proxy_test.go | 91 ++++++++++++++++++++++++++++---- integration_test.go | 109 ++++++++++++++++++++++++++------------- main.go | 1 - opts.go | 30 +++++++---- opts_test.go | 31 ++++++++++- proxy.go | 2 +- proxy_test.go | 6 +-- setup.sh | 6 +-- util.go | 1 + 18 files changed, 304 insertions(+), 100 deletions(-) create mode 100644 articles/templates.md diff --git a/README.md b/README.md index 6ebc606..8f5bac9 100644 --- a/README.md +++ b/README.md @@ -333,44 +333,47 @@ Arguments can be specified through *docker-flow.yml* file, environment variables |Command argument |Description| |-------------------------------------|-----------| -|-H, --host= |Docker daemon socket to connect to. If not specified, DOCKER_HOST environment variable will be used instead.| +|-b, --blue-green |Perform blue-green deployment. (**bool**)| | --cert-path= |Docker certification path. If not specified, DOCKER_CERT_PATH environment variable will be used instead.| |-f, --compose-path=docker-compose.yml|Path to the Docker Compose configuration file. (default: docker-compose.yml)| -|-b, --blue-green |Perform blue-green deployment. (**bool**)| -|-t, --target= |Docker Compose target. (default: app)| -|-T, --side-target= |Side or auxiliary Docker Compose targets. Multiple values are allowed. (default: [db]) (**multi**)| -|-S, --pull-side-targets |Pull side or auxiliary targets. (**bool**)| -|-p, --project= |Docker Compose project. If not specified, the current directory will be used instead.| |-c, --consul-address= |The address of the Consul server.| -|-s, --scale= |Number of instances to deploy. If the value starts with the plus sign (+), the number of instances will be increased by the given number. If the value begins with the minus sign (-), the number of instances will be decreased by the given number.| +| --consul-template-path= |The path to the Consul Template. If specified, proxy template will be loaded from the specified file.| |-F, --flow= |The actions that should be performed as the flow. Multiple values are allowed.
**deploy**: Deploys a new release
**scale**: Scales currently running release
**stop-old**: Stops the old release
**proxy**: Reconfigures the proxy
(default: [deploy]) (**multi**)| -| --proxy-host= |The host of the proxy. Visitors should request services from this domain. Docker Flow uses it to request reconfiguration when a new service is deployed or an existing one is scaled. This argument is required only if the proxy flow step is used.| -| --proxy-docker-host= |Docker daemon socket of the proxy host. This argument is required only if the proxy flow step is used.| +|-h, --help |Show this help message| +|-H, --host= |Docker daemon socket to connect to. If not specified, DOCKER_HOST environment variable will be used instead.| +|-p, --project= |Docker Compose project. If not specified, the current directory will be used instead.| | --proxy-docker-cert-path= |Docker certification path for the proxy host.| +| --proxy-docker-host= |Docker daemon socket of the proxy host. This argument is required only if the proxy flow step is used.| +| --proxy-host= |The host of the proxy. Visitors should request services from this domain. Docker Flow uses it to request reconfiguration when a new service is deployed or an existing one is scaled. This argument is required only if the proxy flow step is used.| | --proxy-reconf-port= |The port used by the proxy to reconfigure its configuration| +|-S, --pull-side-targets |Pull side or auxiliary targets. (**bool**)| +|-s, --scale= |Number of instances to deploy. If the value starts with the plus sign (+), the number of instances will be increased by the given number. If the value begins with the minus sign (-), the number of instances will be decreased by the given number.| | --service-path= |Path that should be configured in the proxy (e.g. /api/v1/my-service). This argument is required only if the proxy flow step is used. (**multi**)| -|-h, --help |Show this help message| +|-T, --side-target= |Side or auxiliary Docker Compose targets. Multiple values are allowed. (default: [db]) (**multi**)| +|-t, --target= |Docker Compose target. (default: app)| ### Mappings from command line arguments to YML and environment variables |Command argument |YML |Environment variable | |-------------------------------------|----------------------|---------------------------| -|-H, --host= |host |FLOW_HOST or DOCKER_HOST | +|-b, --blue-green |blue_green |FLOW_BLUE_GREEN | | --cert-path= |cert_path |FLOW_CERT_PATH | |-f, --compose-path=docker-compose.yml|compose_path |FLOW_COMPOSE_PATH | -|-b, --blue-green |blue_green |FLOW_BLUE_GREEN | -|-t, --target= |target |FLOW_TARGET | -|-T, --side-target= |side_targets |FLOW_SIDE_TARGETS | -|-S, --pull-side-targets |pull_side_targets |FLOW_PULL_SIDE_TARGETS | -|-p, --project= |project |FLOW_PROJECT | |-c, --consul-address= |consul_address |FLOW_CONSUL_ADDRESS | -|-s, --scale= |scale |SCALE | +| --consul-template-path= |consul_template_path |FLOW_CONSUL_TEMPLATE_PATH | |-F, --flow= |flow |FLOW | -| --proxy-host= |proxy_host |FLOW_PROXY_HOST | -| --proxy-docker-host= |proxy_docker_host|FLOW_PROXY_DOCKER_HOST | +|-H, --host= |host |FLOW_HOST or DOCKER_HOST | +|-p, --project= |project |FLOW_PROJECT | | --proxy-docker-cert-path= |proxy_docker_cert_path|FLOW_PROXY_DOCKER_CERT_PATH| +| --proxy-docker-host= |proxy_docker_host|FLOW_PROXY_DOCKER_HOST | +| --proxy-host= |proxy_host |FLOW_PROXY_HOST | | --proxy-reconf-port= |proxy_reconf_port |FLOW_PROXY_RECONF_PORT | +|-S, --pull-side-targets |pull_side_targets |FLOW_PULL_SIDE_TARGETS | +|-s, --scale= |scale |SCALE | | --service-path= |service_path |FLOW_SERVICE_PATH | +|-T, --side-target= |side_targets |FLOW_SIDE_TARGETS | +|-t, --target= |target |FLOW_TARGET | + Arguments can be strings, boolean, or multiple values. Command line arguments of boolean type do not have any value (i.e. *--blue-green*). Environment variables and YML arguments of boolean type should use *true* as value (i.e. *FLOW_BLUE_GREEN=true* and *blue_green: true*). When allowed, multiple values can be specified by repeating the command line argument (e.g. *--flow=deploy --flow=stop-old*). When specified through environment variables, multiple values should be separated with comma (e.g. *FLOW=deploy,stop-old*). YML accepts multiple values through the standard format. diff --git a/articles/templates.md b/articles/templates.md new file mode 100644 index 0000000..2defd2a --- /dev/null +++ b/articles/templates.md @@ -0,0 +1,5 @@ +```bash +./docker-flow \ + -f docker-compose-demo.yml \ + --flow deploy --flow proxy +``` \ No newline at end of file diff --git a/consul.go b/consul.go index 99c05aa..745ccb0 100644 --- a/consul.go +++ b/consul.go @@ -33,7 +33,6 @@ func (c Consul) GetScaleCalc(address, serviceName, scale string) (int, error) { } } total := s + inc - fmt.Println(string(total)) if total <= 0 { return 1, nil } diff --git a/docker-compose-setup.yml b/docker-compose-setup.yml index 66a38b4..b21ff05 100644 --- a/docker-compose-setup.yml +++ b/docker-compose-setup.yml @@ -10,9 +10,29 @@ services: - 8300:8300 command: -server -bootstrap + consul-server: + container_name: consul + image: consul + network_mode: host + environment: + - 'CONSUL_LOCAL_CONFIG={"skip_leave_on_interrupt": true}' + command: agent -server -bind=$HOST_IP -bootstrap-expect=1 -client=$HOST_IP + registrator: container_name: registrator image: gliderlabs/registrator volumes: - /var/run/docker.sock:/tmp/docker.sock - command: -ip $DOCKER_IP consul://$CONSUL_IP:8500 + command: -ip $HOST_IP consul://$CONSUL_IP:8500 + + proxy: + container_name: docker-flow-proxy + image: vfarcic/docker-flow-proxy + environment: + CONSUL_ADDRESS: $CONSUL_IP:8500 + volumes: + - ./test_configs/:/consul_templates/ + ports: + - 80:80 + - 443:443 + - 8080:8080 diff --git a/docker-compose.yml b/docker-compose.yml index 78b4d29..0608ccc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,16 +1,11 @@ version: '2' services: + app: - image: vfarcic/books-ms${BOOKS_MS_VERSION} + image: vfarcic/go-demo ports: - 8080 - environment: - - SERVICE_NAME=books-ms - - DB_HOST=books-ms-db db: - container_name: books-ms-db image: mongo - environment: - - SERVICE_NAME=books-ms-db diff --git a/docker-flow.yml b/docker-flow.yml index 5f8e932..2fe7b34 100644 --- a/docker-flow.yml +++ b/docker-flow.yml @@ -3,4 +3,4 @@ side_targets: - db blue_green: true service_path: - - /api/v1/books \ No newline at end of file + - /demo/hello \ No newline at end of file diff --git a/flow.go b/flow.go index b8f2c5f..c5dc013 100644 --- a/flow.go +++ b/flow.go @@ -107,6 +107,7 @@ func (m Flow) Proxy(opts Opts, proxy Proxy) error { opts.ServiceName, color, opts.ServicePath, + opts.ConsulTemplatePath, ); err != nil { return err } diff --git a/flow_test.go b/flow_test.go index 66c67f8..56592a9 100644 --- a/flow_test.go +++ b/flow_test.go @@ -507,6 +507,7 @@ func (s FlowTestSuite) Test_Proxy_InvokesReconfigure_WhenDeploy() { s.opts.ServiceName, s.opts.NextColor, s.opts.ServicePath, + "", ) } @@ -524,6 +525,7 @@ func (s FlowTestSuite) Test_Proxy_InvokesReconfigure_WhenScale() { s.opts.ServiceName, s.opts.CurrentColor, s.opts.ServicePath, + "", ) } @@ -537,6 +539,7 @@ func (s FlowTestSuite) Test_Proxy_ReturnsError_WhenReconfigureFails() { mock.Anything, mock.Anything, mock.Anything, + mock.Anything, ).Return(fmt.Errorf("This is an error")) actual := Flow{}.Proxy(s.opts, mockObj) diff --git a/ha_proxy.go b/ha_proxy.go index 87d32a4..a75508e 100644 --- a/ha_proxy.go +++ b/ha_proxy.go @@ -20,6 +20,7 @@ var haProxy Proxy = HaProxy{} type HaProxy struct{} var runHaProxyRunCmd = runCmd +var runHaProxyExecCmd = runCmd var runHaProxyPsCmd = runCmd var runHaProxyStartCmd = runCmd var httpGet = http.Get @@ -53,20 +54,35 @@ func (m HaProxy) Provision(host, reconfPort, certPath, scAddress string) error { return nil } -func (m HaProxy) Reconfigure(host, reconfPort, serviceName, serviceColor string, servicePath []string) error { +func (m HaProxy) Reconfigure(host, reconfPort, serviceName, serviceColor string, servicePath []string, consulTemplatePath string) error { + // TODO: Only if consulTemplatePath is not empty + if err := m.sendConsulTemplate(consulTemplatePath); err != nil { + return err + } if len(host) == 0 { return fmt.Errorf("Proxy host is mandatory for the proxy step. Please set the proxy-host argument.") } if len(serviceName) == 0 { return fmt.Errorf("Service name is mandatory for the proxy step.") } + // TODO: only is consultemplatepath is not set if len(servicePath) == 0 { return fmt.Errorf("Service path is mandatory.") } - if len(reconfPort) == 0 { + if len(reconfPort) == 0 && !strings.Contains(host, ":") { return fmt.Errorf("Reconfigure port is mandatory.") } - address := fmt.Sprintf("%s:%s", host, reconfPort) + if err := m.sendReconfigureRequest(host, reconfPort, serviceName, serviceColor, servicePath); err != nil { + return err + } + return nil +} + +func (m HaProxy) sendReconfigureRequest(host, reconfPort, serviceName, serviceColor string, servicePath []string) error { + address := host + if len(reconfPort) > 0 { + address = fmt.Sprintf("%s:%s", host, reconfPort) + } if !strings.HasPrefix(strings.ToLower(address), "http") { address = fmt.Sprintf("http://%s", address) } @@ -88,8 +104,27 @@ func (m HaProxy) Reconfigure(host, reconfPort, serviceName, serviceColor string, } defer resp.Body.Close() if resp.StatusCode != 200 { - return fmt.Errorf("The response from the proxy was incorrect\n%s\n", err.Error()) + return fmt.Errorf("The response from the proxy was incorrect\n%s\n", resp.StatusCode) + } + return nil +} + +func (m HaProxy) sendConsulTemplate(consulTemplatePath string) error { + data, err := readConsulTemplate(consulTemplatePath) + if err != nil { + return err } + // TODO: Change DOCKER_HOST + command := fmt.Sprintf("echo '%s'", strings.Replace(string(data), `'`, `\'`, -1)) + args := []string{ "exec", "-it", "docker-flow-proxy", command } + cmd := exec.Command("docker", args...) + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = os.Stderr + runHaProxyExecCmd(cmd) +// if err := runHaProxyExecCmd(cmd); err != nil { +// return 0, fmt.Errorf("Docker exec command failed\n%s\n%s\n", strings.Join(cmd, " "), err.Error()) +// } return nil } diff --git a/ha_proxy_test.go b/ha_proxy_test.go index 590418c..7e8c07b 100644 --- a/ha_proxy_test.go +++ b/ha_proxy_test.go @@ -59,6 +59,11 @@ func (s *HaProxyTestSuite) SetupTest() { } logPrintln = func(v ...interface{}) {} sleep = func(d time.Duration) {} + httpGetOrig := httpGet + defer func() { httpGet = httpGetOrig }() + httpGet = func(url string) (resp *http.Response, err error) { + return nil, nil + } } // Provision @@ -223,25 +228,25 @@ func (s HaProxyTestSuite) Test_Provision_ReturnsError_WhenStartFailure() { // Reconfigure func (s HaProxyTestSuite) Test_Reconfigure_ReturnsError_WhenProxyHostIsEmpty() { - err := HaProxy{}.Reconfigure("", s.ReconfPort, s.Project, s.Color, s.ServicePath) + err := HaProxy{}.Reconfigure("", s.ReconfPort, s.Project, s.Color, s.ServicePath, "") s.Error(err) } func (s HaProxyTestSuite) Test_Reconfigure_ReturnsError_WhenProjectIsEmpty() { - err := HaProxy{}.Reconfigure(s.ProxyHost, s.ReconfPort, "", s.Color, s.ServicePath) + err := HaProxy{}.Reconfigure(s.ProxyHost, s.ReconfPort, "", s.Color, s.ServicePath, "") s.Error(err) } func (s HaProxyTestSuite) Test_Reconfigure_ReturnsError_WhenServicePathIsEmpty() { - err := HaProxy{}.Reconfigure(s.ProxyHost, s.ReconfPort, s.Project, s.Color, []string{""}) + err := HaProxy{}.Reconfigure(s.ProxyHost, s.ReconfPort, s.Project, s.Color, []string{""}, "") s.Error(err) } func (s HaProxyTestSuite) Test_Reconfigure_ReturnsError_WhenReconfPortIsEmpty() { - err := HaProxy{}.Reconfigure(s.ProxyHost, "", s.Project, s.Color, s.ServicePath) + err := HaProxy{}.Reconfigure(s.ProxyHost, "", s.Project, s.Color, s.ServicePath, "") s.Error(err) } @@ -263,7 +268,7 @@ func (s HaProxyTestSuite) Test_Reconfigure_SendsHttpRequest() { return nil, fmt.Errorf("This is an error") } - HaProxy{}.Reconfigure(s.ProxyHost, s.ReconfPort, s.Project, s.Color, s.ServicePath) + HaProxy{}.Reconfigure(s.ProxyHost, s.ReconfPort, s.Project, s.Color, s.ServicePath, "") s.Equal(expected, actual) } @@ -284,7 +289,7 @@ func (s HaProxyTestSuite) Test_Reconfigure_SendsHttpRequest_WithOutColor_WhenNot return nil, fmt.Errorf("This is an error") } - HaProxy{}.Reconfigure(s.ProxyHost, s.ReconfPort, s.Project, "", s.ServicePath) + HaProxy{}.Reconfigure(s.ProxyHost, s.ReconfPort, s.Project, "", s.ServicePath, "") s.Equal(expected, actual) } @@ -305,7 +310,7 @@ func (s HaProxyTestSuite) Test_Reconfigure_SendsHttpRequestWithPrependedHttp() { return nil, fmt.Errorf("This is an error") } - HaProxy{}.Reconfigure("my-docker-proxy-host.com", s.ReconfPort, s.Project, "", s.ServicePath) + HaProxy{}.Reconfigure("my-docker-proxy-host.com", s.ReconfPort, s.Project, "", s.ServicePath, "") s.Equal(expected, actual) } @@ -317,21 +322,87 @@ func (s HaProxyTestSuite) Test_Reconfigure_ReturnsError_WhenRequestFails() { return nil, fmt.Errorf("This is an error") } - err := HaProxy{}.Reconfigure(s.ProxyHost, s.ReconfPort, s.Project, s.Color, s.ServicePath) + err := HaProxy{}.Reconfigure(s.ProxyHost, s.ReconfPort, s.Project, s.Color, s.ServicePath, "") s.Error(err) } func (s HaProxyTestSuite) Test_Reconfigure_ReturnsError_WhenResponseCodeIsNot2xx() { - s.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) })) - err := HaProxy{}.Reconfigure(s.Server.URL, s.ReconfPort, s.Project, s.Color, s.ServicePath) + err := HaProxy{}.Reconfigure(server.URL, "", s.Project, s.Color, s.ServicePath, "") + + s.Error(err) +} + +func (s HaProxyTestSuite) Test_Reconfigure_ReadsConsulTemplatePath() { + consulTemplatePath := "/path/to/consul/template" + actual := "" + readConsulTemplate = func(fileName string) ([]byte, error) { + actual = fileName + return []byte(""), nil + } + + HaProxy{}.Reconfigure(s.Server.URL, s.ReconfPort, s.Project, s.Color, s.ServicePath, consulTemplatePath) + + s.Equal(consulTemplatePath, actual) +} + +func (s HaProxyTestSuite) Test_Reconfigure_ReturnsError_WhenReadingConsulTemplatePathFails() { + consulTemplatePath := "/path/to/consul/template" + readConsulTemplateOrig := readConsulTemplate + defer func() { readConsulTemplate = readConsulTemplateOrig }() + readConsulTemplate = func(fileName string) ([]byte, error) { + return []byte(""), fmt.Errorf("This is an error") + } + + err := HaProxy{}.Reconfigure(s.Server.URL, "", s.Project, s.Color, s.ServicePath, consulTemplatePath) s.Error(err) } +func (s HaProxyTestSuite) Test_Reconfigure_TransfersConsulTemplateToTheProxy() { + consulTemplate := `This is a 'consul' template` + consulTemplatePath := "/path/to/consul/template" + var actual []string + command := fmt.Sprintf("echo '%s'", `This is a \'consul\' template`) + expected := []string{"docker", "exec", "-it", "docker-flow-proxy", command} + runHaProxyExecCmd = func(cmd *exec.Cmd) error { + actual = cmd.Args + return nil + } + readConsulTemplate = func(fileName string) ([]byte, error) { + return []byte(consulTemplate), nil + } + + HaProxy{}.Reconfigure(s.Server.URL, s.ReconfPort, s.Project, s.Color, s.ServicePath, consulTemplatePath) + + s.Equal(expected, actual) +} + +//func (s HaProxyTestSuite) Test_Reconfigure_SendsHttpRequestWithConsulTemplatePath_WhenSpecified() { +// actual := "" +// expected := fmt.Sprintf( +// "%s:%s/v1/docker-flow-proxy/reconfigure?serviceName=%s&consulTemplatePath=%s", +// s.ProxyHost, +// s.ReconfPort, +// s.Project, +// s.ConsulTemplatePath, +// ) +// httpGetOrig := httpGet +// defer func() { httpGet = httpGetOrig }() +// httpGet = func(url string) (resp *http.Response, err error) { +// actual = url +// return nil, fmt.Errorf("This is an error") +// } +// +// HaProxy{}.Reconfigure(s.ProxyHost, s.ReconfPort, s.Project, s.Color, s.ServicePath) +// +// s.Equal(expected, actual) +//} + // Suite func TestHaProxyTestSuite(t *testing.T) { diff --git a/integration_test.go b/integration_test.go index c4cea86..763f913 100644 --- a/integration_test.go +++ b/integration_test.go @@ -9,9 +9,8 @@ package main // With Docker Machine // $ docker-machine create -d virtualbox docker-flow-test // $ eval "$(docker-machine env docker-flow-test)" -// $ go build && go test --cover --tags integration +// $ go build && go test --cover --tags integration | tee tests.log // $ docker-machine rm -f docker-flow-test -// TODO: Change books-ms for a "lighter" service import ( "fmt" @@ -29,25 +28,16 @@ import ( type IntegrationTestSuite struct { suite.Suite ConsulIp string + ProxyIp string ProxyHost string ProxyDockerHost string ProxyDockerCertPath string ServicePath string + ServiceName string } func (s *IntegrationTestSuite) SetupTest() { - _, ids := s.runCmdWithoutStdOut(true, "docker", "ps", "-a", "--filter", "name=booksms", "--format", "{{.ID}}") - for _, id := range strings.Split(ids, "\n") { - s.runCmdWithStdOut(false, "docker", "rm", "-f", string(id)) - } - s.runCmdWithStdOut(false, "docker", "rm", "-f", "consul", "docker-flow-proxy", "registrator") - s.runCmdWithStdOut( - true, - "docker", "run", "-d", "--name", "consul", - "-p", "8500:8500", - "-h", "consul", - "progrium/consul", "-server", "-bootstrap", - ) + s.removeAll() time.Sleep(time.Second) } @@ -71,16 +61,16 @@ func (s IntegrationTestSuite) Test_BlueGreenDeployment() { "--blue-green", ) s.verifyContainer([]ContainerStatus{ - {"booksms_app-blue_1", "Up" }, - {"books-ms-db", "Up" }, + {"godemo_app-blue_1", "Up" }, + {"godemo_db", "Up" }, }) log.Println("Second deployment (green)") os.Setenv("FLOW_CONSUL_ADDRESS", fmt.Sprintf("http://%s:8500", s.ConsulIp)) s.runCmdWithStdOut(true, "./docker-flow", "--flow", "deploy") s.verifyContainer([]ContainerStatus{ - {"booksms_app-blue_1", "Up" }, - {"booksms_app-green_1", "Up" }, + {"godemo_app-blue_1", "Up" }, + {"godemo_app-green_1", "Up" }, }) log.Println("Third deployment (blue) with stop old release (green)") @@ -89,8 +79,8 @@ func (s IntegrationTestSuite) Test_BlueGreenDeployment() { "./docker-flow", "--flow", "deploy", "--flow", "stop-old") s.verifyContainer([]ContainerStatus{ - {"booksms_app-blue_1", "Up" }, - {"booksms_app-green_1", "Exited" }, + {"godemo_app-blue_1", "Up" }, + {"godemo_app-green_1", "Exited" }, }) } @@ -106,9 +96,9 @@ func (s IntegrationTestSuite) Test_Scaling() { "--scale", "2", ) s.verifyContainer([]ContainerStatus{ - {"booksms_app-blue_1", "Up" }, - {"booksms_app-blue_2", "Up" }, - {"books-ms-db", "Up" }, + {"godemo_app-blue_1", "Up" }, + {"godemo_app-blue_2", "Up" }, + {"godemo_db", "Up" }, }) log.Println("Second deployment (green, 4 (+2) instances)") @@ -120,10 +110,10 @@ func (s IntegrationTestSuite) Test_Scaling() { "--scale", "+2", ) s.verifyContainer([]ContainerStatus{ - {"booksms_app-green_1", "Up" }, - {"booksms_app-green_2", "Up" }, - {"booksms_app-green_3", "Up" }, - {"booksms_app-green_4", "Up" }, + {"godemo_app-green_1", "Up" }, + {"godemo_app-green_2", "Up" }, + {"godemo_app-green_3", "Up" }, + {"godemo_app-green_4", "Up" }, }) log.Println("Scaling (green, 3 (-1) instances)") @@ -135,10 +125,10 @@ func (s IntegrationTestSuite) Test_Scaling() { "--scale", "\"-1\"", ) s.verifyContainer([]ContainerStatus{ - {"booksms_app-green_1", "Up" }, - {"booksms_app-green_2", "Up" }, - {"booksms_app-green_3", "Up" }, - {"booksms_app-green_4", "N/A" }, + {"godemo_app-green_1", "Up" }, + {"godemo_app-green_2", "Up" }, + {"godemo_app-green_3", "Up" }, + {"godemo_app-green_4", "N/A" }, }) } @@ -167,9 +157,10 @@ func (s IntegrationTestSuite) Test_Proxy() { s.verifyContainer([]ContainerStatus{ {"docker-flow-proxy", "Up" }, }) - resp, err := http.Get(fmt.Sprintf("http://%s%s", s.ConsulIp, s.ServicePath)) + url := fmt.Sprintf("http://%s%s", s.ConsulIp, s.ServicePath) + resp, err := http.Get(url) s.NoError(err) - s.Equal(200, resp.StatusCode) + s.Equal(200, resp.StatusCode, "Failed to send the request %s", url) log.Println("Runs proxy when stopped and reconfigures it when scale") s.runCmdWithStdOut(false, "docker", "stop", "docker-flow-proxy") @@ -193,7 +184,7 @@ func (s IntegrationTestSuite) Test_Proxy() { resp, err = http.Get(fmt.Sprintf("http://%s%s", s.ConsulIp, s.ServicePath)) s.NoError(err) s.Equal(200, resp.StatusCode) - s.runCmdWithStdOut(true, "docker", "rm", "-f", "booksms_app-blue_1") + s.runCmdWithStdOut(true, "docker", "rm", "-f", "godemo_app-blue_1") resp, err = http.Get(fmt.Sprintf("http://%s%s", s.ConsulIp, s.ServicePath)) s.NoError(err) s.Equal(200, resp.StatusCode) @@ -220,6 +211,35 @@ func (s IntegrationTestSuite) Test_Proxy() { s.Equal(200, resp.StatusCode) } +func (s IntegrationTestSuite) Test_Proxy_Templates() { + log.Println(">> Integration tests: proxy with templates") + + s.runCmdWithStdOut( + true, + "docker", "run", "-d", "--name", "registrator", + "-v", "/var/run/docker.sock:/tmp/docker.sock", + "gliderlabs/registrator", + "-ip", s.ConsulIp, fmt.Sprintf("consul://%s:8500", s.ConsulIp), + ) + s.runCmdWithStdOut( + true, + "./docker-flow", + "--consul-address", fmt.Sprintf("http://%s:8500", s.ConsulIp), + "--proxy-host", s.ProxyHost, + "--proxy-docker-host", s.ProxyDockerHost, + "--proxy-docker-cert-path", s.ProxyDockerCertPath, + "--service-path", "INCORRECT", + "--flow", "deploy", "--flow", "proxy", + ) + s.verifyContainer([]ContainerStatus{ + {"docker-flow-proxy", "Up" }, + }) + url := fmt.Sprintf("http://%s%s", s.ConsulIp, s.ServicePath) + resp, err := http.Get(url) + s.NoError(err) + s.Equal(200, resp.StatusCode, "Failed to send the request %s", url) +} + // Util type ContainerStatus struct { Name string @@ -279,6 +299,19 @@ func (s IntegrationTestSuite) runCmdWithStdOut(failOnError bool, command string, return s.runCmd(failOnError, true, command, args...) } +func (s *IntegrationTestSuite) removeAll() { + _, ids := s.runCmdWithoutStdOut(true, "docker", "ps", "-a", "--filter", "name=dockerflow", "--format", "{{.ID}}") + for _, id := range strings.Split(ids, "\n") { + s.runCmdWithStdOut(false, "docker", "rm", "-f", string(id)) + } + s.runCmdWithStdOut(false, "docker", "rm", "-f", "consul", "docker-flow-proxy", "registrator") + s.runCmdWithStdOut(false, "docker-compose", "-f", "docker-compose-setup.yml", "-p", "tests-setup", "down") + s.runCmdWithStdOut( + true, + "docker-compose", "-f", "docker-compose-setup.yml", "-p", "tests-setup", "up", "-d", "consul", + ) +} + // Suite func TestIntegrationTestSuite(t *testing.T) { @@ -289,11 +322,13 @@ func TestIntegrationTestSuite(t *testing.T) { ip = strings.Trim(msg, "\n") } s.ConsulIp = ip + s.ProxyIp = ip s.ProxyHost = ip s.ProxyDockerHost = os.Getenv("DOCKER_HOST") s.ProxyDockerCertPath = os.Getenv("DOCKER_CERT_PATH") - s.ServicePath = "/api/v1/books" + s.ServicePath = "/demo/hello" + s.ServiceName = "go-demo" os.Setenv("FLOW_CONSUL_IP", s.ConsulIp) - os.Setenv("FLOW_PROJECT", "booksms") + os.Setenv("FLOW_PROJECT", s.ServiceName) suite.Run(t, s) -} \ No newline at end of file +} diff --git a/main.go b/main.go index 303658f..eb5c4f6 100644 --- a/main.go +++ b/main.go @@ -49,7 +49,6 @@ func main() { case FLOW_SCALE: if !deployed { logPrintln(fmt.Sprintf("Scaling (%s)...", opts.CurrentTarget)) - fmt.Println(opts.Flow) if err := flow.Scale(opts, dc, opts.CurrentTarget, true); err != nil { logFatal(err) } diff --git a/opts.go b/opts.go index c22a172..034a588 100644 --- a/opts.go +++ b/opts.go @@ -20,27 +20,30 @@ var parseArgs = ParseArgs var processOpts = ProcessOpts type Opts struct { - Host string `short:"H" long:"host" description:"Docker daemon socket to connect to. If not specified, DOCKER_HOST environment variable will be used instead."` + BlueGreen bool `short:"b" long:"blue-green" description:"Perform blue-green deployment." yaml:"blue_green" envconfig:"blue_green"` CertPath string `long:"cert-path" description:"Docker certification path. If not specified, DOCKER_CERT_PATH environment variable will be used instead." yaml:"cert_path" envconfig:"cert_path"` ComposePath string `short:"f" long:"compose-path" value-name:"docker-compose.yml" description:"Path to the Docker Compose configuration file." yaml:"compose_path" envconfig:"compose_path"` - BlueGreen bool `short:"b" long:"blue-green" description:"Perform blue-green deployment." yaml:"blue_green" envconfig:"blue_green"` - Target string `short:"t" long:"target" description:"Docker Compose target."` - SideTargets []string `short:"T" long:"side-target" description:"Side or auxiliary Docker Compose targets. Multiple values are allowed." yaml:"side_targets"` - PullSideTargets bool `short:"S" long:"pull-side-targets" description:"Pull side or auxiliary targets." yaml:"pull_side_targets" envconfig:"pull_side_targets"` - Project string `short:"p" long:"project" description:"Docker Compose project. If not specified, the current directory will be used instead."` ServiceDiscoveryAddress string `short:"c" long:"consul-address" description:"The address of the Consul server." yaml:"consul_address" envconfig:"consul_address"` - Scale string `short:"s" long:"scale" description:"Number of instances to deploy. If the value starts with the plus sign (+), the number of instances will be increased by the given number. If the value begins with the minus sign (-), the number of instances will be decreased by the given number." yaml:"scale" envconfig:"scale"` + ConsulTemplatePath string `long:"consul-template-path" description:"The path to the Consul Template. If specified, proxy template will be loaded from the specified file." yaml:"consul_template_path" envconfig:"consul_template_path"` Flow []string `short:"F" long:"flow" description:"The actions that should be performed as the flow. Multiple values are allowed.\ndeploy: Deploys a new release\nscale: Scales currently running release\nstop-old: Stops the old release\nproxy: Reconfigures the proxy\n" yaml:"flow" envconfig:"flow"` - ProxyHost string `long:"proxy-host" description:"The host of the proxy. Visitors should request services from this domain. Docker Flow uses it to request reconfiguration when a new service is deployed or an existing one is scaled. This argument is required only if the proxy flow step is used." yaml:"proxy_host" envconfig:"proxy_host"` - ProxyDockerHost string `long:"proxy-docker-host" description:"Docker daemon socket of the proxy host. This argument is required only if the proxy flow step is used." yaml:"proxy_docker_host" envconfig:"proxy_docker_host"` + Host string `short:"H" long:"host" description:"Docker daemon socket to connect to. If not specified, DOCKER_HOST environment variable will be used instead."` + Project string `short:"p" long:"project" description:"Docker Compose project. If not specified, the current directory will be used instead."` ProxyDockerCertPath string `long:"proxy-docker-cert-path" description:"Docker certification path for the proxy host." yaml:"proxy_docker_cert_path" envconfig:"proxy_docker_cert_path"` + ProxyDockerHost string `long:"proxy-docker-host" description:"Docker daemon socket of the proxy host. This argument is required only if the proxy flow step is used." yaml:"proxy_docker_host" envconfig:"proxy_docker_host"` + ProxyHost string `long:"proxy-host" description:"The host of the proxy. Visitors should request services from this domain. Docker Flow uses it to request reconfiguration when a new service is deployed or an existing one is scaled. This argument is required only if the proxy flow step is used." yaml:"proxy_host" envconfig:"proxy_host"` ProxyReconfPort string `long:"proxy-reconf-port" description:"The port used by the proxy to reconfigure its configuration" yaml:"proxy_reconf_port" envconfig:"proxy_reconf_port"` + PullSideTargets bool `short:"S" long:"pull-side-targets" description:"Pull side or auxiliary targets." yaml:"pull_side_targets" envconfig:"pull_side_targets"` + Scale string `short:"s" long:"scale" description:"Number of instances to deploy. If the value starts with the plus sign (+), the number of instances will be increased by the given number. If the value begins with the minus sign (-), the number of instances will be decreased by the given number." yaml:"scale" envconfig:"scale"` ServicePath []string `long:"service-path" description:"Path that should be configured in the proxy (e.g. /api/v1/my-service). This argument is required only if the proxy flow step is used." yaml:"service_path"` + SideTargets []string `short:"T" long:"side-target" description:"Side or auxiliary Docker Compose targets. Multiple values are allowed." yaml:"side_targets"` + Target string `short:"t" long:"target" description:"Docker Compose target."` + ServiceName string CurrentColor string NextColor string CurrentTarget string NextTarget string + ConsulTemplate string } var GetOpts = func() (Opts, error) { @@ -114,11 +117,18 @@ func ProcessOpts(opts *Opts) (err error) { if len(opts.ServiceDiscoveryAddress) == 0 { return fmt.Errorf("consul-address argument is required") } - if len(opts.Scale) != 0 { + if len(opts.Scale) > 0 { if _, err := strconv.Atoi(opts.Scale); err != nil { return fmt.Errorf("scale must be a number or empty") } } + if (len(opts.ConsulTemplatePath) > 0) { + data, err := readFile(opts.ConsulTemplatePath) + if err != nil { + return fmt.Errorf("Consul Template %s could not be loaded", opts.ConsulTemplatePath) + } + opts.ConsulTemplate = string(data) + } if len(opts.Flow) == 0 { opts.Flow = []string{"deploy"} } diff --git a/opts_test.go b/opts_test.go index 17a5bd2..bd22ff0 100644 --- a/opts_test.go +++ b/opts_test.go @@ -221,6 +221,29 @@ func (s OptsTestSuite) Test_ProcessOpts_SetsFlowToDeploy_WhenEmpty() { s.Equal(expected, s.opts.Flow) } +func (s OptsTestSuite) Test_ProcessOpts_ReturnsError_WhenConsulTemplateFileDoesNotExist() { + s.opts.ConsulTemplatePath = "/this/path/does/not/exist" + readFile = func(fileName string) ([]byte, error) { + return []byte(""), fmt.Errorf("This is an error") + } + + actual := ProcessOpts(&s.opts) + + s.Error(actual) +} + +func (s OptsTestSuite) Test_ProcessOpts_SetsConsulTemplate_WhenConsulTemplateFileIsSpecified() { + expected := "This is content of a Consul Template" + s.opts.ConsulTemplatePath = "/this/path/does/not/exist" + readFile = func(fileName string) ([]byte, error) { + return []byte(expected), nil + } + + ProcessOpts(&s.opts) + + s.Equal(expected, s.opts.ConsulTemplate) +} + // ParseEnvVars func (s OptsTestSuite) Test_ParseEnvVars_Strings() { @@ -240,6 +263,7 @@ func (s OptsTestSuite) Test_ParseEnvVars_Strings() { {"myProxyDockerHost", "FLOW_PROXY_DOCKER_HOST", &s.opts.ProxyDockerHost}, {"myProxyCertPath", "FLOW_PROXY_DOCKER_CERT_PATH", &s.opts.ProxyDockerCertPath}, {"4357", "FLOW_PROXY_RECONF_PORT", &s.opts.ProxyReconfPort}, + {"myConsulTemplatePath", "FLOW_CONSUL_TEMPLATE_PATH", &s.opts.ConsulTemplatePath}, } for _, d := range data { os.Setenv(d.key, d.expected) @@ -331,6 +355,7 @@ func (s OptsTestSuite) Test_ParseArgs_LongStrings() { {"proxyHostFromArgs", "proxy-docker-host", &s.opts.ProxyDockerHost}, {"proxyCertPathFromArgs", "proxy-docker-cert-path", &s.opts.ProxyDockerCertPath}, {"1234", "proxy-reconf-port", &s.opts.ProxyReconfPort}, + {"consulTemplatePathFromArgs", "consul-template-path", &s.opts.ConsulTemplatePath}, } for _, d := range data { @@ -511,6 +536,7 @@ func (s OptsTestSuite) Test_ParseYml_SetsOpts() { proxyDockerHost := "proxyDomainFromYml" proxyDockerCertPath := "proxyCertPathFromYml" proxyReconfPort := "1245" + consulTemplatePath := "/path/to/consul/template" yml := fmt.Sprintf(` host: %s cert_path: %s @@ -535,11 +561,11 @@ flow: service_path: - %s - %s -`, +consul_template_path: %s`, host, certPath, composePath, target, sideTarget1, sideTarget2, project, consulAddress, scale, proxyHost, proxyDockerHost, proxyDockerCertPath, proxyReconfPort, flow1, flow2, path1, - path2, + path2, consulTemplatePath, ) readFile = func(fileName string) ([]byte, error) { return []byte(yml), nil @@ -562,6 +588,7 @@ service_path: s.Equal(proxyReconfPort, s.opts.ProxyReconfPort) s.Equal([]string{flow1, flow2}, s.opts.Flow) s.Equal([]string{path1, path2}, s.opts.ServicePath) + s.Equal(consulTemplatePath, s.opts.ConsulTemplatePath) } // GetOpts diff --git a/proxy.go b/proxy.go index 2b89920..8052eef 100644 --- a/proxy.go +++ b/proxy.go @@ -2,5 +2,5 @@ package main type Proxy interface { Provision(host, reconfPort, certPath, scAddress string) error - Reconfigure(host, reconfPort, serviceName, serviceColor string, servicePath []string) error + Reconfigure(host, reconfPort, serviceName, serviceColor string, servicePath []string, consulTemplatePath string) error } diff --git a/proxy_test.go b/proxy_test.go index e6f085c..e284acb 100644 --- a/proxy_test.go +++ b/proxy_test.go @@ -15,8 +15,8 @@ func (m *ProxyMock) Provision(host, reconfPort, certPath, scAddress string) erro return args.Error(0) } -func (m *ProxyMock) Reconfigure(host, reconfPort, serviceName, serviceColor string, servicePath []string) error { - args := m.Called(host, reconfPort, serviceName, serviceColor, servicePath) +func (m *ProxyMock) Reconfigure(host, reconfPort, serviceName, serviceColor string, servicePath []string, consulTemplatePath string) error { + args := m.Called(host, reconfPort, serviceName, serviceColor, servicePath, consulTemplatePath) return args.Error(0) } @@ -26,7 +26,7 @@ func getProxyMock(skipMethod string) *ProxyMock { mockObj.On("Provision", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) } if skipMethod != "Reconfigure" { - mockObj.On("Reconfigure", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) + mockObj.On("Reconfigure", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) } return mockObj } diff --git a/setup.sh b/setup.sh index ba4632d..d59f822 100755 --- a/setup.sh +++ b/setup.sh @@ -34,7 +34,7 @@ docker-machine create -d virtualbox \ eval "$(docker-machine env swarm-master)" -export DOCKER_IP=$(docker-machine ip swarm-master) +export HOST_IP=$(docker-machine ip swarm-master) docker-compose \ -p setup \ @@ -43,7 +43,7 @@ docker-compose \ eval "$(docker-machine env swarm-node-1)" -export DOCKER_IP=$(docker-machine ip swarm-node-1) +export HOST_IP=$(docker-machine ip swarm-node-1) docker-compose \ -p setup \ @@ -52,7 +52,7 @@ docker-compose \ eval "$(docker-machine env swarm-node-2)" -export DOCKER_IP=$(docker-machine ip swarm-node-2) +export HOST_IP=$(docker-machine ip swarm-node-2) docker-compose \ -p setup \ diff --git a/util.go b/util.go index 15db266..047906c 100644 --- a/util.go +++ b/util.go @@ -8,6 +8,7 @@ import ( ) var readFile = ioutil.ReadFile +var readConsulTemplate = ioutil.ReadFile var writeFile = ioutil.WriteFile var removeFile = os.Remove var execCmd = exec.Command From a20362f5ffcb7fdf900ebb34e0ad3d8c050b2014 Mon Sep 17 00:00:00 2001 From: vfarcic Date: Sun, 22 May 2016 00:37:03 +0200 Subject: [PATCH 2/5] Refactoring #9 --- flow.go | 2 + flow_test.go | 6 + ha_proxy.go | 116 ++++++++---- ha_proxy_test.go | 274 ++++++++++++++++++++--------- integration_test.go | 77 ++++---- opts.go | 14 +- proxy.go | 4 +- proxy_test.go | 6 +- test_configs/tmpl/go-demo-app.tmpl | 11 ++ util.go | 1 - 10 files changed, 340 insertions(+), 171 deletions(-) create mode 100644 test_configs/tmpl/go-demo-app.tmpl diff --git a/flow.go b/flow.go index c5dc013..0e3f018 100644 --- a/flow.go +++ b/flow.go @@ -102,6 +102,8 @@ func (m Flow) Proxy(opts Opts, proxy Proxy) error { color = opts.NextColor } if err := proxy.Reconfigure( + opts.ProxyDockerHost, + opts.ProxyDockerCertPath, opts.ProxyHost, opts.ProxyReconfPort, opts.ServiceName, diff --git a/flow_test.go b/flow_test.go index 56592a9..1530b90 100644 --- a/flow_test.go +++ b/flow_test.go @@ -502,6 +502,8 @@ func (s FlowTestSuite) Test_Proxy_InvokesReconfigure_WhenDeploy() { mockObj.AssertCalled( s.T(), "Reconfigure", + s.opts.ProxyDockerHost, + s.opts.ProxyDockerCertPath, s.opts.ProxyHost, s.opts.ProxyReconfPort, s.opts.ServiceName, @@ -520,6 +522,8 @@ func (s FlowTestSuite) Test_Proxy_InvokesReconfigure_WhenScale() { mockObj.AssertCalled( s.T(), "Reconfigure", + s.opts.ProxyDockerHost, + s.opts.ProxyDockerCertPath, s.opts.ProxyHost, s.opts.ProxyReconfPort, s.opts.ServiceName, @@ -540,6 +544,8 @@ func (s FlowTestSuite) Test_Proxy_ReturnsError_WhenReconfigureFails() { mock.Anything, mock.Anything, mock.Anything, + mock.Anything, + mock.Anything, ).Return(fmt.Errorf("This is an error")) actual := Flow{}.Proxy(s.opts, mockObj) diff --git a/ha_proxy.go b/ha_proxy.go index a75508e..70afd1b 100644 --- a/ha_proxy.go +++ b/ha_proxy.go @@ -14,6 +14,7 @@ const containerStatusRunning = 1 const containerStatusExited = 2 const containerStatusRemoved = 3 const ProxyReconfigureDefaultPort = 8080 +const ConsulTemplatesDir = "/consul_templates" var haProxy Proxy = HaProxy{} @@ -21,18 +22,19 @@ type HaProxy struct{} var runHaProxyRunCmd = runCmd var runHaProxyExecCmd = runCmd +var runHaProxyCpCmd = runCmd var runHaProxyPsCmd = runCmd var runHaProxyStartCmd = runCmd var httpGet = http.Get -func (m HaProxy) Provision(host, reconfPort, certPath, scAddress string) error { - if len(host) == 0 { +func (m HaProxy) Provision(dockerHost, reconfPort, certPath, scAddress string) error { + if len(dockerHost) == 0 { return fmt.Errorf("Proxy docker host is mandatory for the proxy step. Please set the proxy-docker-host argument.") } if len(scAddress) == 0 { return fmt.Errorf("Service Discovery Address is mandatory.") } - SetDockerHost(host, certPath) + SetDockerHost(dockerHost, certPath) status, err := m.ps() if err != nil { return err @@ -54,10 +56,18 @@ func (m HaProxy) Provision(host, reconfPort, certPath, scAddress string) error { return nil } -func (m HaProxy) Reconfigure(host, reconfPort, serviceName, serviceColor string, servicePath []string, consulTemplatePath string) error { - // TODO: Only if consulTemplatePath is not empty - if err := m.sendConsulTemplate(consulTemplatePath); err != nil { - return err +// TODO: Change args to struct +func (m HaProxy) Reconfigure( + dockerHost, dockerCertPath, host, reconfPort, serviceName, serviceColor string, + servicePath []string, + consulTemplatePath string, +) error { + if len(consulTemplatePath) > 0 { + if err := m.sendConsulTemplateToTheProxy(dockerHost, dockerCertPath, consulTemplatePath, serviceName, serviceColor); err != nil { + return err + } + } else if len(servicePath) == 0 { + return fmt.Errorf("It is mandatory to specify servicePath or consulTemplatePath. Please set one of the two.") } if len(host) == 0 { return fmt.Errorf("Proxy host is mandatory for the proxy step. Please set the proxy-host argument.") @@ -65,20 +75,20 @@ func (m HaProxy) Reconfigure(host, reconfPort, serviceName, serviceColor string, if len(serviceName) == 0 { return fmt.Errorf("Service name is mandatory for the proxy step.") } - // TODO: only is consultemplatepath is not set - if len(servicePath) == 0 { - return fmt.Errorf("Service path is mandatory.") - } if len(reconfPort) == 0 && !strings.Contains(host, ":") { return fmt.Errorf("Reconfigure port is mandatory.") } - if err := m.sendReconfigureRequest(host, reconfPort, serviceName, serviceColor, servicePath); err != nil { + if err := m.sendReconfigureRequest(host, reconfPort, serviceName, serviceColor, servicePath, consulTemplatePath); err != nil { return err } return nil } -func (m HaProxy) sendReconfigureRequest(host, reconfPort, serviceName, serviceColor string, servicePath []string) error { +func (m HaProxy) sendReconfigureRequest( + host, reconfPort, serviceName, serviceColor string, + servicePath []string, + consulTemplatePath string, +) error { address := host if len(reconfPort) > 0 { address = fmt.Sprintf("%s:%s", host, reconfPort) @@ -86,17 +96,19 @@ func (m HaProxy) sendReconfigureRequest(host, reconfPort, serviceName, serviceCo if !strings.HasPrefix(strings.ToLower(address), "http") { address = fmt.Sprintf("http://%s", address) } - colorQuery := "" - if len(serviceColor) > 0 { - colorQuery = fmt.Sprintf("&serviceColor=%s", serviceColor) - } proxyUrl := fmt.Sprintf( - "%s/v1/docker-flow-proxy/reconfigure?serviceName=%s%s&servicePath=%s", + "%s/v1/docker-flow-proxy/reconfigure?serviceName=%s", address, serviceName, - colorQuery, - strings.Join(servicePath, ","), ) + if len(consulTemplatePath) > 0 { + proxyUrl = fmt.Sprintf("%s&consulTemplatePath=%s/%s.tmpl", proxyUrl, ConsulTemplatesDir, serviceName) + } else { + if len(serviceColor) > 0 { + proxyUrl = fmt.Sprintf("%s&serviceColor=%s", proxyUrl, serviceColor) + } + proxyUrl = fmt.Sprintf("%s&servicePath=%s", proxyUrl, strings.Join(servicePath, ",")) + } logPrintf("Sending request to %s to reconfigure the proxy", proxyUrl) resp, err := httpGet(proxyUrl) if err != nil { @@ -104,27 +116,61 @@ func (m HaProxy) sendReconfigureRequest(host, reconfPort, serviceName, serviceCo } defer resp.Body.Close() if resp.StatusCode != 200 { - return fmt.Errorf("The response from the proxy was incorrect\n%s\n", resp.StatusCode) + return fmt.Errorf("The request to the proxy (%s) failed with status code %d\n", proxyUrl, resp.StatusCode) } return nil } -func (m HaProxy) sendConsulTemplate(consulTemplatePath string) error { - data, err := readConsulTemplate(consulTemplatePath) - if err != nil { +func (m HaProxy) sendConsulTemplateToTheProxy(dockerHost, dockerCertPath, consulTemplatePath, serviceName, color string) error { + if err := m.createTempConsulTemplate(consulTemplatePath, serviceName, color); err != nil { return err } - // TODO: Change DOCKER_HOST - command := fmt.Sprintf("echo '%s'", strings.Replace(string(data), `'`, `\'`, -1)) - args := []string{ "exec", "-it", "docker-flow-proxy", command } - cmd := exec.Command("docker", args...) - var out bytes.Buffer - cmd.Stdout = &out - cmd.Stderr = os.Stderr - runHaProxyExecCmd(cmd) -// if err := runHaProxyExecCmd(cmd); err != nil { -// return 0, fmt.Errorf("Docker exec command failed\n%s\n%s\n", strings.Join(cmd, " "), err.Error()) -// } + if err := m.copyConsulTemplateToTheProxy(dockerHost, dockerCertPath, consulTemplatePath, serviceName); err != nil { + return err + } + removeFile(fmt.Sprintf("%s.tmp", consulTemplatePath)) + + return nil +} + +func (m HaProxy) copyConsulTemplateToTheProxy(dockerHost, dockerCertPath, consulTemplatePath, serviceName string) error { + SetDockerHost(dockerHost, dockerCertPath) + args := []string{"exec", "-i", "docker-flow-proxy", "mkdir", "-p", ConsulTemplatesDir} + execCmd := exec.Command("docker", args...) + execCmd.Stdout = os.Stdout + execCmd.Stderr = os.Stderr + // TODO: Remove. Deprecated since Docker Flow: Proxy has that directory by default. + if err := runHaProxyExecCmd(execCmd); err != nil { + return err + } + args = []string{ + "cp", + fmt.Sprintf("%s.tmp", consulTemplatePath), + fmt.Sprintf("docker-flow-proxy:%s/%s.tmpl", ConsulTemplatesDir, serviceName), + } + cpCmd := exec.Command("docker", args...) + cpCmd.Stdout = os.Stdout + cpCmd.Stderr = os.Stderr + if err := runHaProxyCpCmd(cpCmd); err != nil { + return err + } + return nil +} + +func (m HaProxy) createTempConsulTemplate(consulTemplatePath, serviceName, color string) error { + fullServiceName := fmt.Sprintf("%s-%s", serviceName, color) + tmpPath := fmt.Sprintf("%s.tmp", consulTemplatePath) + data, err := readFile(consulTemplatePath) + if err != nil { + return fmt.Errorf("Could not read the Consul template %s\n%s", consulTemplatePath, err.Error()) + } + if err := writeFile( + tmpPath, + []byte(strings.Replace(string(data), "SERVICE_NAME", fullServiceName, -1)), + 0644, + ); err != nil { + return fmt.Errorf("Could not write temporary Consul template to %s\n%s", tmpPath, err.Error()) + } return nil } diff --git a/ha_proxy_test.go b/ha_proxy_test.go index 7e8c07b..63f2b4b 100644 --- a/ha_proxy_test.go +++ b/ha_proxy_test.go @@ -14,40 +14,42 @@ import ( type HaProxyTestSuite struct { suite.Suite - ScAddress string - Host string - CertPath string - ExitedMessage string - ProxyHost string - Project string - Color string - ServicePath []string - ReconfPort string - Server *httptest.Server + ScAddress string + CertPath string + ExitedMessage string + Host string + ServiceName string + Color string + ServicePath []string + ReconfPort string + DockerHost string + DockerCertPath string + Server *httptest.Server } func (s *HaProxyTestSuite) SetupTest() { s.ScAddress = "1.2.3.4:1234" - s.Host = "tcp://my-docker-proxy-host" - s.ProxyHost = "http://my-docker-proxy-host.com" - s.Project = "myProject" + s.ServiceName = "my-service" s.Color = "purpurina" s.ServicePath = []string{"/path/to/my/service", "/path/to/my/other/service"} s.ExitedMessage = "Exited (2) 15 seconds ago" s.ReconfPort = "5362" s.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { reconfigureUrl := fmt.Sprintf( - "/v1/docker-flow-proxy/reconfigure?serviceName=%s&servicePath=%s", - s.Project, + "/v1/docker-flow-proxy/reconfigure", + s.ServiceName, strings.Join(s.ServicePath, ","), ) actualUrl := fmt.Sprintf("%s?%s", r.URL.Path, r.URL.RawQuery) if r.Method == "GET" { - if actualUrl == reconfigureUrl { + if strings.HasPrefix(actualUrl, reconfigureUrl) { w.WriteHeader(http.StatusOK) } } })) + s.DockerHost = "tcp://my-docker-proxy-host" + s.DockerCertPath = "/path/to/pem" + s.Host = "http://my-docker-proxy-host.com" runHaProxyRunCmd = func(cmd *exec.Cmd) error { return nil } @@ -57,8 +59,6 @@ func (s *HaProxyTestSuite) SetupTest() { runHaProxyStartCmd = func(cmd *exec.Cmd) error { return nil } - logPrintln = func(v ...interface{}) {} - sleep = func(d time.Duration) {} httpGetOrig := httpGet defer func() { httpGet = httpGetOrig }() httpGet = func(url string) (resp *http.Response, err error) { @@ -70,6 +70,8 @@ func (s *HaProxyTestSuite) SetupTest() { func (s HaProxyTestSuite) Test_Provision_SetsDockerHost() { actual := "" + SetDockerHostOrig := SetDockerHost + defer func() { SetDockerHost = SetDockerHostOrig }() SetDockerHost = func(host, certPath string) { actual = host } @@ -139,7 +141,7 @@ func (s HaProxyTestSuite) Test_Provision_RunsDockerPs() { func (s HaProxyTestSuite) Test_Provision_ReturnsError_WhenPsFailure() { runHaProxyPsCmd = func(cmd *exec.Cmd) error { - return fmt.Errorf("This is an error") + return fmt.Errorf("This is an docker ps error") } err := HaProxy{}.Provision(s.Host, s.ReconfPort, s.CertPath, s.ScAddress) @@ -149,7 +151,7 @@ func (s HaProxyTestSuite) Test_Provision_ReturnsError_WhenPsFailure() { func (s HaProxyTestSuite) Test_Provision_ReturnsError_WhenProxyFails() { runHaProxyPsCmd = func(cmd *exec.Cmd) error { - return fmt.Errorf("This is an error") + return fmt.Errorf("This is an docker ps error") } err := HaProxy{}.Provision(s.Host, s.ReconfPort, s.CertPath, s.ScAddress) @@ -217,7 +219,7 @@ func (s HaProxyTestSuite) Test_Provision_ReturnsError_WhenStartFailure() { return nil } runHaProxyStartCmd = func(cmd *exec.Cmd) error { - return fmt.Errorf("This is an error") + return fmt.Errorf("This is an docker start error") } err := HaProxy{}.Provision(s.Host, s.ReconfPort, s.CertPath, s.ScAddress) @@ -228,25 +230,25 @@ func (s HaProxyTestSuite) Test_Provision_ReturnsError_WhenStartFailure() { // Reconfigure func (s HaProxyTestSuite) Test_Reconfigure_ReturnsError_WhenProxyHostIsEmpty() { - err := HaProxy{}.Reconfigure("", s.ReconfPort, s.Project, s.Color, s.ServicePath, "") + err := HaProxy{}.Reconfigure("", "", "", s.ReconfPort, s.ServiceName, s.Color, s.ServicePath, "") s.Error(err) } func (s HaProxyTestSuite) Test_Reconfigure_ReturnsError_WhenProjectIsEmpty() { - err := HaProxy{}.Reconfigure(s.ProxyHost, s.ReconfPort, "", s.Color, s.ServicePath, "") + err := HaProxy{}.Reconfigure("", "", s.Host, s.ReconfPort, "", s.Color, s.ServicePath, "") s.Error(err) } -func (s HaProxyTestSuite) Test_Reconfigure_ReturnsError_WhenServicePathIsEmpty() { - err := HaProxy{}.Reconfigure(s.ProxyHost, s.ReconfPort, s.Project, s.Color, []string{""}, "") +func (s HaProxyTestSuite) Test_Reconfigure_ReturnsError_WhenServicePathAndConsulTemplatePathAreEmpty() { + err := HaProxy{}.Reconfigure("", "", s.Host, s.ReconfPort, s.ServiceName, s.Color, []string{""}, "") s.Error(err) } func (s HaProxyTestSuite) Test_Reconfigure_ReturnsError_WhenReconfPortIsEmpty() { - err := HaProxy{}.Reconfigure(s.ProxyHost, "", s.Project, s.Color, s.ServicePath, "") + err := HaProxy{}.Reconfigure("", "", s.Host, "", s.ServiceName, s.Color, s.ServicePath, "") s.Error(err) } @@ -255,9 +257,9 @@ func (s HaProxyTestSuite) Test_Reconfigure_SendsHttpRequest() { actual := "" expected := fmt.Sprintf( "%s:%s/v1/docker-flow-proxy/reconfigure?serviceName=%s&serviceColor=%s&servicePath=%s", - s.ProxyHost, + s.Host, s.ReconfPort, - s.Project, + s.ServiceName, s.Color, strings.Join(s.ServicePath, ","), ) @@ -265,31 +267,31 @@ func (s HaProxyTestSuite) Test_Reconfigure_SendsHttpRequest() { defer func() { httpGet = httpGetOrig }() httpGet = func(url string) (resp *http.Response, err error) { actual = url - return nil, fmt.Errorf("This is an error") + return nil, fmt.Errorf("This is an HTTP error") } - HaProxy{}.Reconfigure(s.ProxyHost, s.ReconfPort, s.Project, s.Color, s.ServicePath, "") + HaProxy{}.Reconfigure("", "", s.Host, s.ReconfPort, s.ServiceName, s.Color, s.ServicePath, "") s.Equal(expected, actual) } -func (s HaProxyTestSuite) Test_Reconfigure_SendsHttpRequest_WithOutColor_WhenNotBlueGreen() { +func (s HaProxyTestSuite) Test_Reconfigure_SendsHttpRequestWithOutColor_WhenNotBlueGreen() { actual := "" expected := fmt.Sprintf( "%s:%s/v1/docker-flow-proxy/reconfigure?serviceName=%s&servicePath=%s", - s.ProxyHost, + s.Host, s.ReconfPort, - s.Project, + s.ServiceName, strings.Join(s.ServicePath, ","), ) httpGetOrig := httpGet defer func() { httpGet = httpGetOrig }() httpGet = func(url string) (resp *http.Response, err error) { actual = url - return nil, fmt.Errorf("This is an error") + return nil, fmt.Errorf("This is an HTTP error") } - HaProxy{}.Reconfigure(s.ProxyHost, s.ReconfPort, s.Project, "", s.ServicePath, "") + HaProxy{}.Reconfigure("", "", s.Host, s.ReconfPort, s.ServiceName, "", s.ServicePath, "") s.Equal(expected, actual) } @@ -298,19 +300,19 @@ func (s HaProxyTestSuite) Test_Reconfigure_SendsHttpRequestWithPrependedHttp() { actual := "" expected := fmt.Sprintf( "%s:%s/v1/docker-flow-proxy/reconfigure?serviceName=%s&servicePath=%s", - s.ProxyHost, + s.Host, s.ReconfPort, - s.Project, + s.ServiceName, strings.Join(s.ServicePath, ","), ) httpGetOrig := httpGet defer func() { httpGet = httpGetOrig }() httpGet = func(url string) (resp *http.Response, err error) { actual = url - return nil, fmt.Errorf("This is an error") + return nil, fmt.Errorf("This is an HTTP error") } - HaProxy{}.Reconfigure("my-docker-proxy-host.com", s.ReconfPort, s.Project, "", s.ServicePath, "") + HaProxy{}.Reconfigure("", "", "my-docker-proxy-host.com", s.ReconfPort, s.ServiceName, "", s.ServicePath, "") s.Equal(expected, actual) } @@ -319,10 +321,10 @@ func (s HaProxyTestSuite) Test_Reconfigure_ReturnsError_WhenRequestFails() { httpGetOrig := httpGet defer func() { httpGet = httpGetOrig }() httpGet = func(url string) (resp *http.Response, err error) { - return nil, fmt.Errorf("This is an error") + return nil, fmt.Errorf("This is an HTTP error") } - err := HaProxy{}.Reconfigure(s.ProxyHost, s.ReconfPort, s.Project, s.Color, s.ServicePath, "") + err := HaProxy{}.Reconfigure("", "", s.Host, s.ReconfPort, s.ServiceName, s.Color, s.ServicePath, "") s.Error(err) } @@ -332,82 +334,182 @@ func (s HaProxyTestSuite) Test_Reconfigure_ReturnsError_WhenResponseCodeIsNot2xx w.WriteHeader(http.StatusBadRequest) })) - err := HaProxy{}.Reconfigure(server.URL, "", s.Project, s.Color, s.ServicePath, "") + err := HaProxy{}.Reconfigure("", "", server.URL, "", s.ServiceName, s.Color, s.ServicePath, "") s.Error(err) } -func (s HaProxyTestSuite) Test_Reconfigure_ReadsConsulTemplatePath() { - consulTemplatePath := "/path/to/consul/template" - actual := "" - readConsulTemplate = func(fileName string) ([]byte, error) { - actual = fileName - return []byte(""), nil +func (s HaProxyTestSuite) Test_Reconfigure_SetsDockerHost_WhenConsulTemplatePathIsPresent() { + os.Unsetenv("DOCKER_HOST") + + err := HaProxy{}.Reconfigure(s.DockerHost, s.DockerCertPath, s.Server.URL, "", s.ServiceName, s.Color, s.ServicePath, "/path/to/consul/template") + + s.NoError(err) + s.Equal(s.DockerHost, os.Getenv("DOCKER_HOST")) + s.Equal(s.DockerCertPath, os.Getenv("DOCKER_CERT_PATH")) +} + +func (s HaProxyTestSuite) Test_Reconfigure_CreatesConsulTemplatesDirectory_WhenConsulTemplatePathIsPresent() { + var actual []string + expected := []string{"docker", "exec", "-i", "docker-flow-proxy", "mkdir", "-p", "/consul_templates"} + runHaProxyExecCmd = func(cmd *exec.Cmd) error { + actual = cmd.Args + return nil } - HaProxy{}.Reconfigure(s.Server.URL, s.ReconfPort, s.Project, s.Color, s.ServicePath, consulTemplatePath) + HaProxy{}.Reconfigure("", "", s.Server.URL, "", s.ServiceName, s.Color, s.ServicePath, "/path/to/consul/template") - s.Equal(consulTemplatePath, actual) + s.Equal(expected, actual) } -func (s HaProxyTestSuite) Test_Reconfigure_ReturnsError_WhenReadingConsulTemplatePathFails() { - consulTemplatePath := "/path/to/consul/template" - readConsulTemplateOrig := readConsulTemplate - defer func() { readConsulTemplate = readConsulTemplateOrig }() - readConsulTemplate = func(fileName string) ([]byte, error) { - return []byte(""), fmt.Errorf("This is an error") +func (s HaProxyTestSuite) Test_Reconfigure_ReturnsError_WhenDirectoryCreationFails() { + runHaProxyExecCmdOrig := runHaProxyExecCmd + defer func() { runHaProxyExecCmd = runHaProxyExecCmdOrig }() + runHaProxyExecCmd = func(cmd *exec.Cmd) error { + return fmt.Errorf("This is an docker exec error") } - err := HaProxy{}.Reconfigure(s.Server.URL, "", s.Project, s.Color, s.ServicePath, consulTemplatePath) + actual := HaProxy{}.Reconfigure("", "", s.Server.URL, "", s.ServiceName, s.Color, s.ServicePath, "/path/to/consul/template") - s.Error(err) + s.Error(actual) } -func (s HaProxyTestSuite) Test_Reconfigure_TransfersConsulTemplateToTheProxy() { - consulTemplate := `This is a 'consul' template` +func (s HaProxyTestSuite) Test_Reconfigure_CopiesTemplate_WhenConsulTemplatePathIsPresent() { consulTemplatePath := "/path/to/consul/template" var actual []string - command := fmt.Sprintf("echo '%s'", `This is a \'consul\' template`) - expected := []string{"docker", "exec", "-it", "docker-flow-proxy", command} - runHaProxyExecCmd = func(cmd *exec.Cmd) error { + expected := []string{ + "docker", + "cp", + fmt.Sprintf("%s.tmp", consulTemplatePath), + fmt.Sprintf("docker-flow-proxy:/consul_templates/%s.tmpl", s.ServiceName), + } + runHaProxyCpCmdOrig := runHaProxyCpCmd + defer func() { runHaProxyCpCmd = runHaProxyCpCmdOrig }() + runHaProxyCpCmd = func(cmd *exec.Cmd) error { actual = cmd.Args return nil } - readConsulTemplate = func(fileName string) ([]byte, error) { - return []byte(consulTemplate), nil + + HaProxy{}.Reconfigure("", "", s.Server.URL, "", s.ServiceName, s.Color, s.ServicePath, "/path/to/consul/template") + + s.Equal(expected, actual) +} + +func (s HaProxyTestSuite) Test_Reconfigure_ReturnsError_WhenTemplateCopyFails() { + runHaProxyCpCmdOrig := runHaProxyCpCmd + defer func() { runHaProxyCpCmd = runHaProxyCpCmdOrig }() + runHaProxyCpCmd = func(cmd *exec.Cmd) error { + return fmt.Errorf("This is an docker cp error") } - HaProxy{}.Reconfigure(s.Server.URL, s.ReconfPort, s.Project, s.Color, s.ServicePath, consulTemplatePath) + actual := HaProxy{}.Reconfigure("", "", s.Server.URL, "", s.ServiceName, s.Color, s.ServicePath, "/path/to/consul/template") + + s.Error(actual) +} + +func (s HaProxyTestSuite) Test_Reconfigure_SendsHttpRequestWithConsulTemplatePath_WhenSpecified() { + actual := "" + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + actual = fmt.Sprintf("%s?%s", r.URL.Path, r.URL.RawQuery) + })) + expected := fmt.Sprintf( + "/v1/docker-flow-proxy/reconfigure?serviceName=%s&consulTemplatePath=/consul_templates/%s.tmpl", + s.ServiceName, + s.ServiceName, + ) + + HaProxy{}.Reconfigure("", "", server.URL, "", s.ServiceName, s.Color, s.ServicePath, "/path/to/consul/template") s.Equal(expected, actual) } -//func (s HaProxyTestSuite) Test_Reconfigure_SendsHttpRequestWithConsulTemplatePath_WhenSpecified() { -// actual := "" -// expected := fmt.Sprintf( -// "%s:%s/v1/docker-flow-proxy/reconfigure?serviceName=%s&consulTemplatePath=%s", -// s.ProxyHost, -// s.ReconfPort, -// s.Project, -// s.ConsulTemplatePath, -// ) -// httpGetOrig := httpGet -// defer func() { httpGet = httpGetOrig }() -// httpGet = func(url string) (resp *http.Response, err error) { -// actual = url -// return nil, fmt.Errorf("This is an error") -// } -// -// HaProxy{}.Reconfigure(s.ProxyHost, s.ReconfPort, s.Project, s.Color, s.ServicePath) -// -// s.Equal(expected, actual) -//} +func (s HaProxyTestSuite) Test_Reconfigure_CreatesTempTemplateFile() { + actualFilename := "" + actualData := "" + data := "This is a %s template" + expectedData := fmt.Sprintf(data, s.ServiceName+"-"+s.Color) + writeFileOrig := writeFile + defer func() { writeFile = writeFileOrig }() + writeFile = func(filename string, data []byte, perm os.FileMode) error { + actualFilename = filename + actualData = string(data) + return nil + } + readFileOrig := readFile + defer func() { readFile = readFileOrig }() + readFile = func(fileName string) ([]byte, error) { + return []byte(fmt.Sprintf(data, "SERVICE_NAME")), nil + } + + HaProxy{}.Reconfigure("", "", s.Server.URL, "", s.ServiceName, s.Color, s.ServicePath, "/path/to/consul/template") + + s.Equal("/path/to/consul/template.tmp", actualFilename) + s.Equal(expectedData, actualData) +} + +func (s HaProxyTestSuite) Test_Reconfigure_ReturnsError_WhenTemplateFileReadFails() { + readFileOrig := readFile + defer func() { readFile = readFileOrig }() + readFile = func(fileName string) ([]byte, error) { + return []byte(""), fmt.Errorf("This is an read file error") + } + + err := HaProxy{}.Reconfigure("", "", s.Server.URL, "", s.ServiceName, s.Color, s.ServicePath, "/path/to/consul/template") + + s.Error(err) +} + +func (s HaProxyTestSuite) Test_Reconfigure_ReturnsError_WhenTempTemplateFileCreationFails() { + writeFileOrig := writeFile + defer func() { writeFile = writeFileOrig }() + writeFile = func(filename string, data []byte, perm os.FileMode) error { + return fmt.Errorf("This is an write file error") + } + + err := HaProxy{}.Reconfigure("", "", s.Server.URL, "", s.ServiceName, s.Color, s.ServicePath, "/path/to/consul/template") + + s.Error(err) +} + +func (s HaProxyTestSuite) Test_Reconfigure_RemovesTempTemplateFile() { + path := "/path/to/consul/template" + expected := fmt.Sprintf("%s.tmp", path) + actual := "" + removeFileOrig := removeFile + defer func() { removeFile = removeFileOrig }() + removeFile = func(name string) error { + actual = name + return nil + } + + HaProxy{}.Reconfigure("", "", s.Server.URL, "", s.ServiceName, s.Color, s.ServicePath, "/path/to/consul/template") + + s.Equal(expected, actual) +} // Suite func TestHaProxyTestSuite(t *testing.T) { + logPrintln = func(v ...interface{}) {} + logPrintf = func(format string, v ...interface{}) {} + sleep = func(d time.Duration) {} dockerHost := os.Getenv("DOCKER_HOST") dockerCertPath := os.Getenv("DOCKER_CERT_PATH") + runHaProxyExecCmd = func(cmd *exec.Cmd) error { + return nil + } + runHaProxyCpCmd = func(cmd *exec.Cmd) error { + return nil + } + writeFile = func(fileName string, data []byte, perm os.FileMode) error { + return nil + } + readFile = func(fileName string) ([]byte, error) { + return []byte(""), nil + } + removeFile = func(name string) error { + return nil + } defer func() { os.Setenv("DOCKER_HOST", dockerHost) os.Setenv("DOCKER_CERT_PATH", dockerCertPath) diff --git a/integration_test.go b/integration_test.go index 763f913..d9e43ae 100644 --- a/integration_test.go +++ b/integration_test.go @@ -13,27 +13,27 @@ package main // $ docker-machine rm -f docker-flow-test import ( + "bytes" "fmt" "github.com/stretchr/testify/suite" - "testing" - "os/exec" - "strings" "log" - "bytes" + "net/http" "os" + "os/exec" + "strings" + "testing" "time" - "net/http" ) type IntegrationTestSuite struct { suite.Suite - ConsulIp string - ProxyIp string - ProxyHost string - ProxyDockerHost string + ConsulIp string + ProxyIp string + ProxyHost string + ProxyDockerHost string ProxyDockerCertPath string - ServicePath string - ServiceName string + ServicePath string + ServiceName string } func (s *IntegrationTestSuite) SetupTest() { @@ -43,7 +43,7 @@ func (s *IntegrationTestSuite) SetupTest() { // Integration -func (s IntegrationTestSuite) Test_BlueGreenDeployment() { +func (s IntegrationTestSuite) XTest_BlueGreenDeployment() { origConsulAddress := os.Getenv("FLOW_CONSUL_ADDRESS") defer func() { os.Setenv("FLOW_CONSUL_ADDRESS", origConsulAddress) @@ -61,16 +61,16 @@ func (s IntegrationTestSuite) Test_BlueGreenDeployment() { "--blue-green", ) s.verifyContainer([]ContainerStatus{ - {"godemo_app-blue_1", "Up" }, - {"godemo_db", "Up" }, + {"godemo_app-blue_1", "Up"}, + {"godemo_db", "Up"}, }) log.Println("Second deployment (green)") os.Setenv("FLOW_CONSUL_ADDRESS", fmt.Sprintf("http://%s:8500", s.ConsulIp)) s.runCmdWithStdOut(true, "./docker-flow", "--flow", "deploy") s.verifyContainer([]ContainerStatus{ - {"godemo_app-blue_1", "Up" }, - {"godemo_app-green_1", "Up" }, + {"godemo_app-blue_1", "Up"}, + {"godemo_app-green_1", "Up"}, }) log.Println("Third deployment (blue) with stop old release (green)") @@ -79,12 +79,12 @@ func (s IntegrationTestSuite) Test_BlueGreenDeployment() { "./docker-flow", "--flow", "deploy", "--flow", "stop-old") s.verifyContainer([]ContainerStatus{ - {"godemo_app-blue_1", "Up" }, - {"godemo_app-green_1", "Exited" }, + {"godemo_app-blue_1", "Up"}, + {"godemo_app-green_1", "Exited"}, }) } -func (s IntegrationTestSuite) Test_Scaling() { +func (s IntegrationTestSuite) XTest_Scaling() { log.Println(">> Integration tests: scaling") log.Println("First deployment (blue, 2 instances)") @@ -96,9 +96,9 @@ func (s IntegrationTestSuite) Test_Scaling() { "--scale", "2", ) s.verifyContainer([]ContainerStatus{ - {"godemo_app-blue_1", "Up" }, - {"godemo_app-blue_2", "Up" }, - {"godemo_db", "Up" }, + {"godemo_app-blue_1", "Up"}, + {"godemo_app-blue_2", "Up"}, + {"godemo_db", "Up"}, }) log.Println("Second deployment (green, 4 (+2) instances)") @@ -110,10 +110,10 @@ func (s IntegrationTestSuite) Test_Scaling() { "--scale", "+2", ) s.verifyContainer([]ContainerStatus{ - {"godemo_app-green_1", "Up" }, - {"godemo_app-green_2", "Up" }, - {"godemo_app-green_3", "Up" }, - {"godemo_app-green_4", "Up" }, + {"godemo_app-green_1", "Up"}, + {"godemo_app-green_2", "Up"}, + {"godemo_app-green_3", "Up"}, + {"godemo_app-green_4", "Up"}, }) log.Println("Scaling (green, 3 (-1) instances)") @@ -125,14 +125,14 @@ func (s IntegrationTestSuite) Test_Scaling() { "--scale", "\"-1\"", ) s.verifyContainer([]ContainerStatus{ - {"godemo_app-green_1", "Up" }, - {"godemo_app-green_2", "Up" }, - {"godemo_app-green_3", "Up" }, - {"godemo_app-green_4", "N/A" }, + {"godemo_app-green_1", "Up"}, + {"godemo_app-green_2", "Up"}, + {"godemo_app-green_3", "Up"}, + {"godemo_app-green_4", "N/A"}, }) } -func (s IntegrationTestSuite) Test_Proxy() { +func (s IntegrationTestSuite) XTest_Proxy() { log.Println(">> Integration tests: proxy") s.runCmdWithStdOut( @@ -155,7 +155,7 @@ func (s IntegrationTestSuite) Test_Proxy() { "--flow", "deploy", "--flow", "proxy", ) s.verifyContainer([]ContainerStatus{ - {"docker-flow-proxy", "Up" }, + {"docker-flow-proxy", "Up"}, }) url := fmt.Sprintf("http://%s%s", s.ConsulIp, s.ServicePath) resp, err := http.Get(url) @@ -165,7 +165,7 @@ func (s IntegrationTestSuite) Test_Proxy() { log.Println("Runs proxy when stopped and reconfigures it when scale") s.runCmdWithStdOut(false, "docker", "stop", "docker-flow-proxy") s.verifyContainer([]ContainerStatus{ - {"docker-flow-proxy", "Exited" }, + {"docker-flow-proxy", "Exited"}, }) s.runCmdWithStdOut( true, @@ -179,7 +179,7 @@ func (s IntegrationTestSuite) Test_Proxy() { "--flow", "scale", "--flow", "proxy", ) s.verifyContainer([]ContainerStatus{ - {"docker-flow-proxy", "Up" }, + {"docker-flow-proxy", "Up"}, }) resp, err = http.Get(fmt.Sprintf("http://%s%s", s.ConsulIp, s.ServicePath)) s.NoError(err) @@ -229,10 +229,11 @@ func (s IntegrationTestSuite) Test_Proxy_Templates() { "--proxy-docker-host", s.ProxyDockerHost, "--proxy-docker-cert-path", s.ProxyDockerCertPath, "--service-path", "INCORRECT", + "--consul-template-path", "test_configs/tmpl/go-demo-app.tmpl", "--flow", "deploy", "--flow", "proxy", ) s.verifyContainer([]ContainerStatus{ - {"docker-flow-proxy", "Up" }, + {"docker-flow-proxy", "Up"}, }) url := fmt.Sprintf("http://%s%s", s.ConsulIp, s.ServicePath) resp, err := http.Get(url) @@ -241,10 +242,12 @@ func (s IntegrationTestSuite) Test_Proxy_Templates() { } // Util + type ContainerStatus struct { - Name string - Status string + Name string + Status string } + func (s IntegrationTestSuite) verifyContainer(csList []ContainerStatus) { s.runCmdWithStdOut(false, "docker", "ps", "-a") for _, cs := range csList { diff --git a/opts.go b/opts.go index 034a588..619f994 100644 --- a/opts.go +++ b/opts.go @@ -38,12 +38,12 @@ type Opts struct { SideTargets []string `short:"T" long:"side-target" description:"Side or auxiliary Docker Compose targets. Multiple values are allowed." yaml:"side_targets"` Target string `short:"t" long:"target" description:"Docker Compose target."` - ServiceName string - CurrentColor string - NextColor string - CurrentTarget string - NextTarget string - ConsulTemplate string + ServiceName string + CurrentColor string + NextColor string + CurrentTarget string + NextTarget string + ConsulTemplate string } var GetOpts = func() (Opts, error) { @@ -122,7 +122,7 @@ func ProcessOpts(opts *Opts) (err error) { return fmt.Errorf("scale must be a number or empty") } } - if (len(opts.ConsulTemplatePath) > 0) { + if len(opts.ConsulTemplatePath) > 0 { data, err := readFile(opts.ConsulTemplatePath) if err != nil { return fmt.Errorf("Consul Template %s could not be loaded", opts.ConsulTemplatePath) diff --git a/proxy.go b/proxy.go index 8052eef..9a1ce79 100644 --- a/proxy.go +++ b/proxy.go @@ -1,6 +1,6 @@ package main type Proxy interface { - Provision(host, reconfPort, certPath, scAddress string) error - Reconfigure(host, reconfPort, serviceName, serviceColor string, servicePath []string, consulTemplatePath string) error + Provision(dockerHost, reconfPort, certPath, scAddress string) error + Reconfigure(dockerHost, proxyCertPath, host, reconfPort, serviceName, serviceColor string, servicePath []string, consulTemplatePath string) error } diff --git a/proxy_test.go b/proxy_test.go index e284acb..71e3e7c 100644 --- a/proxy_test.go +++ b/proxy_test.go @@ -15,8 +15,8 @@ func (m *ProxyMock) Provision(host, reconfPort, certPath, scAddress string) erro return args.Error(0) } -func (m *ProxyMock) Reconfigure(host, reconfPort, serviceName, serviceColor string, servicePath []string, consulTemplatePath string) error { - args := m.Called(host, reconfPort, serviceName, serviceColor, servicePath, consulTemplatePath) +func (m *ProxyMock) Reconfigure(dockerHost, proxyCertPath, host, reconfPort, serviceName, serviceColor string, servicePath []string, consulTemplatePath string) error { + args := m.Called(dockerHost, proxyCertPath, host, reconfPort, serviceName, serviceColor, servicePath, consulTemplatePath) return args.Error(0) } @@ -26,7 +26,7 @@ func getProxyMock(skipMethod string) *ProxyMock { mockObj.On("Provision", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) } if skipMethod != "Reconfigure" { - mockObj.On("Reconfigure", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) + mockObj.On("Reconfigure", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) } return mockObj } diff --git a/test_configs/tmpl/go-demo-app.tmpl b/test_configs/tmpl/go-demo-app.tmpl new file mode 100644 index 0000000..4131baf --- /dev/null +++ b/test_configs/tmpl/go-demo-app.tmpl @@ -0,0 +1,11 @@ +frontend go-demo-app-fe + bind *:80 + bind *:443 + option http-server-close + acl url_test-service path_beg /demo + use_backend go-demo-app-be if url_test-service + +backend go-demo-app-be + {{ range $i, $e := service "SERVICE_NAME" "any" }} + server {{$e.Node}}_{{$i}}_{{$e.Port}} {{$e.Address}}:{{$e.Port}} check + {{end}} diff --git a/util.go b/util.go index 047906c..15db266 100644 --- a/util.go +++ b/util.go @@ -8,7 +8,6 @@ import ( ) var readFile = ioutil.ReadFile -var readConsulTemplate = ioutil.ReadFile var writeFile = ioutil.WriteFile var removeFile = os.Remove var execCmd = exec.Command From 14fa666f1330bcd08520253bf1161979be5ae9e8 Mon Sep 17 00:00:00 2001 From: vfarcic Date: Sun, 22 May 2016 19:08:12 +0200 Subject: [PATCH 3/5] Updating README #9 --- README.md | 13 +++++++++---- setup.sh | 4 +++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8f5bac9..bfc13b4 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,12 @@ The latest release can be found [here](/~https://github.com/vfarcic/docker-flow/re Examples -------- -The examples that follow will use [Docker Machine](https://www.docker.com/products/docker-machine) to simulate a [Docker Swarm](https://www.docker.com/products/docker-swarm) cluster. That does not mean that the usage of **Docker Flow** is limited to either of those two. You can use it with a single [Docker Engine](https://www.docker.com/products/docker-engine) or a Swarm cluster set up in any other way. +The examples that follow assume that you have Docker Machine and Docker Compose installed. The easiest way to get them is through [Docker Toolbox](https://www.docker.com/products/docker-toolbox). + +> If you are a Windows user, please run all the examples from *Git Bash* (installed through *Docker Toolbox*). + +We'll use [Docker Machine](https://www.docker.com/products/docker-machine) to simulate a [Docker Swarm](https://www.docker.com/products/docker-swarm) cluster. That does not mean that the usage of **Docker Flow** is limited to either of those two. You can use it with a single [Docker Engine](https://www.docker.com/products/docker-engine) or a Swarm cluster set up in any other way. -Please note that the examples presented below have been tested on OS X and Linux. In case you are a Windows user, you might want to explore the OS agnostic examples provided in the **[Docker Flow: Walkthrough](https://technologyconversations.com/2016/04/18/docker-flow/)** article. ### Setting it up @@ -86,8 +89,8 @@ docker ps -a The output of the `ps` command is as follows. ``` -CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -6a33159ba9e3 progrium/consul "/bin/start -server -" 5 minutes ago Up 5 minutes 53/udp, 53/tcp, 8302/tcp, 0.0.0.0:8300-8301->8300-8301/tcp, 8400/tcp, 8301-8302/udp, 0.0.0.0:8500->8500/tcp consul +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +ccb5e812b7e1 consul "docker-entrypoint.sh" 23 minutes ago Up 23 minutes consul ``` With the cluster and the proxy server set up, we are ready to give **Docker Flow** a spin and see it in action. @@ -101,6 +104,8 @@ Let's start by defining proxy and Consul data through environment variables. ```bash export FLOW_PROXY_HOST=$(docker-machine ip proxy) +export CONSUL_IP=$(docker-machine ip proxy) + export FLOW_CONSUL_ADDRESS=http://$CONSUL_IP:8500 eval "$(docker-machine env proxy)" diff --git a/setup.sh b/setup.sh index d59f822..a734670 100755 --- a/setup.sh +++ b/setup.sh @@ -4,12 +4,14 @@ docker-machine create -d virtualbox proxy export CONSUL_IP=$(docker-machine ip proxy) +export HOST_IP=$(docker-machine ip proxy) + eval "$(docker-machine env proxy)" docker-compose \ -p setup \ -f docker-compose-setup.yml \ - up -d consul + up -d consul-server docker-machine create -d virtualbox \ --swarm --swarm-master \ From 2b05adeaeede05c5b420bb2f278b6e09bd3925c1 Mon Sep 17 00:00:00 2001 From: vfarcic Date: Sun, 22 May 2016 19:44:15 +0200 Subject: [PATCH 4/5] Finished README #9 --- .travis.yml | 2 +- README.md | 93 ++++++++++++++++++++++++++++++++++--------------- docker-flow.yml | 2 +- 3 files changed, 66 insertions(+), 31 deletions(-) diff --git a/.travis.yml b/.travis.yml index bb28599..fc0173d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ env: - - VERSION=1.0.1-beta + - VERSION=1.0 language: go diff --git a/README.md b/README.md index bfc13b4..447e055 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,14 @@ Docker Flow * [Introduction](#introduction) * [Examples](#examples) * [Usage](#usage) + + * [Setting it up](#setting-it-up) + * [Reconfiguring proxy after deployment](#reconfiguring-proxy-after-deployment) + * [Deploying a new release without downtime](#deploying-a-new-release-without-downtime) + * [Scaling the service](#scaling-the-service) + * [Testing deployments to production](#testing-deployments-to-production) + * [Using custom Consul templates](#using-custom-consul-templates) + * [Feedback and Contribution](#feedback-and-contribution) Introduction @@ -102,10 +110,12 @@ With the cluster and the proxy server set up, we are ready to give **Docker Flow Let's start by defining proxy and Consul data through environment variables. ```bash -export FLOW_PROXY_HOST=$(docker-machine ip proxy) +export PROXY_IP=$(docker-machine ip proxy) export CONSUL_IP=$(docker-machine ip proxy) +export FLOW_PROXY_HOST=$(docker-machine ip proxy) + export FLOW_CONSUL_ADDRESS=http://$CONSUL_IP:8500 eval "$(docker-machine env proxy)" @@ -125,7 +135,7 @@ eval "$(docker-machine env --swarm swarm-master)" ./docker-flow \ --blue-green \ --target=app \ - --service-path="/api/v1/books" \ + --service-path="/demo" \ --side-target=db \ --flow=deploy --flow=proxy ``` @@ -141,9 +151,9 @@ docker ps --format "table {{.Names}}\t{{.Image}}" The output of the `ps` command is as follows. ``` -NAMES IMAGE -swarm-node-2/dockerflow_app-blue_1 vfarcic/books-ms -swarm-node-1/books-ms-db mongo +NAMES IMAGE +swarm-node-2/dockerflow_app-green_1 vfarcic/go-demo +swarm-node-1/dockerflow_db_1 mongo ... ``` @@ -162,7 +172,7 @@ The output of the `ps` command is as follows. ``` NAMES IMAGE docker-flow-proxy vfarcic/docker-flow-proxy -consul progrium/consul +consul consul ``` *Docker Flow* detected that there was no *proxy* on that node and run it for us. The *docker-flow-proxy* container contains *HAProxy* together with custom code that reconfigures it every time a new service is run. For more information about the *Docker Flow: Proxy*, please read the [project README](/~https://github.com/vfarcic/docker-flow-proxy). @@ -170,18 +180,18 @@ consul progrium/consul Since we instructed Swarm to run our service somewhere inside the cluster, we could not know in advance which server will be chosen. In this particular case, our service ended up running inside the *swarm-node-2*. Moreover, to avoid potential conflicts and allow easier scaling, we did not specify which port the service should expose. In other words, both the IP and the port of the service were not defined in advance. Among other things, *Docker Flow* solves this by running *Docker Flow: Proxy* and instructing it to reconfigure itself with the information gathered after the container is run. We can confirm that the proxy reconfiguration was indeed successful by sending an HTTP request to the newly deployed service. ```bash -curl -I $PROXY_IP/api/v1/books +curl -i $PROXY_IP/demo/hello ``` The output of the `curl` command is as follows. ``` HTTP/1.1 200 OK -Server: spray-can/1.3.1 -Date: Thu, 07 Apr 2016 19:23:34 GMT -Access-Control-Allow-Origin: * -Content-Type: application/json; charset=UTF-8 -Content-Length: 2 +Date: Sun, 22 May 2016 17:23:41 GMT +Content-Length: 14 +Content-Type: text/plain; charset=utf-8 + +hello, world! ``` Even though our service is running in one of the servers chosen by Swarm and is exposing a random port, the proxy was reconfigured and we can access it through a fixed IP and without a port (to be more precise through standard HTTP port 80 or HTTPS port 443). @@ -198,7 +208,7 @@ side_targets: - db blue_green: true service_path: - - /api/v1/books + - /demo ``` Let's run the new release. @@ -220,9 +230,9 @@ The output of the `ps` command is as follows. ```bash NAMES IMAGE STATUS -swarm-node-1/dockerflow_app-green_1 vfarcic/books-ms Up 52 seconds -swarm-node-2/dockerflow_app-blue_1 vfarcic/books-ms Exited (137) 54 seconds ago -swarm-node-1/books-ms-db mongo Up About an hour +swarm-node-2/dockerflow_app-green_1 vfarcic/go-demo Up 7 seconds +swarm-master/dockerflow_app-blue_1 vfarcic/go-demo Exited (2) 6 seconds ago +swarm-node-1/dockerflow_db_1 mongo Up 12 minutes ... ``` @@ -231,18 +241,18 @@ From the output, we can observe that the new release (*green*) is running and th Let's confirm that the proxy was reconfigured as well. ```bash -curl -I $PROXY_IP/api/v1/books +curl -i $PROXY_IP/demo/hello ``` The output of the `curl` command is as follows. ``` HTTP/1.1 200 OK -Server: spray-can/1.3.1 -Date: Thu, 07 Apr 2016 19:45:07 GMT -Access-Control-Allow-Origin: * -Content-Type: application/json; charset=UTF-8 -Content-Length: 2 +Date: Sun, 22 May 2016 17:25:19 GMT +Content-Length: 14 +Content-Type: text/plain; charset=utf-8 + +hello, world! ``` The new release was deployed without any downtime and the proxy has been reconfigured to redirect all requests to it. @@ -269,9 +279,9 @@ The output of the `ps` command is as follows. ``` NAMES IMAGE STATUS -swarm-node-2/dockerflow_app-green_2 vfarcic/books-ms Up About a minute -swarm-master/dockerflow_app-green_3 vfarcic/books-ms Up About a minute -swarm-node-1/dockerflow_app-green_1 vfarcic/books-ms Up 33 minutes +swarm-node-2/dockerflow_app-green_3 vfarcic/go-demo Up 8 seconds +swarm-node-1/dockerflow_app-green_2 vfarcic/go-demo Up 8 seconds +swarm-node-2/dockerflow_app-green_1 vfarcic/go-demo Up About a minute ``` The number of instances was increased by two. While only one instance was running before, now we have three. @@ -309,10 +319,11 @@ The output of the `ps` command is as follows. ``` NAMES STATUS PORTS -swarm-node-2/dockerflow_app-blue_1 Up 5 minutes 192.168.99.103:32770->8080/tcp -swarm-node-1/dockerflow_app-blue_2 Up 5 minutes 192.168.99.102:32773->8080/tcp -swarm-node-1/dockerflow_app-green_1 Up About an hour 192.168.99.102:32768->8080/tcp -swarm-node-2/dockerflow_app-green_2 Up About an hour 192.168.99.103:32763->8080/tcp +swarm-master/dockerflow_app-blue_2 Up 8 seconds 192.168.99.101:32769->8080/tcp +swarm-node-2/dockerflow_app-blue_1 Up 9 seconds 192.168.99.103:32771->8080/tcp +swarm-node-1/dockerflow_app-green_2 Up About a minute 192.168.99.102:32768->8080/tcp +swarm-node-2/dockerflow_app-green_1 Up 2 minutes 192.168.99.103:32769->8080/tcp +... ``` At this moment, the new release (*blue*) is running in parallel with the old release (*green*). Since we did not specify the *--flow=proxy* argument, the proxy is left unchanged and still redirects to all the instances of the old release. What this means is that the users of our service still see the old release while we have the opportunity to test it. We can run integration, functional, or any other type of tests and validate that the new release indeed meets the expectations we have. While testing in production does not exclude testing in other environments (e.g. staging), this approach gives us greater level of trust by being able to validate the software under exactly the same circumstances our users will use it while, at the same time, not affecting them during the process (they are still oblivious of the existence of the new release). @@ -327,6 +338,30 @@ After the tests are run, we have two paths we can take. If one of the tests fail --flow=stop-old ``` +### Using custom Consul templates + +While, in most cases, automatic proxy configuration is all you need, you might have a special use case that would benefit from custom Consul templates. In such a case, you'd prepare your own templates and let *Docker Flow* use them throughout the process. + +For more information regarding templating format, please visit the [Consul Template](/~https://github.com/hashicorp/consul-template) project. + +The following example illustrates the usage of custom Consul templates. + +```bash +eval "$(docker-machine env --swarm swarm-master)" + +./docker-flow \ + --consul-template-path test_configs/tmpl/go-demo-app.tmpl \ + --flow=deploy --flow=proxy --flow=stop-old +``` + +*Docker Flow* processed the template located in the `test_configs/tmpl/go-demo-app.tmpl` file and sent the result to the proxy. The rest of the process was the same as explained earlier. + +Let's confirm whether the proxy was indeed configured correctly. + +```bash +curl -i $PROXY_IP/demo/hello +``` + That concludes the quick tour through some of the features *Docker Flow* provides. Please explore the [Usage](#usage) section for more details. Usage diff --git a/docker-flow.yml b/docker-flow.yml index 2fe7b34..41afb8e 100644 --- a/docker-flow.yml +++ b/docker-flow.yml @@ -3,4 +3,4 @@ side_targets: - db blue_green: true service_path: - - /demo/hello \ No newline at end of file + - /demo \ No newline at end of file From c7cf42876b1e75dbfa5d4aea8c75934ac44ffc33 Mon Sep 17 00:00:00 2001 From: vfarcic Date: Sun, 22 May 2016 19:46:55 +0200 Subject: [PATCH 5/5] README proofreading #9 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 447e055..e3edb70 100644 --- a/README.md +++ b/README.md @@ -340,7 +340,7 @@ After the tests are run, we have two paths we can take. If one of the tests fail ### Using custom Consul templates -While, in most cases, automatic proxy configuration is all you need, you might have a special use case that would benefit from custom Consul templates. In such a case, you'd prepare your own templates and let *Docker Flow* use them throughout the process. +While, in most cases, the automatic proxy configuration is all you need, you might have a particular use case that would benefit from custom Consul templates. In such a case, you'd prepare your own templates and let *Docker Flow* use them throughout the process. For more information regarding templating format, please visit the [Consul Template](/~https://github.com/hashicorp/consul-template) project.