diff --git a/.gitmodules b/.gitmodules
index 276d7af8e..b7d85ec9c 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,6 @@
[submodule "vendor/src/github.com/GeertJohan/go.rice"]
path = vendor/src/github.com/GeertJohan/go.rice
url = https://github.com/GeertJohan/go.rice.git
+[submodule "vendor/src/github.com/GeertJohan/rsrc"]
+ path = vendor/src/github.com/GeertJohan/rsrc
+ url = https://github.com/GeertJohan/rsrc.git
diff --git a/Dockerfile b/Dockerfile
index c03944e86..612d4cbbd 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -10,13 +10,15 @@ RUN wget https://go.googlecode.com/files/go1.2.src.tar.gz && tar zxvf go1.2.src.
ENV PATH $PATH:/go/bin:/gocode/bin
ENV GOPATH /gocode
+RUN go get github.com/tools/godep
+
RUN mkdir -p /gocode/src/github.com/drone
ADD . /gocode/src/github.com/drone/drone
WORKDIR /gocode/src/github.com/drone/drone
-RUN make deps
+RUN godep restore
RUN make
RUN make install
@@ -24,4 +26,4 @@ EXPOSE 80
ENTRYPOINT ["/usr/local/bin/droned"]
-CMD ["--port=:80", "--path=/var/lib/drone/drone.sqlite"]
+CMD ["--port=:80", "--datasource=/var/lib/drone/drone.sqlite"]
diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json
index 0ca983eb7..6ded8527f 100644
--- a/Godeps/Godeps.json
+++ b/Godeps/Godeps.json
@@ -174,6 +174,10 @@
"Comment": "1.5.0-168-g2abea07",
"Rev": "2abea075ec076abf0572d29bdb28ae7da64cfb7a"
},
+ {
+ "ImportPath": "github.com/stvp/flowdock",
+ "Rev": "50362abeabebf40b0f56a326d62f17e61a59302b"
+ },
{
"ImportPath": "launchpad.net/goyaml",
"Comment": "51",
diff --git a/Godeps/_workspace/src/github.com/stvp/flowdock/README.md b/Godeps/_workspace/src/github.com/stvp/flowdock/README.md
new file mode 100644
index 000000000..16da2b066
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/stvp/flowdock/README.md
@@ -0,0 +1,53 @@
+flowdock
+========
+
+A Flowdock client for Golang. See the [Flowdock API docs][api_docs] for more
+information on the individual attributes.
+
+[Go API Documentation][godocs]
+
+Examples
+--------
+
+You can set global defaults and use `flowdock.Inbox()` directly:
+
+```go
+import (
+ "github.com/stvp/flowdock"
+)
+
+func main() {
+ flowdock.Token = "732da505d0284d5b1d909b1a32426345"
+ flowdock.Source = "My Cool App"
+ flowdock.FromAddress = "cool@dudes.com"
+ // See API docs for more variables
+
+ go flowdock.Inbox("My subject", "My content goes here.")
+}
+```
+
+Or you can create a `flowdock.Client` to use different Flowdock message
+settings in the same app:
+
+```go
+import (
+ "github.com/stvp/flowdock"
+)
+
+func main() {
+ client := flowdock.Client{
+ Token: "732da505d0284d5b1d909b1a32426345",
+ Source: "App A",
+ FromAddress: "email@stovepipestudios.com",
+ FromName: "Client A",
+ ReplyTo: "app_a@stovepipestudios.com",
+ Tags: []string{"app_a"},
+ }
+
+ go client.Inbox("Subject", "Content")
+}
+```
+
+[api_docs]: https://www.flowdock.com/api/team-inbox
+[godocs]: http://godoc.org/github.com/stvp/flowdock
+
diff --git a/Godeps/_workspace/src/github.com/stvp/flowdock/flowdock.go b/Godeps/_workspace/src/github.com/stvp/flowdock/flowdock.go
new file mode 100644
index 000000000..ba0c7835c
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/stvp/flowdock/flowdock.go
@@ -0,0 +1,105 @@
+package flowdock
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+)
+
+const (
+ ENDPOINT = "https://api.flowdock.com/v1/messages/team_inbox/"
+)
+
+var (
+ // Required default client settings
+ Token = ""
+ Source = ""
+ FromAddress = ""
+
+ // Optional default client settings
+ FromName = ""
+ ReplyTo = ""
+ Project = ""
+ Link = ""
+ Tags = []string{}
+)
+
+type Client struct {
+ // Required
+ Token string
+ Source string
+ FromAddress string
+ Subject string
+ Content string
+
+ // Optional
+ FromName string
+ ReplyTo string
+ Project string
+ Link string
+ Tags []string
+}
+
+func (c *Client) Inbox(subject, content string) error {
+ return send(c.Token, c.Source, c.FromAddress, subject, content, c.FromName, c.ReplyTo, c.Project, c.Link, c.Tags)
+}
+
+func Inbox(subject, content string) error {
+ return send(Token, Source, FromAddress, subject, content, FromName, ReplyTo, Project, Link, Tags)
+}
+
+func send(token, source, fromAddress, subject, content, fromName, replyTo, project, link string, tags []string) error {
+ // Required validation
+ if len(token) == 0 {
+ return fmt.Errorf(`"Token" is required`)
+ }
+ if len(source) == 0 {
+ return fmt.Errorf(`"Source" is required`)
+ }
+ if len(fromAddress) == 0 {
+ return fmt.Errorf(`"FromAddress" is required`)
+ }
+ if len(subject) == 0 {
+ return fmt.Errorf(`"Subject" is required`)
+ }
+
+ // Build payload
+ payload := map[string]interface{}{
+ "source": source,
+ "from_address": fromAddress,
+ "subject": subject,
+ "content": content,
+ }
+ if len(fromName) > 0 {
+ payload["from_name"] = fromName
+ }
+ if len(replyTo) > 0 {
+ payload["reply_to"] = replyTo
+ }
+ if len(project) > 0 {
+ payload["project"] = project
+ }
+ if len(link) > 0 {
+ payload["link"] = link
+ }
+ if len(tags) > 0 {
+ payload["tags"] = tags
+ }
+ jsonPayload, err := json.Marshal(payload)
+ if err != nil {
+ return err
+ }
+
+ // Send to Flowdock
+ resp, err := http.Post(ENDPOINT+token, "application/json", bytes.NewReader(jsonPayload))
+ defer resp.Body.Close()
+
+ if resp.StatusCode == 200 {
+ return nil
+ } else {
+ bodyBytes, _ := ioutil.ReadAll(resp.Body)
+ return fmt.Errorf("Unexpected response from Flowdock: %s %s", resp.Status, string(bodyBytes))
+ }
+}
diff --git a/Makefile b/Makefile
index 93ed269bf..07ed3c4bc 100644
--- a/Makefile
+++ b/Makefile
@@ -94,4 +94,5 @@ godep:
go get github.com/tools/godep
rice:
- go install github.com/GeertJohan/go.rice/rice
+ go get github.com/GeertJohan/go.rice/rice
+ go build github.com/GeertJohan/go.rice/rice
diff --git a/cmd/droned/drone.go b/cmd/droned/drone.go
index c638357c5..189f6c9e5 100644
--- a/cmd/droned/drone.go
+++ b/cmd/droned/drone.go
@@ -227,6 +227,7 @@ func setupHandlers() {
// handlers for repository, commits and build details
m.Get("/:host/:owner/:name/commit/:commit/build/:label/out.txt", handler.RepoHandler(handler.BuildOut))
+ m.Get("/:host/:owner/:name/commit/:commit/build/:label/status.json", handler.PublicHandler(handler.BuildStatus))
m.Post("/:host/:owner/:name/commit/:commit/build/:label/rebuild", handler.RepoAdminHandler(rebuild.CommitRebuild))
m.Get("/:host/:owner/:name/commit/:commit/build/:label", handler.RepoHandler(handler.CommitShow))
m.Post("/:host/:owner/:name/commit/:commit/rebuild", handler.RepoAdminHandler(rebuild.CommitRebuild))
diff --git a/deb/drone/etc/default/drone b/deb/drone/etc/default/drone
index 0e9e67321..793d0a170 100644
--- a/deb/drone/etc/default/drone
+++ b/deb/drone/etc/default/drone
@@ -4,7 +4,6 @@
#
# -datasource="drone.sqlite":
# -driver="sqlite3":
-# -path="":
# -port=":8080":
# -workers="4":
#
diff --git a/pkg/build/build.go b/pkg/build/build.go
index 3a4a6ff55..694518f48 100644
--- a/pkg/build/build.go
+++ b/pkg/build/build.go
@@ -244,6 +244,8 @@ func (b *Builder) setup() error {
if err := b.dockerClient.Images.Pull(b.Build.Image); err != nil {
return err
}
+ } else if err != nil {
+ log.Errf("failed to inspect image %s", b.Build.Image)
}
// create the Docker image
@@ -439,7 +441,7 @@ func (b *Builder) writeDockerfile(dir string) error {
switch {
case strings.HasPrefix(b.Build.Image, "bradrydzewski/"),
strings.HasPrefix(b.Build.Image, "drone/"):
- // the default user for all official Drone imnage
+ // the default user for all official Drone images
// is the "ubuntu" user, since all build images
// inherit from the ubuntu cloud ISO
dockerfile.WriteUser("ubuntu")
diff --git a/pkg/build/git/git.go b/pkg/build/git/git.go
index 36e67f13e..74e711cc0 100644
--- a/pkg/build/git/git.go
+++ b/pkg/build/git/git.go
@@ -13,10 +13,7 @@ type Git struct {
Depth *int `yaml:"depth,omitempty"`
// The name of a directory to clone into.
- // TODO this still needs to be implemented. this field is
- // critical for forked Go projects, that need to clone
- // to a specific repository.
- Path string `yaml:"path,omitempty"`
+ Path *string `yaml:"path,omitempty"`
}
// GitDepth returns GitDefaultDepth
@@ -29,3 +26,14 @@ func GitDepth(g *Git) int {
}
return *g.Depth
}
+
+// GitPath returns the given default path
+// when Git.Path is empty.
+// GitPath returns Git.Path
+// when it is not empty.
+func GitPath(g *Git, defaultPath string) string {
+ if g == nil || g.Path == nil {
+ return defaultPath
+ }
+ return *g.Path
+}
diff --git a/pkg/handler/builds.go b/pkg/handler/builds.go
index be524a411..e06cdac16 100644
--- a/pkg/handler/builds.go
+++ b/pkg/handler/builds.go
@@ -7,6 +7,10 @@ import (
. "github.com/drone/drone/pkg/model"
)
+type BuildResult struct {
+ Status string
+}
+
// Returns the combined stdout / stderr for an individual Build.
func BuildOut(w http.ResponseWriter, r *http.Request, u *User, repo *Repo) error {
branch := r.FormValue("branch")
@@ -32,6 +36,33 @@ func BuildOut(w http.ResponseWriter, r *http.Request, u *User, repo *Repo) error
return RenderText(w, build.Stdout, http.StatusOK)
}
+// Returns the combined stdout / stderr for an individual Build.
+func BuildStatus(w http.ResponseWriter, r *http.Request, repo *Repo) error {
+ branch := r.FormValue("branch")
+ if branch == "" {
+ branch = "master"
+ }
+
+ hash := r.FormValue(":commit")
+ labl := r.FormValue(":label")
+
+ // get the commit from the database
+ commit, err := database.GetCommitBranchHash(branch, hash, repo.ID)
+ if err != nil {
+ return err
+ }
+
+ // get the build from the database
+ build, err := database.GetBuildSlug(labl, commit.ID)
+ if err != nil {
+ return err
+ }
+
+ build_result := BuildResult{build.Status}
+
+ return RenderJson(w, build_result)
+}
+
// Returns the gzipped stdout / stderr for an individual Build
func BuildOutGzip(w http.ResponseWriter, r *http.Request, u *User) error {
// TODO
diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go
index 40de6fd3c..7636ab3a2 100644
--- a/pkg/handler/handler.go
+++ b/pkg/handler/handler.go
@@ -64,10 +64,33 @@ func (h AdminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
+// PublicHandler wraps the default http.HandlerFunc to include
+// requested Repository in the method signature, in addition
+// to handling an error as the return value.
+type PublicHandler func(w http.ResponseWriter, r *http.Request, repo *Repo) error
+
+func (h PublicHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ // repository name from the URL parameters
+ hostParam := r.FormValue(":host")
+ userParam := r.FormValue(":owner")
+ nameParam := r.FormValue(":name")
+ repoName := fmt.Sprintf("%s/%s/%s", hostParam, userParam, nameParam)
+
+ repo, err := database.GetRepoSlug(repoName)
+ if err != nil || repo == nil {
+ RenderNotFound(w)
+ return
+ }
+
+ h(w, r, repo)
+ return
+}
+
// RepoHandler wraps the default http.HandlerFunc to include
// the currently authenticated User and requested Repository
// in the method signature, in addition to handling an error
// as the return value.
+
type RepoHandler func(w http.ResponseWriter, r *http.Request, user *User, repo *Repo) error
func (h RepoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
diff --git a/pkg/handler/repos.go b/pkg/handler/repos.go
index 93e83099a..629edb74c 100644
--- a/pkg/handler/repos.go
+++ b/pkg/handler/repos.go
@@ -326,6 +326,7 @@ func RepoUpdate(w http.ResponseWriter, r *http.Request, u *User, repo *Repo) err
return err
}
default:
+ repo.URL = r.FormValue("URL")
repo.Disabled = len(r.FormValue("Disabled")) == 0
repo.DisabledPullRequest = len(r.FormValue("DisabledPullRequest")) == 0
repo.Private = len(r.FormValue("Private")) > 0
diff --git a/pkg/plugin/deploy/git.go b/pkg/plugin/deploy/git.go
index 03df6f083..269d6d160 100644
--- a/pkg/plugin/deploy/git.go
+++ b/pkg/plugin/deploy/git.go
@@ -35,7 +35,7 @@ func (g *Git) Write(f *buildfile.Buildfile) {
// that need to be deployed to git remote.
f.WriteCmd(fmt.Sprintf("git add -A"))
f.WriteCmd(fmt.Sprintf("git commit -m 'add build artifacts'"))
- f.WriteCmd(fmt.Sprintf("git push deploy $COMMIT:%s --force", destinationBranch))
+ f.WriteCmd(fmt.Sprintf("git push deploy HEAD:%s --force", destinationBranch))
case false:
// otherwise we just do a standard git push
f.WriteCmd(fmt.Sprintf("git push deploy $COMMIT:%s", destinationBranch))
diff --git a/pkg/plugin/deploy/heroku.go b/pkg/plugin/deploy/heroku.go
index 4e4aa808d..a1050eed8 100644
--- a/pkg/plugin/deploy/heroku.go
+++ b/pkg/plugin/deploy/heroku.go
@@ -30,7 +30,7 @@ func (h *Heroku) Write(f *buildfile.Buildfile) {
// that need to be deployed to Heroku.
f.WriteCmd(fmt.Sprintf("git add -A"))
f.WriteCmd(fmt.Sprintf("git commit -m 'adding build artifacts'"))
- f.WriteCmd(fmt.Sprintf("git push heroku $COMMIT:master --force"))
+ f.WriteCmd(fmt.Sprintf("git push heroku HEAD:master --force"))
case false:
// otherwise we just do a standard git push
f.WriteCmd(fmt.Sprintf("git push heroku $COMMIT:master"))
diff --git a/pkg/plugin/deploy/tsuru.go b/pkg/plugin/deploy/tsuru.go
index 12f9a3c59..fa87e92e9 100644
--- a/pkg/plugin/deploy/tsuru.go
+++ b/pkg/plugin/deploy/tsuru.go
@@ -30,7 +30,7 @@ func (h *Tsuru) Write(f *buildfile.Buildfile) {
// that need to be deployed to Tsuru.
f.WriteCmd(fmt.Sprintf("git add -A"))
f.WriteCmd(fmt.Sprintf("git commit -m 'adding build artifacts'"))
- f.WriteCmd(fmt.Sprintf("git push tsuru $COMMIT:master --force"))
+ f.WriteCmd(fmt.Sprintf("git push tsuru HEAD:master --force"))
case false:
// otherwise we just do a standard git push
f.WriteCmd(fmt.Sprintf("git push tsuru $COMMIT:master"))
diff --git a/pkg/plugin/notify/flowdock.go b/pkg/plugin/notify/flowdock.go
new file mode 100644
index 000000000..5837fc3b2
--- /dev/null
+++ b/pkg/plugin/notify/flowdock.go
@@ -0,0 +1,90 @@
+package notify
+
+import (
+ "fmt"
+ "strings"
+ "net/url"
+ "github.com/stvp/flowdock"
+)
+
+const (
+ flowdockStartedSubject = "Building %s (%s)"
+ flowdockSuccessSubject = "Build: %s (%s) is SUCCESS"
+ flowdockFailureSubject = "Build: %s (%s) is FAILED"
+ flowdockMessage = "
%s
\nBuild: %s
\nResult: %s
\nAuthor: %s
Commit: %s
\nRepository Url: %s"
+ flowdockBuildOkEmail = "build+ok@flowdock.com"
+ flowdockBuildFailEmail = "build+fail@flowdock.com";
+)
+
+type Flowdock struct {
+ Token string `yaml:"token,omitempty"`
+ Source string `yaml:"source,omitempty"`
+ Tags string `yaml:"tags,omitempty"`
+ Started bool `yaml:"on_started,omitempty"`
+ Success bool `yaml:"on_success,omitempty"`
+ Failure bool `yaml:"on_failure,omitempty"`
+}
+
+func (f *Flowdock) Send(context *Context) error {
+ switch {
+ case context.Commit.Status == "Started" && f.Started:
+ return f.sendStarted(context)
+ case context.Commit.Status == "Success" && f.Success:
+ return f.sendSuccess(context)
+ case context.Commit.Status == "Failure" && f.Failure:
+ return f.sendFailure(context)
+ }
+
+ return nil
+}
+
+func (f *Flowdock) getBuildUrl(context *Context) string {
+ branchQuery := url.Values{}
+ if context.Commit.Branch != "" {
+ branchQuery.Set("branch", context.Commit.Branch)
+ }
+
+ return fmt.Sprintf("%s/%s/commit/%s?%s", context.Host, context.Repo.Slug, context.Commit.Hash, branchQuery.Encode())
+}
+
+func (f *Flowdock) getRepoUrl(context *Context) string {
+ return fmt.Sprintf("%s/%s", context.Host, context.Repo.Slug)
+}
+
+func (f *Flowdock) getMessage(context *Context) string {
+ buildUrl := fmt.Sprintf("%s", f.getBuildUrl(context), context.Commit.HashShort())
+ return fmt.Sprintf(flowdockMessage, context.Repo.Name, buildUrl, context.Commit.Status, context.Commit.Author, context.Commit.Message, f.getRepoUrl(context))
+}
+
+func (f *Flowdock) sendStarted(context *Context) error {
+ fromAddress := context.Commit.Author
+ subject := fmt.Sprintf(flowdockStartedSubject, context.Repo.Name, context.Commit.Branch)
+ msg := f.getMessage(context)
+ tags := strings.Split(f.Tags, ",")
+ return f.send(fromAddress, subject, msg, tags)
+}
+
+func (f *Flowdock) sendFailure(context *Context) error {
+ fromAddress := flowdockBuildFailEmail
+ tags := strings.Split(f.Tags, ",")
+ subject := fmt.Sprintf(flowdockFailureSubject, context.Repo.Name, context.Commit.Branch)
+ msg := f.getMessage(context)
+ return f.send(fromAddress, subject, msg, tags)
+}
+
+func (f *Flowdock) sendSuccess(context *Context) error {
+ fromAddress := flowdockBuildOkEmail
+ tags := strings.Split(f.Tags, ",")
+ subject := fmt.Sprintf(flowdockSuccessSubject, context.Repo.Name, context.Commit.Branch)
+ msg := f.getMessage(context)
+ return f.send(fromAddress, subject, msg, tags)
+}
+
+// helper function to send Flowdock requests
+func (f *Flowdock) send(fromAddress, subject, message string, tags []string) error {
+
+ c := flowdock.Client{Token: f.Token, Source: f.Source, FromName: "drone.io", FromAddress: fromAddress, Tags: tags}
+
+ go c.Inbox(subject, message)
+ return nil
+}
diff --git a/pkg/plugin/notify/notification.go b/pkg/plugin/notify/notification.go
index 31b2ce3ef..c0df619ef 100644
--- a/pkg/plugin/notify/notification.go
+++ b/pkg/plugin/notify/notification.go
@@ -28,11 +28,12 @@ type Sender interface {
// for notifying a user, or group of users,
// when their Build has completed.
type Notification struct {
- Email *Email `yaml:"email,omitempty"`
- Webhook *Webhook `yaml:"webhook,omitempty"`
- Hipchat *Hipchat `yaml:"hipchat,omitempty"`
- Irc *IRC `yaml:"irc,omitempty"`
- Slack *Slack `yaml:"slack,omitempty"`
+ Email *Email `yaml:"email,omitempty"`
+ Webhook *Webhook `yaml:"webhook,omitempty"`
+ Hipchat *Hipchat `yaml:"hipchat,omitempty"`
+ Irc *IRC `yaml:"irc,omitempty"`
+ Slack *Slack `yaml:"slack,omitempty"`
+ Flowdock *Flowdock `yaml:"flowdock,omitempty"`
}
func (n *Notification) Send(context *Context) error {
@@ -61,5 +62,10 @@ func (n *Notification) Send(context *Context) error {
n.Slack.Send(context)
}
+ // send flowdock notifications
+ if n.Flowdock != nil {
+ n.Flowdock.Send(context)
+ }
+
return nil
}
diff --git a/pkg/plugin/notify/slack.go b/pkg/plugin/notify/slack.go
index 009c45387..13109ed49 100644
--- a/pkg/plugin/notify/slack.go
+++ b/pkg/plugin/notify/slack.go
@@ -8,9 +8,9 @@ import (
const (
slackEndpoint = "https://%s.slack.com/services/hooks/incoming-webhook?token=%s"
- slackStartedMessage = "*Building* %s, commit <%s|%s>, author %s"
- slackSuccessMessage = "*Success* %s, commit <%s|%s>, author %s"
- slackFailureMessage = "*Failed* %s, commit <%s|%s>, author %s"
+ slackStartedMessage = "*Building* %s <%s|%s>, by %s:\n> %s"
+ slackSuccessMessage = "*Success* %s <%s|%s>, by %s:\n> %s"
+ slackFailureMessage = "*Failed* %s <%s|%s>, by %s:\n> %s"
)
type Slack struct {
@@ -47,7 +47,13 @@ func getBuildUrl(context *Context) string {
func getMessage(context *Context, message string) string {
url := getBuildUrl(context)
- return fmt.Sprintf(message, context.Repo.Name, url, context.Commit.HashShort(), context.Commit.Author)
+ return fmt.Sprintf(
+ message,
+ context.Repo.Name,
+ url,
+ context.Commit.HashShort(),
+ context.Commit.Author,
+ context.Commit.Message)
}
func (s *Slack) sendStarted(context *Context) error {
diff --git a/pkg/plugin/publish/docker.go b/pkg/plugin/publish/docker.go
new file mode 100644
index 000000000..f69ac1d4d
--- /dev/null
+++ b/pkg/plugin/publish/docker.go
@@ -0,0 +1,118 @@
+package publish
+
+import (
+ "fmt"
+ "strconv"
+
+ "github.com/drone/drone/pkg/build/buildfile"
+ "github.com/drone/drone/pkg/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/pkg/plugin/publish/docker_test.go b/pkg/plugin/publish/docker_test.go
new file mode 100644
index 000000000..01e10e03a
--- /dev/null
+++ b/pkg/plugin/publish/docker_test.go
@@ -0,0 +1,240 @@
+package publish
+
+import (
+ "strings"
+ "testing"
+
+ "gopkg.in/v1/yaml"
+ "github.com/drone/drone/pkg/build/buildfile"
+ "github.com/drone/drone/pkg/build/repo"
+)
+
+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/pkg/plugin/publish/publish.go b/pkg/plugin/publish/publish.go
index 546be583e..d9bdbf928 100644
--- a/pkg/plugin/publish/publish.go
+++ b/pkg/plugin/publish/publish.go
@@ -9,10 +9,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 `yaml:"npm,omitempty"`
+ S3 *S3 `yaml:"s3,omitempty"`
+ Swift *Swift `yaml:"swift,omitempty"`
+ PyPI *PyPI `yaml:"pypi,omitempty"`
+ NPM *NPM `yaml:"npm,omitempty"`
+ Docker *Docker `yaml:"docker,omitempty"`
}
func (p *Publish) Write(f *buildfile.Buildfile, r *repo.Repo) {
@@ -35,4 +36,9 @@ func (p *Publish) Write(f *buildfile.Buildfile, r *repo.Repo) {
if p.NPM != nil && (len(p.NPM.Branch) == 0 || (len(p.NPM.Branch) > 0 && r.Branch == p.NPM.Branch)) {
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)
+ }
}
diff --git a/pkg/queue/worker.go b/pkg/queue/worker.go
index 71adb15bd..53d6101bb 100644
--- a/pkg/queue/worker.go
+++ b/pkg/queue/worker.go
@@ -184,7 +184,7 @@ func (w *worker) runBuild(task *BuildTask, buildscript *script.Build, buf io.Wri
Branch: task.Commit.Branch,
Commit: task.Commit.Hash,
PR: task.Commit.PullRequest,
- Dir: filepath.Join("/var/cache/drone/src", task.Repo.Slug),
+ Dir: filepath.Join("/var/cache/drone/src", git.GitPath(buildscript.Git, task.Repo.Slug)),
Depth: git.GitDepth(buildscript.Git),
}
diff --git a/pkg/template/pages/admin_settings.html b/pkg/template/pages/admin_settings.html
index a6f825a7b..3c6c302d4 100644
--- a/pkg/template/pages/admin_settings.html
+++ b/pkg/template/pages/admin_settings.html
@@ -39,7 +39,7 @@