diff --git a/README.md b/README.md index e620ea6a..4f139474 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ Docker compose compatible client that deploys to [Rancher](https://github.com/rancherio/rancher). +## Binaries + +Binaries are available for Linux, OS X, and Windows. Refer to the latest [release](https://github.com/rancherio/rancher-compose/releases). + ## Building Run `./scripts/build` to create `./bin/rancher-compose` @@ -58,6 +62,7 @@ do nothing. This is because the first up will create everything and leave it ru Docker builds are supported in two ways. First is to set `build:` to a git or HTTP URL that is compatible with the remote parameter in https://docs.docker.com/reference/api/docker_remote_api_v1.18/#build-image-from-a-dockerfile. The second approach is to set `build:` to a local directory and the build context will be uploaded to S3 and then built on demand on each node. +For S3 based builds to work you must [setup AWS credentials](https://github.com/aws/aws-sdk-go/#configuring-credentials). ## Contact diff --git a/librcompose/docker/convert.go b/librcompose/docker/convert.go index f2c83172..a6e68eef 100644 --- a/librcompose/docker/convert.go +++ b/librcompose/docker/convert.go @@ -36,10 +36,6 @@ func Convert(c *project.ServiceConfig) (*runconfig.Config, *runconfig.HostConfig volumes[v] = struct{}{} } - cmd, err := shlex.Split(c.Command.ToString()) - if err != nil { - return nil, nil, err - } entrypoint, err := shlex.Split(c.Entrypoint) if err != nil { return nil, nil, err @@ -72,7 +68,7 @@ func Convert(c *project.ServiceConfig) (*runconfig.Config, *runconfig.HostConfig Domainname: c.DomainName, User: c.User, Env: c.Environment.Slice(), - Cmd: runconfig.NewCommand(cmd...), + Cmd: runconfig.NewCommand(c.Command.Slice()...), Image: c.Image, Labels: c.Labels.MapParts(), ExposedPorts: ports, diff --git a/librcompose/project/types.go b/librcompose/project/types.go index 15ad7ba7..a066fbd7 100644 --- a/librcompose/project/types.go +++ b/librcompose/project/types.go @@ -44,7 +44,7 @@ type ServiceConfig struct { CapDrop []string `yaml:"cap_drop,omitempty"` CpuSet string `yaml:"cpu_set,omitempty"` CpuShares int64 `yaml:"cpu_shares,omitempty"` - Command SliceorString `yaml:"command,omitempty"` + Command Command `yaml:"command,omitempty"` Detach string `yaml:"detach,omitempty"` Devices []string `yaml:"devices,omitempty"` Dns Stringorslice `yaml:"dns,omitempty"` diff --git a/librcompose/project/types_yaml.go b/librcompose/project/types_yaml.go index 0c074593..2cb963e6 100644 --- a/librcompose/project/types_yaml.go +++ b/librcompose/project/types_yaml.go @@ -2,6 +2,8 @@ package project import ( "strings" + + "github.com/flynn/go-shlex" ) type Stringorslice struct { @@ -48,41 +50,42 @@ func NewStringorslice(parts ...string) Stringorslice { return Stringorslice{parts} } -type SliceorString struct { - parts string +type Command struct { + parts []string } -func (s SliceorString) MarshalYAML() (interface{}, error) { +func (s Command) MarshalYAML() (interface{}, error) { return s.parts, nil } -func (s *SliceorString) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (s *Command) UnmarshalYAML(unmarshal func(interface{}) error) error { var stringType string err := unmarshal(&stringType) if err == nil { - s.parts = stringType - return nil + s.parts, err = shlex.Split(stringType) + return err } var sliceType []string err = unmarshal(&sliceType) if err == nil { - s.parts = strings.Join(sliceType, " ") + s.parts = sliceType return nil } return err } -func (s *SliceorString) ToString() string { - if s == nil { - return "" - } +func (s *Command) ToString() string { + return strings.Join(s.parts, " ") +} + +func (s *Command) Slice() []string { return s.parts } -func NewSliceorString(parts string) SliceorString { - return SliceorString{parts} +func NewCommand(parts []string) Command { + return Command{parts} } type SliceorMap struct { @@ -102,36 +105,29 @@ func (s *SliceorMap) UnmarshalYAML(unmarshal func(interface{}) error) error { } var sliceType []string - var keyValueSlice []string var key string var value string err = unmarshal(&sliceType) - if err == nil { - mapType = make(map[string]string) - for _, slice := range sliceType { - slice = strings.Trim(slice, " ") - //split up key and value into []string based on separator - if strings.Contains(slice, "=") { - keyValueSlice = strings.Split(slice, "=") - } else if strings.Contains(slice, ":") { - keyValueSlice = strings.Split(slice, ":") - } else if strings.Count(slice, " ") == 1 && strings.Contains(slice, " ") { - keyValueSlice = strings.Split(slice, " ") - } else { - //if no clear separator, use slice as key and value - keyValueSlice[0] = slice - keyValueSlice[1] = slice - } - - key = keyValueSlice[0] + if err != nil { + return err + } + + mapType = make(map[string]string) + for _, slice := range sliceType { + slice = strings.TrimSpace(slice) + keyValueSlice := strings.SplitN(slice, "=", 2) + + key = keyValueSlice[0] + value = "" + if len(keyValueSlice) == 2 { value = keyValueSlice[1] - mapType[key] = value } - s.parts = mapType - return nil + + mapType[key] = value } - return err + s.parts = mapType + return nil } func (s *SliceorMap) MapParts() map[string]string { diff --git a/librcompose/project/types_yaml_test.go b/librcompose/project/types_yaml_test.go index 2573a4a0..a3e8b6f3 100644 --- a/librcompose/project/types_yaml_test.go +++ b/librcompose/project/types_yaml_test.go @@ -51,7 +51,7 @@ func TestSliceOrMapYaml(t *testing.T) { } type StructMaporslice struct { - Foo Maporslice + Foo MaporEqualSlice } func contains(list []string, item string) bool { diff --git a/librcompose/version.go b/librcompose/version.go new file mode 100644 index 00000000..6d8f2173 --- /dev/null +++ b/librcompose/version.go @@ -0,0 +1,3 @@ +package librcompose + +var VERSION = "0.0.0-dev" diff --git a/main.go b/main.go index bea16f1c..ff5b688d 100644 --- a/main.go +++ b/main.go @@ -7,14 +7,17 @@ import ( "github.com/Sirupsen/logrus" "github.com/codegangsta/cli" cliApp "github.com/rancherio/rancher-compose/app" + "github.com/rancherio/rancher-compose/librcompose" ) +var VERSION = "0.0.0-dev" + func main() { app := cli.NewApp() app.Name = "rancher-compose" app.Usage = "Docker-compose to Rancher" - app.Version = "0.1.0" + app.Version = librcompose.VERSION app.Author = "Rancher" app.Email = "" app.Before = func(c *cli.Context) error { @@ -118,7 +121,7 @@ func main() { Action: cliApp.ProjectDelete, Flags: []cli.Flag{ cli.BoolFlag{ - Name: "force", + Name: "force,f", Usage: "Allow deletion of all services", }, }, diff --git a/rancher/context.go b/rancher/context.go index d4d86be0..265ea633 100644 --- a/rancher/context.go +++ b/rancher/context.go @@ -32,9 +32,10 @@ type Context struct { } type RancherConfig struct { - Scale int `yaml:"scale,omitempty"` - LoadBalancerConfig *rancherClient.LoadBalancerConfig `yaml:"load_balancer_config,omitempty"` - ExternalIps []string `yaml:"external_ips,omitempty"` + Scale int `yaml:"scale,omitempty"` + LoadBalancerConfig *rancherClient.LoadBalancerConfig `yaml:"load_balancer_config,omitempty"` + ExternalIps []string `yaml:"external_ips,omitempty"` + HealthCheck *rancherClient.InstanceHealthCheck `yaml:"health_check,omitempty"` } func (c *Context) readComposeFile() error { diff --git a/rancher/service.go b/rancher/service.go index 82317771..93a2a1eb 100644 --- a/rancher/service.go +++ b/rancher/service.go @@ -20,8 +20,9 @@ import ( ) const ( - LB_IMAGE = "rancher/load-balancer" - DNS_IMAGE = "rancher/dns-service" + LB_IMAGE = "rancher/load-balancer-service" + DNS_IMAGE = "rancher/dns-service" + EXTERNAL_IMAGE = "rancher/external-service" ) var ( @@ -274,9 +275,17 @@ func (r *RancherService) createNormalService() (*rancherClient.Service, error) { }) } +func (r *RancherService) getHealthCheck() *rancherClient.InstanceHealthCheck { + if config, ok := r.context.RancherConfig[r.name]; ok { + return config.HealthCheck + } + + return nil +} + func (r *RancherService) getConfiguredScale() int { scale := 1 - if config, ok := r.context.RancherConfig[r.Name()]; ok { + if config, ok := r.context.RancherConfig[r.name]; ok { if config.Scale > 0 { scale = config.Scale } @@ -334,6 +343,10 @@ func (r *RancherService) getLinks() (map[string]interface{}, error) { if len(parts) == 2 { alias = parts[1] } + + name = strings.TrimSpace(name) + alias = strings.TrimSpace(alias) + linkedService, err := r.findExisting(name) if err != nil { return nil, err @@ -431,6 +444,8 @@ func (r *RancherService) createLaunchConfig(serviceConfig *project.ServiceConfig return result, err } + result.HealthCheck = r.getHealthCheck() + setupNetworking(serviceConfig.Net, &result) setupVolumesFrom(serviceConfig.VolumesFrom, &result) diff --git a/scripts/build b/scripts/build index f0ac691f..ff5fd416 100755 --- a/scripts/build +++ b/scripts/build @@ -19,11 +19,17 @@ if [ ! -e ${PACKAGE} ]; then ln -s $(pwd) $PACKAGE fi +VERSION=$(git tag -l --contains HEAD) +if [ -z "$VERSION" ]; then + VERSION=$(git rev-parse --short HEAD) +fi + echo export GOPATH=$GOPATH +echo VERSION=$VERSION -go build -o bin/${PROJECT} +go build -ldflags="-w -X github.com/rancherio/rancher-compose/librcompose.VERSION $VERSION" -o bin/${PROJECT} if [ -n "$BUILD_CROSS" ]; then mkdir -p dist/artifacts - gox -os="darwin linux windows" -arch="386 amd64 arm" -output="dist/artifacts/rancher-compose_{{.OS}}-{{.Arch}}" + gox -os="darwin linux windows" -arch="386 amd64 arm" -output="dist/artifacts/rancher-compose_{{.OS}}-{{.Arch}}" -ldflags="-w -X github.com/rancherio/rancher-compose/librcompose.VERSION $VERSION" fi diff --git a/tests/integration/cattletest/core/assets/health/rancher-compose.yml b/tests/integration/cattletest/core/assets/health/rancher-compose.yml new file mode 100644 index 00000000..f366d0ef --- /dev/null +++ b/tests/integration/cattletest/core/assets/health/rancher-compose.yml @@ -0,0 +1,8 @@ +web: + health_check: + port: 80 + interval: 2000 + unhealthy_threshold: 3 + request_line: "OPTIONS /ping HTTP/1.1\r\nHost:\\ www.example.com" + healthy_threshold: 2 + response_timeout: 2000 diff --git a/tests/integration/cattletest/core/assets/health/test.yml b/tests/integration/cattletest/core/assets/health/test.yml new file mode 100644 index 00000000..c318b7c1 --- /dev/null +++ b/tests/integration/cattletest/core/assets/health/test.yml @@ -0,0 +1,2 @@ +web: + image: nginx diff --git a/tests/integration/cattletest/core/assets/lb/docker-compose.yml b/tests/integration/cattletest/core/assets/lb/docker-compose.yml index 6d01b162..cfe2f230 100644 --- a/tests/integration/cattletest/core/assets/lb/docker-compose.yml +++ b/tests/integration/cattletest/core/assets/lb/docker-compose.yml @@ -1,5 +1,5 @@ lb: - image: rancher/load-balancer + image: rancher/load-balancer-service ports: - "80:80" links: diff --git a/tests/integration/cattletest/core/test_compose.py b/tests/integration/cattletest/core/test_compose.py index b73467b2..e1d56c5b 100644 --- a/tests/integration/cattletest/core/test_compose.py +++ b/tests/integration/cattletest/core/test_compose.py @@ -316,7 +316,7 @@ def test_restart_policies_on_failure_default(client, compose): def test_lb(client, compose): template = ''' lb: - image: rancher/load-balancer + image: rancher/load-balancer-service links: - web - web2 @@ -518,7 +518,7 @@ def test_dns_service(client, compose): def test_up_relink(client, compose): template = ''' lb: - image: rancher/load-balancer + image: rancher/load-balancer-service links: - web web: @@ -548,6 +548,58 @@ def test_up_relink(client, compose): assert consumed[0].name == 'web2' +def test_service_map_syntax(client, compose): + template = ''' +foo: + image: nginx + links: + web: alias +web: + image: nginx +''' + + project_name = create_project(compose, input=template) + project = find_one(client.list_environment, name=project_name) + foo = _get_service(project.services(), 'foo') + maps = client.list_serviceConsumeMap(serviceId=foo.id) + + assert len(maps) == 1 + assert maps[0].name == 'alias' + + +def test_service_link_with_space(client, compose): + template = ''' +foo: + image: nginx + links: + - "web: alias" +web: + image: nginx +''' + + project_name = create_project(compose, input=template) + project = find_one(client.list_environment, name=project_name) + foo = _get_service(project.services(), 'foo') + maps = client.list_serviceConsumeMap(serviceId=foo.id) + + assert len(maps) == 1 + assert maps[0].name == 'alias' + + +def test_healthchecks(client, compose): + project_name = create_project(compose, file='assets/health/test.yml') + + project = find_one(client.list_environment, name=project_name) + service = find_one(project.services) + + assert service.name == 'web' + assert service.launchConfig.healthCheck.port == 80 + assert service.launchConfig.healthCheck.interval == 2000 + assert service.launchConfig.healthCheck.unhealthyThreshold == 3 + assert service.launchConfig.healthCheck.requestLine == \ + "OPTIONS /ping HTTP/1.1\r\nHost:\\ www.example.com" + + def _get_service(services, name): service = None diff --git a/tests/integration/toenv.sh b/tests/integration/toenv.sh new file mode 100755 index 00000000..62cffc51 --- /dev/null +++ b/tests/integration/toenv.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +echo export RANCHER_URL=http://localhost:8080 +echo export RANCHER_ACCESS_KEY=$(grep 'access-key=' tox.ini | cut -f2 -d'=') +echo export RANCHER_SECRET_KEY=$(grep 'secret-key=' tox.ini | cut -f2 -d'=')