diff --git a/pkg/plugin/publish/docker.go b/pkg/plugin/publish/docker.go index e1f63be8..46bb01a9 100644 --- a/pkg/plugin/publish/docker.go +++ b/pkg/plugin/publish/docker.go @@ -15,19 +15,33 @@ type Docker struct { Dockerfile string `yaml:"docker_file"` // Connection information for the docker server that will build the image - Server string `yaml:"docker_server"` - Port int `yaml:"docker_port"` + DockerServer string `yaml:"docker_server"` + DockerServerPort int `yaml:"docker_port"` // The Docker client version to download. This must match the docker version on the server DockerVersion string `yaml:"docker_version"` - RepoBaseName string `yaml:"repo_base_name"` + // Optional Arguments to allow finer-grained control of registry + // endpoints + RegistryHost string `yaml:"registry_host"` + RegistryProtocol string `yaml:"registry_protocol"` + RegistryPort int `yaml:"registry_port"` + RegistryLogin bool `yaml:"registry_login"` + RegistryLoginUri string `yaml:"registry_login_uri"` + + ImageName string `yaml:"image_name"` // Authentication credentials for index.docker.io Username string `yaml:"username"` Password string `yaml:"password"` Email string `yaml:"email"` + // Keep the build on the Docker host after pushing? + KeepBuild bool `yaml:"keep_build"` + // Do we want to override "latest" automatically with this build? + PushLatest bool `yaml:"push_latest"` + Branch string `yaml:"branch,omitempty"` + Tag string `yaml:"custom_tag"` } // Write adds commands to the buildfile to do the following: @@ -36,12 +50,15 @@ type Docker struct { // 3. Push that docker image to index.docker.io. // 4. Delete the docker image on the server it was build on so we conserve disk space. func (d *Docker) Write(f *buildfile.Buildfile, r *repo.Repo) { - if len(d.Email) == 0 || len(d.Server) == 0 || d.Port == 0 || len(d.DockerVersion) == 0 || - len(d.RepoBaseName) == 0 || len(d.Username) == 0 || len(d.Password) == 0 { + if len(d.DockerServer) == 0 || d.DockerServerPort == 0 || len(d.DockerVersion) == 0 || + len(d.ImageName) == 0 { f.WriteCmdSilent(`echo "Docker Plugin: Missing argument(s)"`) return } + // Ensure correct apt-get has the https method-driver as per (http://askubuntu.com/questions/165676/) + f.WriteCmd("sudo apt-get install apt-transport-https") + // Install Docker on the container f.WriteCmd("sudo sh -c \"echo deb https://get.docker.io/ubuntu docker main\\ > " + "/etc/apt/sources.list.d/docker.list\"") @@ -50,27 +67,83 @@ func (d *Docker) Write(f *buildfile.Buildfile, r *repo.Repo) { f.WriteCmd("sudo apt-get update") f.WriteCmd("sudo apt-get --yes install lxc-docker-" + d.DockerVersion) - dockerServerUrl := d.Server + ":" + strconv.Itoa(d.Port) - splitRepoName := strings.Split(r.Name, "/") - dockerRepo := d.RepoBaseName + "/" + splitRepoName[len(splitRepoName)-1] + // Format our Build Server Endpoint + dockerServerUrl := d.DockerServer + ":" + strconv.Itoa(d.DockerServerPort) + + // Construct Image BaseName + // e.g. "docker.mycompany.com/myimage" for private registries + // "myuser/myimage" for index.docker.io + imageBaseName := "" + if len(d.RegistryHost) > 0 { + imageBaseName = fmt.Sprintf("%s/%s",d.RegistryHost,d.ImageName) + } else { + imageBaseName = fmt.Sprintf("%s/%s",d.Username,d.ImageName) + } + + registryLoginEndpoint := "" + + // Gather information to build our Registry Endpoint for private registries + if len(d.RegistryHost) > 0 { + // Set Protocol + if len(d.RegistryProtocol) > 0 { + registryLoginEndpoint = fmt.Sprintf("%s://%s", d.RegistryProtocol,d.RegistryHost) + } else { + registryLoginEndpoint = fmt.Sprintf("http://%s", d.RegistryHost) + } + // Set Port + if d.RegistryPort > 0 { + registryLoginEndpoint = fmt.Sprintf("%s:%d",registryLoginEndpoint,d.RegistryPort) + } + // Set Login URI + if len(d.RegistryLoginUri) > 0 { + registryLoginEndpoint = fmt.Sprintf("%s/%s",registryLoginEndpoint,strings.TrimPrefix(d.RegistryLoginUri,"/")) + } else { + registryLoginEndpoint = fmt.Sprintf("%s/v1/",registryLoginEndpoint) + } + } + + //splitRepoName := strings.Split(r.Name, "/") + //dockerRepo := d.ImageName + "/" + splitRepoName[len(splitRepoName)-1] dockerPath := "." if len(d.Dockerfile) != 0 { dockerPath = fmt.Sprintf("- < %s", d.Dockerfile) } - // Run the command commands to build and deploy the image. Note that we both create a new image - // tagged with the git hash as well as update the "latest" image. - f.WriteCmd(fmt.Sprintf("docker -H %s build -t %s %s", dockerServerUrl, dockerRepo, dockerPath)) - f.WriteCmd(fmt.Sprintf("docker -H %s build -t %s:$(git rev-parse --short HEAD) %s", - dockerServerUrl, dockerRepo, dockerPath)) + // Run the command commands to build and deploy the image. + // Are we setting a custom tag, or do we use the git hash? + imageTag := "" + if len(d.Tag) > 0 { + imageTag = d.Tag + } else { + imageTag = "$(git rev-parse --short HEAD)" + } + f.WriteCmd(fmt.Sprintf("docker -H %s build -t %s:%s %s", dockerServerUrl, imageBaseName, imageTag, dockerPath)) - // Login and push to index.docker.io - f.WriteCmdSilent(fmt.Sprintf("docker -H %s login -u %s -p %s -e %s", - dockerServerUrl, d.Username, d.Password, d.Email)) - f.WriteCmd(fmt.Sprintf("docker -H %s push %s", dockerServerUrl, dockerRepo)) + // Login? + if len(d.RegistryHost) > 0 && d.RegistryLogin == true { + f.WriteCmdSilent(fmt.Sprintf("docker -H %s login -u %s -p %s -e %s %s", + dockerServerUrl, d.Username, d.Password, d.Email, registryLoginEndpoint)) + } else if len(d.RegistryHost) == 0 { + // Assume that because no private registry is specified it requires auth + // for index.docker.io + f.WriteCmdSilent(fmt.Sprintf("docker -H %s login -u %s -p %s -e %s", + dockerServerUrl, d.Username, d.Password, d.Email)) + } + + // Are we overriding the "latest" tag? + if d.PushLatest { + f.WriteCmd(fmt.Sprintf("docker -H %s tag %s:%s %s:latest", + dockerServerUrl, imageBaseName, imageTag, imageBaseName)) + } + + f.WriteCmd(fmt.Sprintf("docker -H %s push %s", dockerServerUrl, imageBaseName)) // Delete the image from the docker server we built on. - f.WriteCmd(fmt.Sprintf("docker -H %s rmi %s:$(git rev-parse --short HEAD)", - dockerServerUrl, dockerRepo)) + if ! d.KeepBuild { + f.WriteCmd(fmt.Sprintf("docker -H %s rmi %s:%s", + dockerServerUrl, imageBaseName, imageTag)) + f.WriteCmd(fmt.Sprintf("docker -H %s rmi %s:latest", + dockerServerUrl, imageBaseName)) + } } diff --git a/pkg/plugin/publish/docker_test.go b/pkg/plugin/publish/docker_test.go index d319f882..c18495aa 100644 --- a/pkg/plugin/publish/docker_test.go +++ b/pkg/plugin/publish/docker_test.go @@ -24,6 +24,143 @@ func setUpWithDrone(input string) (string, error) { return bf.String(), err } + +// Private Registry Test (no auth) +var privateRegistryNoAuthYaml = ` +publish: + docker: + dockerfile: file_path + docker_server: server + docker_port: 1000 + docker_version: 1.0 + registry_host: registry + registry_login: false + image_name: image +` +func TestPrivateRegistryNoAuth(t *testing.T) { + response, err := setUpWithDrone(privateRegistryNoAuthYaml) + if err != nil { + t.Fatalf("Can't unmarshal script: %s\n\n", err.Error()) + } + if !strings.Contains(response, "docker -H server:1000 build -t registry/image:$(git rev-parse --short HEAD)") { + t.Fatalf("Response: " + response + " doesn't contain registry in image-names: expected registry/image\n\n") + } +} + +// Private Registry Test (with auth) +var privateRegistryAuthYaml = ` +publish: + docker: + dockerfile: file_path + docker_server: server + docker_port: 1000 + docker_version: 1.0 + registry_host: registry + registry_protocol: https + registry_port: 8000 + registry_login: true + username: username + password: password + email: email@example.com + image_name: image +` +func TestPrivateRegistryAuth(t *testing.T) { + response, err := setUpWithDrone(privateRegistryAuthYaml) + t.Log(privateRegistryAuthYaml) + if err != nil { + t.Fatalf("Can't unmarshal script: %s\n\n", err.Error()) + } + if !strings.Contains(response, "docker -H server:1000 login -u username -p password -e email@example.com https://registry:8000/v1/") { + t.Log("\n\n\n\ndocker -H server:1000 login -u username -p xxxxxxxx -e email@example.com https://registry:8000/v1/\n\n\n\n") + t.Fatalf("Response: " + response + " doesn't contain private registry login\n\n") + } + if !strings.Contains(response, "docker -H server:1000 build -t registry/image:$(git rev-parse --short HEAD) .") { + t.Log("docker -H server:1000 build -t registry/image:$(git rev-parse --short HEAD) .") + t.Fatalf("Response: " + response + " doesn't contain registry in image-names\n\n") + } +} + +// Override "latest" Test +var overrideLatestTagYaml = ` +publish: + docker: + docker_server: server + docker_port: 1000 + docker_version: 1.0 + username: username + password: password + email: email@example.com + image_name: image + push_latest: true +` +func TestOverrideLatestTag(t *testing.T) { + response, err := setUpWithDrone(overrideLatestTagYaml) + t.Log(overrideLatestTagYaml) + if err != nil { + t.Fatalf("Can't unmarshal script: %s\n\n", err.Error()) + } + if !strings.Contains(response, "docker -H server:1000 build -t username/image:$(git rev-parse --short HEAD) .") { + t.Fatalf("Response: " + response + " doesn't contain the git-ref tagged image\n\n") + } + if !strings.Contains(response, "docker -H server:1000 tag username/image:$(git rev-parse --short HEAD) username/image:latest") { + t.Fatalf("Response: " + response + " doesn't contain 'latest' tag command\n\n") + } +} + +// Keep builds Test +var keepBuildsYaml = ` +publish: + docker: + docker_server: server + docker_port: 1000 + docker_version: 1.0 + keep_build: true + username: username + password: password + email: email@example.com + image_name: image +` +func TestKeepBuilds(t *testing.T) { + response, err := setUpWithDrone(keepBuildsYaml) + t.Log(keepBuildsYaml) + if err != nil { + t.Fatalf("Can't unmarshal script: %s\n\n", err.Error()) + } + if strings.Contains(response, "docker -H server:1000 rmi") { + t.Fatalf("Response: " + response + " incorrectly instructs the docker server to remove the builds when it shouldn't\n\n") + } +} + +// Custom Tag test +var customTagYaml = ` +publish: + docker: + docker_server: server + docker_port: 1000 + docker_version: 1.0 + custom_tag: release-0.1 + username: username + password: password + email: email@example.com + image_name: image +` +func TestCustomTag(t *testing.T) { + response, err := setUpWithDrone(customTagYaml) + t.Log(customTagYaml) + if err != nil { + t.Fatalf("Can't unmarshal script: %s\n", err.Error()) + } + if strings.Contains(response, "$(git rev-parse --short HEAD)") { + t.Fatalf("Response: " + response + " is tagging images from git-refs when it should use a custom tag\n\n") + } + if !strings.Contains(response, "docker -H server:1000 build -t username/image:release-0.1") { + t.Fatalf("Response: " + response + " isn't tagging images using our custom tag\n\n") + } + if !strings.Contains(response, "docker -H server:1000 push username/image"){ + t.Fatalf("Response: " + response + " doesn't push the custom tagged image\n\n") + } +} + var missingFieldsYaml = ` publish: docker: @@ -32,11 +169,12 @@ publish: func TestMissingFields(t *testing.T) { response, err := setUpWithDrone(missingFieldsYaml) + t.Log(missingFieldsYaml) if err != nil { - t.Fatalf("Can't unmarshal script: %s", err.Error()) + t.Fatalf("Can't unmarshal script: %s\n\n", err.Error()) } - if !strings.Contains(response, "echo \"Docker Plugin: Missing argument(s)") { - t.Fatalf("Response: " + response + " didn't contain missing arguments warning") + if !strings.Contains(response, "echo \"Docker Plugin: Missing argument(s)\"") { + t.Fatalf("Response: " + response + " didn't contain missing arguments warning\n\n") } } @@ -51,30 +189,32 @@ publish: username: user password: password email: email + image_name: image + push_latest: true ` func TestValidYaml(t *testing.T) { response, err := setUpWithDrone(validYaml) + t.Log(validYaml) if err != nil { - t.Fatalf("Can't unmarshal script: %s", err.Error()) + t.Fatalf("Can't unmarshal script: %s\n\n", err.Error()) } - if !strings.Contains(response, "docker -H server:1000 build -t base_repo/name - <") { - t.Fatalf("Response: " + response + " doesn't contain build command for latest") + if !strings.Contains(response, "docker -H server:1000 tag user/image:$(git rev-parse --short HEAD) user/image:latest") { + t.Fatalf("Response: " + response + " doesn't contain tag command for latest\n\n") } - if !strings.Contains(response, "docker -H server:1000 build -t base_repo/name" + - ":$(git rev-parse --short HEAD)") { - t.Fatalf("Response: " + response + "doesn't contain build command for commit hash") + if !strings.Contains(response, "docker -H server:1000 build -t user/image:$(git rev-parse --short HEAD) - <") { + t.Fatalf("Response: " + response + "doesn't contain build command for commit hash\n\n") } if !strings.Contains(response, "docker -H server:1000 login -u user -p password -e email") { - t.Fatalf("Response: " + response + " doesn't contain login command") + t.Fatalf("Response: " + response + " doesn't contain login command\n\n") } - if !strings.Contains(response, "docker -H server:1000 push base_repo/name") { - t.Fatalf("Response: " + response + " doesn't contain push command") + if !strings.Contains(response, "docker -H server:1000 push user/image") { + t.Fatalf("Response: " + response + " doesn't contain push command\n\n") } - if !strings.Contains(response, "docker -H server:1000 rmi base_repo/name:" + + if !strings.Contains(response, "docker -H server:1000 rmi user/image:" + "$(git rev-parse --short HEAD)") { - t.Fatalf("Response: " + response + " doesn't contain remove image command") + t.Fatalf("Response: " + response + " doesn't contain remove image command\n\n") } } @@ -84,7 +224,7 @@ publish: docker_server: server docker_port: 1000 docker_version: 1.0 - repo_base_name: base_repo + image_name: image username: user password: password email: email @@ -93,10 +233,10 @@ publish: func TestWithoutDockerFile(t *testing.T) { response, err := setUpWithDrone(withoutDockerFileYaml) if err != nil { - t.Fatalf("Can't unmarshal script: %s", err.Error()) + t.Fatalf("Can't unmarshal script: %s\n\n", err.Error()) } - if !strings.Contains(response, "docker -H server:1000 build -t base_repo/name .") { - t.Fatalf("Response: " + response + " doesn't contain build command") + if !strings.Contains(response, "docker -H server:1000 build -t user/image:$(git rev-parse --short HEAD) .") { + t.Fatalf("Response: " + response + " doesn't contain build command\n\n") } }