func TestGetSettings(t *testing.T) { Setup() defer Teardown() // even though no settings exist yet, we should // not see an error since we supress the msg settings, err := database.GetSettings() //if err != nil { // t.Error(err) //} // add some settings //settings := &modelSettings{} settings.Scheme = "https" settings.Domain = "foo.com" settings.BitbucketKey = "bitbucketkey" settings.BitbucketSecret = "bitbucketsecret" settings.GitHubKey = "githubkey" settings.GitHubSecret = "githubsecret" settings.SmtpAddress = "*****@*****.**" settings.SmtpServer = "0.0.0.0" settings.SmtpUsername = "******" settings.SmtpPassword = "******" // save the updated settings if err := database.SaveSettings(settings); err != nil { t.Error(err) } // re-retrieve the settings post-save settings, err = database.GetSettings() if err != nil { t.Error(err) } if settings.ID != 1 { t.Errorf("Exepected ID %d, got %d", 1, settings.ID) } if settings.Scheme != "https" { t.Errorf("Exepected Scheme %s, got %s", "https", settings.Scheme) } if settings.Domain != "foo.com" { t.Errorf("Exepected Domain %s, got %s", "foo.com", settings.Domain) } // Verify caching works and is threadsafe settingsA, _ := database.GetSettings() settingsB, _ := database.GetSettings() settingsA.Domain = "foo.bar.baz" if settingsA.Domain == settingsB.Domain { t.Errorf("Exepected Domain ThreadSafe and unchanged") } }
// updateGitHubStatus is a helper function that will send // the build status to GitHub using the Status API. // see https://github.com/blog/1227-commit-status-api func updateGitHubStatus(repo *Repo, commit *Commit) error { // convert from drone status to github status var message, status string switch commit.Status { case "Success": status = "success" message = "The build succeeded on drone.io" case "Failure": status = "failure" message = "The build failed on drone.io" case "Pending": status = "pending" message = "The build is pending on drone.io" default: status = "error" message = "The build errored on drone.io" } // get the system settings settings, _ := database.GetSettings() // get the user from the database // since we need his / her GitHub token user, err := database.GetUser(repo.UserID) if err != nil { return err } client := github.New(user.GithubToken) return client.Repos.CreateStatus(repo.Owner, repo.Name, status, settings.URL().String(), message, commit.Hash) }
// Send sends an email message. func Send(msg *Message) error { // retieve the system settings from the database // so that we can get the SMTP details. s, err := database.GetSettings() if err != nil { log.Print(err) return err } // set the FROM address msg.Sender = s.SmtpAddress // format the raw email message body body := fmt.Sprintf(emailTemplate, msg.Sender, msg.To, msg.Subject, msg.Body) var auth smtp.Auth if len(s.SmtpUsername) > 0 { auth = smtp.PlainAuth("", s.SmtpUsername, s.SmtpPassword, s.SmtpServer) } addr := fmt.Sprintf("%s:%s", s.SmtpServer, s.SmtpPort) err = smtp.SendMail(addr, auth, msg.Sender, []string{msg.To}, []byte(body)) if err != nil { log.Print(err) return err } return nil }
// Return an HTML form for the User to login. func Login(w http.ResponseWriter, r *http.Request) error { var settings, _ = database.GetSettings() data := struct { Settings *Settings }{settings} return RenderTemplate(w, "login.html", &data) }
// Return an HTML form for the User to signup. func SignUp(w http.ResponseWriter, r *http.Request) error { var settings, _ = database.GetSettings() if settings == nil || !settings.OpenInvitations { http.Redirect(w, r, "/login", http.StatusSeeOther) return nil } return RenderTemplate(w, "signup.html", nil) }
// execute will execute the build task and persist // the results to the datastore. func (w *worker) execute(task *BuildTask) error { // we need to be sure that we can recover // from any sort panic that could occur // to avoid brining down the entire application defer func() { if e := recover(); e != nil { task.Build.Finished = time.Now().UTC() task.Commit.Finished = time.Now().UTC() task.Build.Duration = task.Build.Finished.Unix() - task.Build.Started.Unix() task.Commit.Duration = task.Build.Finished.Unix() - task.Build.Started.Unix() task.Commit.Status = "Error" task.Build.Status = "Error" database.SaveBuild(task.Build) database.SaveCommit(task.Commit) } }() // update commit and build status task.Commit.Status = "Started" task.Build.Status = "Started" task.Build.Started = time.Now().UTC() task.Commit.Started = time.Now().UTC() // persist the commit to the database if err := database.SaveCommit(task.Commit); err != nil { return err } // persist the build to the database if err := database.SaveBuild(task.Build); err != nil { return err } // get settings settings, _ := database.GetSettings() // notification context context := ¬ify.Context{ Repo: task.Repo, Commit: task.Commit, Host: settings.URL().String(), } // send all "started" notifications if task.Script.Notifications != nil { task.Script.Notifications.Send(context) } // Send "started" notification to Github if err := updateGitHubStatus(task.Repo, task.Commit); err != nil { log.Printf("error updating github status: %s\n", err.Error()) } // make sure a channel exists for the repository, // the commit, and the commit output (TODO) reposlug := fmt.Sprintf("%s/%s/%s", task.Repo.Host, task.Repo.Owner, task.Repo.Name) commitslug := fmt.Sprintf("%s/%s/%s/commit/%s", task.Repo.Host, task.Repo.Owner, task.Repo.Name, task.Commit.Hash) consoleslug := fmt.Sprintf("%s/%s/%s/commit/%s/builds/%s", task.Repo.Host, task.Repo.Owner, task.Repo.Name, task.Commit.Hash, task.Build.Slug) channel.Create(reposlug) channel.Create(commitslug) channel.CreateStream(consoleslug) // notify the channels that the commit and build started channel.SendJSON(reposlug, task.Commit) channel.SendJSON(commitslug, task.Build) var buf = &bufferWrapper{channel: consoleslug} // append private parameters to the environment // variable section of the .drone.yml file, iff // this is not a pull request (for security purposes) if task.Repo.Params != nil && len(task.Commit.PullRequest) == 0 { for k, v := range task.Repo.Params { task.Script.Env = append(task.Script.Env, k+"="+v) } } defer func() { // update the status of the commit using the // GitHub status API. if err := updateGitHubStatus(task.Repo, task.Commit); err != nil { log.Printf("error updating github status: %s\n", err.Error()) } }() // execute the build passed, buildErr := w.runBuild(task, buf) task.Build.Finished = time.Now().UTC() task.Commit.Finished = time.Now().UTC() task.Build.Duration = task.Build.Finished.UnixNano() - task.Build.Started.UnixNano() task.Commit.Duration = task.Build.Finished.UnixNano() - task.Build.Started.UnixNano() task.Commit.Status = "Success" task.Build.Status = "Success" task.Build.Stdout = buf.buf.String() // if exit code != 0 set to failure if passed { task.Commit.Status = "Failure" task.Build.Status = "Failure" if buildErr != nil && task.Build.Stdout == "" { // TODO: If you wanted to have very friendly error messages, you could do that here task.Build.Stdout = buildErr.Error() + "\n" } } // persist the build to the database if err := database.SaveBuild(task.Build); err != nil { return err } // persist the commit to the database if err := database.SaveCommit(task.Commit); err != nil { return err } // notify the channels that the commit and build finished channel.SendJSON(reposlug, task.Commit) channel.SendJSON(commitslug, task.Build) channel.Close(consoleslug) // send all "finished" notifications if task.Script.Notifications != nil { task.Script.Notifications.Send(context) } return nil }
// execute will execute the build task and persist // the results to the datastore. func (b *BuildTask) execute() error { // we need to be sure that we can recover // from any sort panic that could occur // to avoid brining down the entire application defer func() { if e := recover(); e != nil { b.Build.Finished = time.Now().UTC() b.Commit.Finished = time.Now().UTC() b.Build.Duration = b.Build.Finished.Unix() - b.Build.Started.Unix() b.Commit.Duration = b.Build.Finished.Unix() - b.Build.Started.Unix() b.Commit.Status = "Error" b.Build.Status = "Error" database.SaveBuild(b.Build) database.SaveCommit(b.Commit) } }() // update commit and build status b.Commit.Status = "Started" b.Build.Status = "Started" b.Build.Started = time.Now().UTC() b.Commit.Started = time.Now().UTC() // persist the commit to the database if err := database.SaveCommit(b.Commit); err != nil { return err } // persist the build to the database if err := database.SaveBuild(b.Build); err != nil { return err } // get settings settings, _ := database.GetSettings() // notification context context := ¬ification.Context{ Repo: b.Repo, Commit: b.Commit, Host: settings.URL().String(), } // send all "started" notifications if b.Script.Notifications != nil { b.Script.Notifications.Send(context) } // make sure a channel exists for the repository, // the commit, and the commit output (TODO) reposlug := fmt.Sprintf("%s/%s/%s", b.Repo.Host, b.Repo.Owner, b.Repo.Name) commitslug := fmt.Sprintf("%s/%s/%s/commit/%s", b.Repo.Host, b.Repo.Owner, b.Repo.Name, b.Commit.Hash) consoleslug := fmt.Sprintf("%s/%s/%s/commit/%s/builds/%s", b.Repo.Host, b.Repo.Owner, b.Repo.Name, b.Commit.Hash, b.Build.Slug) channel.Create(reposlug) channel.Create(commitslug) channel.CreateStream(consoleslug) // notify the channels that the commit and build started channel.SendJSON(reposlug, b.Commit) channel.SendJSON(commitslug, b.Build) var buf = &bufferWrapper{channel: consoleslug} // append private parameters to the environment // variable section of the .drone.yml file if b.Repo.Params != nil { for k, v := range b.Repo.Params { b.Script.Env = append(b.Script.Env, k+"="+v) } } // execute the build builder := bldr.Builder{} builder.Build = b.Script builder.Repo = &r.Repo{Path: b.Repo.URL, Branch: b.Commit.Branch, Commit: b.Commit.Hash, PR: b.Commit.PullRequest, Dir: filepath.Join("/var/cache/drone/src", b.Repo.Slug)} builder.Key = []byte(b.Repo.PrivateKey) builder.Stdout = buf builder.Timeout = 300 * time.Minute defer func() { // update the status of the commit using the // GitHub status API. if err := updateGitHubStatus(b.Repo, b.Commit); err != nil { log.Printf("error updating github status: %s\n", err.Error()) } }() buildErr := builder.Run() b.Build.Finished = time.Now().UTC() b.Commit.Finished = time.Now().UTC() b.Build.Duration = b.Build.Finished.UnixNano() - b.Build.Started.UnixNano() b.Commit.Duration = b.Build.Finished.UnixNano() - b.Build.Started.UnixNano() b.Commit.Status = "Success" b.Build.Status = "Success" b.Build.Stdout = buf.buf.String() // if exit code != 0 set to failure if builder.BuildState == nil || builder.BuildState.ExitCode != 0 { b.Commit.Status = "Failure" b.Build.Status = "Failure" if buildErr != nil && b.Build.Stdout == "" { // TODO: If you wanted to have very friendly error messages, you could do that here b.Build.Stdout = buildErr.Error() + "\n" } } // persist the build to the database if err := database.SaveBuild(b.Build); err != nil { return err } // persist the commit to the database if err := database.SaveCommit(b.Commit); err != nil { return err } // notify the channels that the commit and build finished channel.SendJSON(reposlug, b.Commit) channel.SendJSON(commitslug, b.Build) channel.Close(consoleslug) // add the smtp address to the notificaitons //if b.Script.Notifications != nil && b.Script.Notifications.Email != nil { // b.Script.Notifications.Email.SetServer(settings.SmtpServer, settings.SmtpPort, // settings.SmtpUsername, settings.SmtpPassword, settings.SmtpAddress) //} // send all "finished" notifications if b.Script.Notifications != nil { b.sendEmail(context) // send email from queue, not from inside /build/script package b.Script.Notifications.Send(context) } return nil }