diff --git a/pkg/build/build.go b/pkg/build/build.go index d245f8121..a9dee8f39 100644 --- a/pkg/build/build.go +++ b/pkg/build/build.go @@ -174,16 +174,31 @@ func (b *Builder) setup() error { // start all services required for the build // that will get linked to the container. for _, service := range b.Build.Services { - image, ok := services[service] - if !ok { - return fmt.Errorf("Error: Invalid or unknown service %s", service) + + // Parse the name of the Docker image + // And then construct a fully qualified image name + owner, name, tag := parseImageName(service) + cname := fmt.Sprintf("%s/%s:%s", owner, name, tag) + + // Get the image info + img, err := b.dockerClient.Images.Inspect(cname) + if err != nil { + // Get the image if it doesn't exist + if err := b.dockerClient.Images.Pull(cname); err != nil { + return fmt.Errorf("Error: Unable to pull image %s", cname) + } + + img, err = b.dockerClient.Images.Inspect(cname) + if err != nil { + return fmt.Errorf("Error: Invalid or unknown image %s", cname) + } } // debugging - log.Infof("starting service container %s", image.Tag) + log.Infof("starting service container %s", cname) // Run the contianer - run, err := b.dockerClient.Containers.RunDaemonPorts(image.Tag, image.Ports...) + run, err := b.dockerClient.Containers.RunDaemonPorts(cname, img.Config.ExposedPorts) if err != nil { return err } @@ -201,7 +216,6 @@ func (b *Builder) setup() error { // Add the running service to the list b.services = append(b.services, info) - } if err := b.writeIdentifyFile(dir); err != nil { @@ -319,13 +333,12 @@ func (b *Builder) run() error { // link service containers for i, service := range b.services { - image, ok := services[b.Build.Services[i]] - if !ok { - continue // THIS SHOULD NEVER HAPPEN - } + // convert name of the image to a slug + _, name, _ := parseImageName(b.Build.Services[i]) + // link the service container to our // build container. - host.Links = append(host.Links, service.Name[1:]+":"+image.Name) + host.Links = append(host.Links, service.Name[1:]+":"+name) } // where are temp files going to go? diff --git a/pkg/build/build_test.go b/pkg/build/build_test.go index 9d26d732e..d6c0aa5ff 100644 --- a/pkg/build/build_test.go +++ b/pkg/build/build_test.go @@ -3,6 +3,7 @@ package build import ( "bytes" "encoding/json" + "fmt" "io/ioutil" "net/http" "net/http/httptest" @@ -108,20 +109,23 @@ func TestSetupEmptyImage(t *testing.T) { } } -// TestSetupUnknownService will test our ability to handle an -// unknown or unsupported service (i.e. mysql). -func TestSetupUnknownService(t *testing.T) { - b := Builder{} - b.Repo = &repo.Repo{} - b.Repo.Path = "git://github.com/drone/drone.git" - b.Build = &script.Build{} - b.Build.Image = "go1.2" - b.Build.Services = append(b.Build.Services, "not-found") +// TestSetupErrorInspectImage will test our ability to handle a +// failure when inspecting an image (i.e. bradrydzewski/mysql:latest), +// which should trigger a `docker pull`. +func TestSetupErrorInspectImage(t *testing.T) { + t.Skip() +} + +// TestSetupErrorPullImage will test our ability to handle a +// failure when pulling an image (i.e. bradrydzewski/mysql:latest) +func TestSetupErrorPullImage(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v1.9/images/bradrydzewski/mysql:5.5/json", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + }) - var got, want = b.setup(), "Error: Invalid or unknown service not-found" - if got == nil || got.Error() != want { - t.Errorf("Expected error %s, got %s", want, got) - } } // TestSetupErrorRunDaemonPorts will test our ability to handle a @@ -130,6 +134,11 @@ func TestSetupErrorRunDaemonPorts(t *testing.T) { setup() defer teardown() + mux.HandleFunc("/v1.9/images/bradrydzewski/mysql:5.5/json", func(w http.ResponseWriter, r *http.Request) { + data := []byte(`{"config": { "ExposedPorts": { "6379/tcp": {}}}}`) + w.Write(data) + }) + mux.HandleFunc("/v1.9/containers/create", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) }) @@ -155,6 +164,11 @@ func TestSetupErrorServiceInspect(t *testing.T) { setup() defer teardown() + mux.HandleFunc("/v1.9/images/bradrydzewski/mysql:5.5/json", func(w http.ResponseWriter, r *http.Request) { + data := []byte(`{"config": { "ExposedPorts": { "6379/tcp": {}}}}`) + w.Write(data) + }) + mux.HandleFunc("/v1.9/containers/create", func(w http.ResponseWriter, r *http.Request) { body := `{ "Id":"e90e34656806", "Warnings":[] }` w.Write([]byte(body)) @@ -188,12 +202,11 @@ func TestSetupErrorImagePull(t *testing.T) { setup() defer teardown() - mux.HandleFunc("/v1.9/images/bradrydzewski/go:1.2/json", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/v1.9/images/bradrydzewski/mysql:5.5/json", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) }) - mux.HandleFunc("/v1.9/images/create", func(w http.ResponseWriter, r *http.Request) { - // validate ?fromImage=bradrydzewski/go&tag=1.2 + mux.HandleFunc("/v1.9/images/create?fromImage=bradrydzewski/mysql&tag=5.5", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) }) @@ -202,10 +215,11 @@ func TestSetupErrorImagePull(t *testing.T) { b.Repo.Path = "git://github.com/drone/drone.git" b.Build = &script.Build{} b.Build.Image = "go1.2" + b.Build.Services = append(b.Build.Services, "mysql") b.dockerClient = client - var got, want = b.setup(), docker.ErrBadRequest - if got == nil || got != want { + var got, want = b.setup(), fmt.Errorf("Error: Unable to pull image bradrydzewski/mysql:5.5") + if got == nil || got.Error() != want.Error() { t.Errorf("Expected error %s, got %s", want, got) } } diff --git a/pkg/build/docker/container.go b/pkg/build/docker/container.go index 31f54b0e3..061c591e9 100644 --- a/pkg/build/docker/container.go +++ b/pkg/build/docker/container.go @@ -127,19 +127,18 @@ func (c *ContainerService) RunDaemon(conf *Config, host *HostConfig) (*Run, erro return run, err } -func (c *ContainerService) RunDaemonPorts(image string, ports ...string) (*Run, error) { +func (c *ContainerService) RunDaemonPorts(image string, ports map[Port]struct{}) (*Run, error) { // setup configuration config := Config{Image: image} - config.ExposedPorts = make(map[Port]struct{}) + config.ExposedPorts = ports // host configuration host := HostConfig{} host.PortBindings = make(map[Port][]PortBinding) // loop through and add ports - for _, port := range ports { - config.ExposedPorts[Port(port+"/tcp")] = struct{}{} - host.PortBindings[Port(port+"/tcp")] = []PortBinding{{HostIp: "127.0.0.1", HostPort: ""}} + for port, _ := range ports { + host.PortBindings[port] = []PortBinding{{HostIp: "127.0.0.1", HostPort: ""}} } //127.0.0.1::%s //map[3306/tcp:{}] map[3306/tcp:[{127.0.0.1 }]] diff --git a/pkg/build/util.go b/pkg/build/util.go index eb96530b3..3829e99ea 100644 --- a/pkg/build/util.go +++ b/pkg/build/util.go @@ -5,6 +5,7 @@ import ( "crypto/sha1" "fmt" "io" + "strings" ) // createUID is a helper function that will @@ -26,3 +27,57 @@ func createRandom() []byte { } return k } + +// list of service aliases and their full, canonical names +var defaultServices = map[string]string{ + "cassandra": "relateiq/cassandra:latest", + "couchdb": "bradrydzewski/couchdb:1.5", + "elasticsearch": "bradrydzewski/elasticsearch:0.90", + "memcached": "bradrydzewski/memcached", + "mongodb": "bradrydzewski/mongodb:2.4", + "mysql": "bradrydzewski/mysql:5.5", + "neo4j": "bradrydzewski/neo4j:1.9", + "postgres": "bradrydzewski/postgres:9.1", + "redis": "bradrydzewski/redis:2.8", + "rabbitmq": "bradrydzewski/rabbitmq:3.2", + "riak": "guillermo/riak:latest", + "zookeeper": "jplock/zookeeper:3.4.5", +} + +// parseImageName parses a Docker image name, in the format owner/name:tag, +// and returns each segment. +// +// If the owner is blank, it is assumed to be an official drone image, +// and will be prefixed with the appropriate owner name. +// +// If the tag is empty, it is assumed to be the latest version. +func parseImageName(image string) (owner, name, tag string) { + owner = "bradrydzewski" // this will eventually change to drone + name = image + tag = "latest" + + // first we check to see if the image name is an alias + // for a known service. + // + // TODO I'm not a huge fan of this code here. Maybe it + // should get handled when the yaml is parsed, and + // convert the image and service names in the yaml + // to fully qualified names? + if cname, ok := defaultServices[image]; ok { + name = cname + } + + parts := strings.Split(name, "/") + if len(parts) == 2 { + owner = parts[0] + name = parts[1] + } + + parts = strings.Split(name, ":") + if len(parts) == 2 { + name = parts[0] + tag = parts[1] + } + + return +} diff --git a/pkg/build/util_test.go b/pkg/build/util_test.go new file mode 100644 index 000000000..ae19b154c --- /dev/null +++ b/pkg/build/util_test.go @@ -0,0 +1,36 @@ +package build + +import "testing" + +func TestParseImageName(t *testing.T) { + images := []struct { + owner string + name string + tag string + cname string + }{ + // full image name with all 3 sections present + {"johnsmith", "redis", "2.8", "johnsmith/redis:2.8"}, + // image name with no tag specified + {"johnsmith", "redis", "latest", "johnsmith/redis"}, + // image name with no owner specified + {"bradrydzewski", "redis", "2.8", "redis:2.8"}, + // image name with ownly name specified + {"bradrydzewski", "redis2", "latest", "redis2"}, + // image name that is a known alias + {"relateiq", "cassandra", "latest", "cassandra"}, + } + + for _, img := range images { + owner, name, tag := parseImageName(img.cname) + if owner != img.owner { + t.Errorf("Expected image %s with owner %s, got %s", img.cname, img.owner, owner) + } + if name != img.name { + t.Errorf("Expected image %s with name %s, got %s", img.cname, img.name, name) + } + if tag != img.tag { + t.Errorf("Expected image %s with tag %s, got %s", img.cname, img.tag, tag) + } + } +}