diff --git a/plugin/publish/docker.go b/plugin/publish/docker.go new file mode 100644 index 00000000..250248e8 --- /dev/null +++ b/plugin/publish/docker.go @@ -0,0 +1,126 @@ +package publish + +import ( + "fmt" + "strconv" + + "github.com/drone/drone/shared/build/buildfile" + "github.com/drone/drone/shared/build/repo" +) + +type Docker struct { + // The path to the dockerfile to create the image from. If the path is empty or no + // path is specified then the docker file will be built from the base directory. + Dockerfile string `yaml:"docker_file"` + + // Connection information for the docker server that will build the image + 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"` + + // Optional Arguments to allow finer-grained control of registry + // endpoints + RegistryLoginUrl string `yaml:"registry_login_url"` + ImageName string `yaml:"image_name"` + RegistryLogin bool `yaml:"registry_login"` + + // 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"` + CustomTag string `yaml:"custom_tag"` + Branch string `yaml:"branch"` +} + +// Write adds commands to the buildfile to do the following: +// 1. Install the docker client in the Drone container. +// 2. Build a docker image based on the dockerfile defined in the config. +// 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.DockerServer) == 0 || d.DockerServerPort == 0 || len(d.DockerVersion) == 0 || + len(d.ImageName) == 0 { + f.WriteCmdSilent(`echo -e "Docker Plugin: Missing argument(s)\n\n"`) + if len(d.DockerServer) == 0 { + f.WriteCmdSilent(`echo -e "\tdocker_server not defined in yaml"`) + } + if d.DockerServerPort == 0 { + f.WriteCmdSilent(`echo -e "\tdocker_port not defined in yaml"`) + } + if len(d.DockerVersion) == 0 { + f.WriteCmdSilent(`echo -e "\tdocker_version not defined in yaml"`) + } + if len(d.ImageName) == 0 { + f.WriteCmdSilent(`echo -e "\timage_name not defined in yaml"`) + } + return + } + + f.WriteCmd("sudo apt-get update") + + // 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\"") + f.WriteCmd("sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys " + + "36A1D7869245C8950F966E92D8576A8BA88D21E9") + f.WriteCmd("sudo apt-get update") + f.WriteCmd("sudo apt-get --yes install lxc-docker-" + d.DockerVersion) + + // Format our Build Server Endpoint + dockerServerUrl := d.DockerServer + ":" + strconv.Itoa(d.DockerServerPort) + + dockerPath := "." + if len(d.Dockerfile) != 0 { + dockerPath = fmt.Sprintf("- < %s", d.Dockerfile) + } + + // 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.CustomTag) > 0 { + imageTag = d.CustomTag + } else { + imageTag = "$(git rev-parse --short HEAD)" + } + f.WriteCmd(fmt.Sprintf("docker -H %s build -t %s:%s %s", dockerServerUrl, d.ImageName, imageTag, dockerPath)) + + // Login? + if d.RegistryLogin == true { + // Are we logging in to a custom Registry? + if len(d.RegistryLoginUrl) > 0 { + f.WriteCmdSilent(fmt.Sprintf("docker -H %s login -u %s -p %s -e %s %s", + dockerServerUrl, d.Username, d.Password, d.Email, d.RegistryLoginUrl)) + } else { + // Assume 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, d.ImageName, imageTag, d.ImageName)) + } + + f.WriteCmd(fmt.Sprintf("docker -H %s push %s", dockerServerUrl, d.ImageName)) + + // Delete the image from the docker server we built on. + if !d.KeepBuild { + f.WriteCmd(fmt.Sprintf("docker -H %s rmi %s:%s", + dockerServerUrl, d.ImageName, imageTag)) + if d.PushLatest { + f.WriteCmd(fmt.Sprintf("docker -H %s rmi %s:latest", + dockerServerUrl, d.ImageName)) + } + } +} diff --git a/plugin/publish/docker_test.go b/plugin/publish/docker_test.go new file mode 100644 index 00000000..b34169fd --- /dev/null +++ b/plugin/publish/docker_test.go @@ -0,0 +1,245 @@ +package publish + +import ( + "strings" + "testing" + + "github.com/drone/drone/shared/build/buildfile" + "github.com/drone/drone/shared/build/repo" + "gopkg.in/v1/yaml" +) + +type PublishToDrone struct { + Publish *Publish `yaml:"publish,omitempty"` +} + +func setUpWithDrone(input string) (string, error) { + var buildStruct PublishToDrone + err := yaml.Unmarshal([]byte(input), &buildStruct) + if err != nil { + return "", err + } + bf := buildfile.New() + buildStruct.Publish.Write(bf, &repo.Repo{Name: "name"}) + 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_login: false + image_name: registry/image +` + +func TestPrivateRegistryNoAuth(t *testing.T) { + response, err := setUpWithDrone(privateRegistryNoAuthYaml) + t.Log(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_login_url: https://registry:8000/v1/ + registry_login: true + username: username + password: password + email: email@example.com + image_name: registry/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: username/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: username/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: + dockerfile: file +` + +func TestMissingFields(t *testing.T) { + response, err := setUpWithDrone(missingFieldsYaml) + t.Log(missingFieldsYaml) + if err != nil { + t.Fatalf("Can't unmarshal script: %s\n\n", err.Error()) + } + if !strings.Contains(response, "Missing argument(s)") { + t.Fatalf("Response: " + response + " didn't contain missing arguments warning\n\n") + } +} + +var validYaml = ` +publish: + docker: + docker_file: file_path + docker_server: server + docker_port: 1000 + docker_version: 1.0 + username: user + password: password + email: email + image_name: user/image + push_latest: true + registry_login: true +` + +func TestValidYaml(t *testing.T) { + response, err := setUpWithDrone(validYaml) + t.Log(validYaml) + if err != nil { + t.Fatalf("Can't unmarshal script: %s\n\n", err.Error()) + } + + 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 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\n\n") + } + 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 user/image:"+ + "$(git rev-parse --short HEAD)") { + t.Fatalf("Response: " + response + " doesn't contain remove image command\n\n") + } +} + +var withoutDockerFileYaml = ` +publish: + docker: + docker_server: server + docker_port: 1000 + docker_version: 1.0 + image_name: user/image + username: user + password: password + email: email +` + +func TestWithoutDockerFile(t *testing.T) { + response, err := setUpWithDrone(withoutDockerFileYaml) + t.Log(withoutDockerFileYaml) + if err != nil { + t.Fatalf("Can't unmarshal script: %s\n\n", err.Error()) + } + + 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") + } +} diff --git a/plugin/publish/publish.go b/plugin/publish/publish.go index 60ed6bb5..cf369915 100644 --- a/plugin/publish/publish.go +++ b/plugin/publish/publish.go @@ -11,10 +11,11 @@ import ( // for publishing build artifacts when // a Build has succeeded type Publish struct { - S3 *S3 `yaml:"s3,omitempty"` - Swift *Swift `yaml:"swift,omitempty"` - PyPI *PyPI `yaml:"pypi,omitempty"` - NPM *npm.NPM `yaml:"npm,omitempty"` + S3 *S3 `yaml:"s3,omitempty"` + Swift *Swift `yaml:"swift,omitempty"` + PyPI *PyPI `yaml:"pypi,omitempty"` + NPM *npm.NPM `yaml:"npm,omitempty"` + Docker *Docker `yaml:"docker,omitempty"` } func (p *Publish) Write(f *buildfile.Buildfile, r *repo.Repo) { @@ -37,6 +38,11 @@ func (p *Publish) Write(f *buildfile.Buildfile, r *repo.Repo) { if p.NPM != nil && match(p.NPM.GetCondition(), r) { p.NPM.Write(f) } + + // Docker + if p.Docker != nil && (len(p.Docker.Branch) == 0 || (len(p.Docker.Branch) > 0 && r.Branch == p.Docker.Branch)) { + p.Docker.Write(f, r) + } } func match(c *condition.Condition, r *repo.Repo) bool {