From 85256d3a22d105730199522deea3729a1922e0fd Mon Sep 17 00:00:00 2001 From: Brad Rydzewski Date: Mon, 11 May 2015 00:45:31 -0700 Subject: [PATCH] backported 0.4 changes to existing database --- builder/build.go | 153 ----- builder/builder.go | 81 --- builder/copy.go | 124 ---- builder/docker/ambassador.go | 110 ---- builder/node.go | 103 ---- builder/util.go | 98 --- common/build.go | 100 +--- common/ccmenu/ccmenu.go | 12 +- common/clone.go | 26 + common/commit.go | 34 ++ common/hook.go | 5 +- common/repo.go | 92 ++- common/status.go | 10 + common/token.go | 20 +- common/user.go | 20 +- datastore/builtin/blob.go | 75 +++ datastore/builtin/blob_test.go | 66 +++ datastore/builtin/bolt.go | 67 --- datastore/builtin/build.go | 213 ++----- datastore/builtin/build_test.go | 132 ----- datastore/builtin/commit.go | 169 ++++++ datastore/builtin/commit_test.go | 178 ++++++ datastore/builtin/datastore.go | 92 +++ datastore/builtin/migrate/helper.go | 47 ++ datastore/builtin/migrate/migrate.go | 213 +++++++ datastore/builtin/migrate/version.go | 57 ++ datastore/builtin/repo.go | 273 ++++----- datastore/builtin/repo_test.go | 280 ++++----- datastore/builtin/star.go | 60 ++ datastore/builtin/star_test.go | 60 ++ datastore/builtin/task.go | 47 -- datastore/builtin/task_test.go | 47 -- datastore/builtin/token.go | 112 ++-- datastore/builtin/token_test.go | 170 ++++-- datastore/builtin/user.go | 166 ++++-- datastore/builtin/user_test.go | 92 --- datastore/builtin/users_test.go | 224 +++++++ datastore/builtin/util.go | 125 +--- datastore/datastore.go | 199 ++++--- drone.go | 39 +- queue/worker.go | 12 +- remote/github/github.go | 107 ++-- remote/remote.go | 4 +- runner/builtin/runner.go | 107 ++-- runner/builtin/updater.go | 46 +- runner/builtin/worker.go | 12 +- runner/runner.go | 10 +- server/badge.go | 18 +- server/builds.go | 263 --------- server/commits.go | 279 +++++++++ server/hooks.go | 76 ++- server/login.go | 4 +- server/queue.go | 310 +++++----- server/repos.go | 119 ++-- server/server.go | 8 +- server/static/images/logo.svg | 93 +++ server/static/index.html | 72 +-- server/static/scripts/controllers/builds.js | 29 +- server/static/scripts/controllers/commits.js | 67 +++ server/static/scripts/controllers/users.js | 2 +- server/static/scripts/views/build.html | 123 ++-- server/static/scripts/views/builds.html | 18 +- server/static/scripts/views/repos_edit.html | 12 +- server/static/styles/drone.css | 590 +++++++++++++++++++ server/token.go | 9 +- server/user.go | 4 +- server/users.go | 8 +- server/ws.go | 19 +- settings/settings.go | 3 +- 69 files changed, 3731 insertions(+), 2884 deletions(-) delete mode 100644 builder/build.go delete mode 100644 builder/builder.go delete mode 100644 builder/copy.go delete mode 100644 builder/docker/ambassador.go delete mode 100644 builder/node.go delete mode 100644 builder/util.go create mode 100644 common/clone.go create mode 100644 common/commit.go create mode 100644 common/status.go create mode 100644 datastore/builtin/blob.go create mode 100644 datastore/builtin/blob_test.go delete mode 100644 datastore/builtin/bolt.go delete mode 100644 datastore/builtin/build_test.go create mode 100644 datastore/builtin/commit.go create mode 100644 datastore/builtin/commit_test.go create mode 100644 datastore/builtin/datastore.go create mode 100644 datastore/builtin/migrate/helper.go create mode 100644 datastore/builtin/migrate/migrate.go create mode 100644 datastore/builtin/migrate/version.go create mode 100644 datastore/builtin/star.go create mode 100644 datastore/builtin/star_test.go delete mode 100644 datastore/builtin/task.go delete mode 100644 datastore/builtin/task_test.go delete mode 100644 datastore/builtin/user_test.go create mode 100644 datastore/builtin/users_test.go delete mode 100644 server/builds.go create mode 100644 server/commits.go create mode 100644 server/static/images/logo.svg create mode 100644 server/static/scripts/controllers/commits.js create mode 100644 server/static/styles/drone.css diff --git a/builder/build.go b/builder/build.go deleted file mode 100644 index bcbe73200..000000000 --- a/builder/build.go +++ /dev/null @@ -1,153 +0,0 @@ -package builder - -import ( - "io" - "sync" - "time" - - "github.com/drone/drone/common" - "github.com/samalba/dockerclient" -) - -// B is a type passed to build nodes. B implements an io.Writer -// and will accumulate build output during execution. -type B struct { - sync.Mutex - - Repo *common.Repo - Build *common.Build - Task *common.Task - Clone *common.Clone - - client dockerclient.Client - - writer io.Writer - - exitCode int - - start time.Time // Time build started - duration time.Duration - timerOn bool - - containers []string -} - -// NewB returns a new Build context. -func NewB(client dockerclient.Client, w io.Writer) *B { - return &B{ - client: client, - writer: w, - } -} - -// Run creates and runs a Docker container. -func (b *B) Run(conf *dockerclient.ContainerConfig) (string, error) { - b.Lock() - defer b.Unlock() - - name, err := b.client.CreateContainer(conf, "") - if err != nil { - // on error try to pull the Docker image. - // note that this may not be the cause of - // the error, but we'll try just in case. - b.client.PullImage(conf.Image, nil) - - // then try to re-create - name, err = b.client.CreateContainer(conf, "") - if err != nil { - return name, err - } - } - b.containers = append(b.containers, name) - err = b.client.StartContainer(name, &conf.HostConfig) - if err != nil { - return name, err - } - - return name, nil -} - -// Inspect inspects the running Docker container and returns -// the contianer runtime information and state. -func (b *B) Inspect(name string) (*dockerclient.ContainerInfo, error) { - return b.client.InspectContainer(name) -} - -// Remove stops and removes the named Docker container. -func (b *B) Remove(name string) { - b.client.StopContainer(name, 5) - b.client.KillContainer(name, "9") - b.client.RemoveContainer(name, true, true) -} - -// RemoveAll stops and removes all Docker containers that were -// created and started during the build process. -func (b *B) RemoveAll() { - b.Lock() - defer b.Unlock() - - for i := len(b.containers) - 1; i >= 0; i-- { - b.Remove(b.containers[i]) - } -} - -// Logs returns an io.ReadCloser for reading the build stream of -// the named Docker container. -func (b *B) Logs(name string) (io.ReadCloser, error) { - opts := dockerclient.LogOptions{ - Follow: true, - Stderr: true, - Stdout: true, - Timestamps: false, - } - return b.client.ContainerLogs(name, &opts) -} - -// StartTimer starts timing a build. This function is called automatically -// before a build starts, but it can also used to resume timing after -// a call to StopTimer. -func (b *B) StartTimer() { - b.Lock() - defer b.Unlock() - - if !b.timerOn { - b.start = time.Now() - b.timerOn = true - } -} - -// StopTimer stops timing a build. This can be used to pause the timer -// while performing complex initialization that you don't want to measure. -func (b *B) StopTimer() { - b.Lock() - defer b.Unlock() - - if b.timerOn { - b.duration += time.Now().Sub(b.start) - b.timerOn = false - } -} - -// Write writes the build stdout and stderr to the result. -func (b *B) Write(p []byte) (n int, err error) { - return b.writer.Write(p) -} - -// Exit writes the function as having failed but continues execution. -func (b *B) Exit(code int) { - b.Lock() - defer b.Unlock() - - if code != 0 { // never override non-zero exit - b.exitCode = code - } -} - -// ExitCode reports the build exit code. A non-zero value indicates -// the build exited with errors. -func (b *B) ExitCode() int { - b.Lock() - defer b.Unlock() - - return b.exitCode -} diff --git a/builder/builder.go b/builder/builder.go deleted file mode 100644 index 203011df4..000000000 --- a/builder/builder.go +++ /dev/null @@ -1,81 +0,0 @@ -package builder - -import "github.com/drone/drone/common" - -// Builder represents a build execution tree. -type Builder struct { - builds Node - deploy Node - notify Node -} - -// Run runs the build, deploy and notify nodes -// in the build tree. -func (b *Builder) Run(build *B) error { - var err error - err = b.RunBuild(build) - if err != nil { - return err - } - err = b.RunDeploy(build) - if err != nil { - return err - } - return b.RunNotify(build) -} - -// RunBuild runs only the build node. -func (b *Builder) RunBuild(build *B) error { - return b.builds.Run(build) -} - -// RunDeploy runs only the deploy node. -func (b *Builder) RunDeploy(build *B) error { - return b.notify.Run(build) -} - -// RunNotify runs on the notify node. -func (b *Builder) RunNotify(build *B) error { - return b.notify.Run(build) -} - -func (b *Builder) HasDeploy() bool { - return len(b.deploy.(serialNode)) != 0 -} - -func (b *Builder) HasNotify() bool { - return len(b.notify.(serialNode)) != 0 -} - -// Load loads a build configuration file. -func Load(conf *common.Config) *Builder { - var ( - builds []Node - deploys []Node - notifys []Node - ) - - for _, step := range conf.Compose { - builds = append(builds, &serviceNode{step}) // compose - } - builds = append(builds, &batchNode{conf.Setup}) // setup - if conf.Clone != nil { - builds = append(builds, &batchNode{conf.Clone}) // clone - } - builds = append(builds, &batchNode{conf.Build}) // build - - for _, step := range conf.Publish { - deploys = append(deploys, &batchNode{step}) // publish - } - for _, step := range conf.Deploy { - deploys = append(deploys, &batchNode{step}) // deploy - } - for _, step := range conf.Notify { - notifys = append(notifys, &batchNode{step}) // notify - } - return &Builder{ - serialNode(builds), - serialNode(deploys), - serialNode(notifys), - } -} diff --git a/builder/copy.go b/builder/copy.go deleted file mode 100644 index ade4a9e27..000000000 --- a/builder/copy.go +++ /dev/null @@ -1,124 +0,0 @@ -package builder - -import ( - "encoding/binary" - "errors" - "io" -) - -const ( - StdWriterPrefixLen = 8 - StdWriterFdIndex = 0 - StdWriterSizeIndex = 4 -) - -type StdType [StdWriterPrefixLen]byte - -var ( - Stdin StdType = StdType{0: 0} - Stdout StdType = StdType{0: 1} - Stderr StdType = StdType{0: 2} -) - -type StdWriter struct { - io.Writer - prefix StdType - sizeBuf []byte -} - -var ErrInvalidStdHeader = errors.New("Unrecognized input header") - -// StdCopy is a modified version of io.Copy. -// -// StdCopy will demultiplex `src`, assuming that it contains two streams, -// previously multiplexed together using a StdWriter instance. -// As it reads from `src`, StdCopy will write to `dstout` and `dsterr`. -// -// StdCopy will read until it hits EOF on `src`. It will then return a nil error. -// In other words: if `err` is non nil, it indicates a real underlying error. -// -// `written` will hold the total number of bytes written to `dstout` and `dsterr`. -func StdCopy(dstout, dsterr io.Writer, src io.Reader) (written int64, err error) { - var ( - buf = make([]byte, 32*1024+StdWriterPrefixLen+1) - bufLen = len(buf) - nr, nw int - er, ew error - out io.Writer - frameSize int - ) - - for { - // Make sure we have at least a full header - for nr < StdWriterPrefixLen { - var nr2 int - nr2, er = src.Read(buf[nr:]) - nr += nr2 - if er == io.EOF { - if nr < StdWriterPrefixLen { - return written, nil - } - break - } - if er != nil { - return 0, er - } - } - - // Check the first byte to know where to write - switch buf[StdWriterFdIndex] { - case 0: - fallthrough - case 1: - // Write on stdout - out = dstout - case 2: - // Write on stderr - out = dsterr - default: - return 0, ErrInvalidStdHeader - } - - // Retrieve the size of the frame - frameSize = int(binary.BigEndian.Uint32(buf[StdWriterSizeIndex : StdWriterSizeIndex+4])) - - // Check if the buffer is big enough to read the frame. - // Extend it if necessary. - if frameSize+StdWriterPrefixLen > bufLen { - buf = append(buf, make([]byte, frameSize+StdWriterPrefixLen-bufLen+1)...) - bufLen = len(buf) - } - - // While the amount of bytes read is less than the size of the frame + header, we keep reading - for nr < frameSize+StdWriterPrefixLen { - var nr2 int - nr2, er = src.Read(buf[nr:]) - nr += nr2 - if er == io.EOF { - if nr < frameSize+StdWriterPrefixLen { - return written, nil - } - break - } - if er != nil { - return 0, er - } - } - - // Write the retrieved frame (without header) - nw, ew = out.Write(buf[StdWriterPrefixLen : frameSize+StdWriterPrefixLen]) - if ew != nil { - return 0, ew - } - // If the frame has not been fully written: error - if nw != frameSize { - return 0, io.ErrShortWrite - } - written += int64(nw) - - // Move the rest of the buffer to the beginning - copy(buf, buf[frameSize+StdWriterPrefixLen:]) - // Move the index - nr -= frameSize + StdWriterPrefixLen - } -} diff --git a/builder/docker/ambassador.go b/builder/docker/ambassador.go deleted file mode 100644 index 8b5fe4cf0..000000000 --- a/builder/docker/ambassador.go +++ /dev/null @@ -1,110 +0,0 @@ -package docker - -import ( - "errors" - - log "github.com/Sirupsen/logrus" - "github.com/samalba/dockerclient" -) - -var errNop = errors.New("Operation not supported") - -// Ambassador is a wrapper around the Docker client that -// provides a shared volume and network for all containers. -type Ambassador struct { - dockerclient.Client - name string -} - -// NewAmbassador creates an ambassador container and wraps the Docker -// client to inject the ambassador volume and network into containers. -func NewAmbassador(client dockerclient.Client) (_ *Ambassador, err error) { - amb := &Ambassador{client, ""} - - conf := &dockerclient.ContainerConfig{} - host := &dockerclient.HostConfig{} - conf.Entrypoint = []string{"/bin/sleep"} - conf.Cmd = []string{"86400"} - conf.Image = "busybox" - conf.Volumes = map[string]struct{}{} - conf.Volumes["/drone"] = struct{}{} - - // creates the ambassador container - amb.name, err = client.CreateContainer(conf, "") - if err != nil { - log.WithField("ambassador", conf.Image).Errorln(err) - - // on failure attempts to pull the image - client.PullImage(conf.Image, nil) - - // then attempts to re-create the container - amb.name, err = client.CreateContainer(conf, "") - if err != nil { - log.WithField("ambassador", conf.Image).Errorln(err) - return nil, err - } - } - err = client.StartContainer(amb.name, host) - if err != nil { - log.WithField("ambassador", conf.Image).Errorln(err) - } - return amb, err -} - -// Destroy stops and deletes the ambassador container. -func (c *Ambassador) Destroy() error { - c.Client.StopContainer(c.name, 5) - c.Client.KillContainer(c.name, "9") - return c.Client.RemoveContainer(c.name, true, true) -} - -// CreateContainer creates a container. -func (c *Ambassador) CreateContainer(conf *dockerclient.ContainerConfig, name string) (string, error) { - log.WithField("image", conf.Image).Infoln("create container") - - // add the affinity flag for swarm - conf.Env = append(conf.Env, "affinity:container=="+c.name) - - id, err := c.Client.CreateContainer(conf, name) - if err != nil { - log.WithField("image", conf.Image).Errorln(err) - } - return id, err -} - -// StartContainer starts a container. The ambassador volume -// is automatically linked. The ambassador network is linked -// iff a network mode is not already specified. -func (c *Ambassador) StartContainer(id string, conf *dockerclient.HostConfig) error { - log.WithField("container", id).Debugln("start container") - - conf.VolumesFrom = append(conf.VolumesFrom, c.name) - if len(conf.NetworkMode) == 0 { - conf.NetworkMode = "container:" + c.name - } - err := c.Client.StartContainer(id, conf) - if err != nil { - log.WithField("container", id).Errorln(err) - } - return err -} - -// StopContainer stops a container. -func (c *Ambassador) StopContainer(id string, timeout int) error { - log.WithField("container", id).Debugln("stop container") - err := c.Client.StopContainer(id, timeout) - if err != nil { - log.WithField("container", id).Errorln(err) - } - return err -} - -// PullImage pulls an image. -func (c *Ambassador) PullImage(name string, auth *dockerclient.AuthConfig) error { - log.WithField("image", name).Debugln("pull image") - err := c.Client.PullImage(name, auth) - if err != nil { - log.WithField("image", name).Errorln(err) - } - return err -} diff --git a/builder/node.go b/builder/node.go deleted file mode 100644 index ce4a753fe..000000000 --- a/builder/node.go +++ /dev/null @@ -1,103 +0,0 @@ -package builder - -import ( - "sync" - - "github.com/drone/drone/common" -) - -// Node is an element in the build execution tree. -type Node interface { - Run(*B) error -} - -// parallelNode runs a set of build nodes in parallel. -type parallelNode []Node - -func (n parallelNode) Run(b *B) error { - var wg sync.WaitGroup - for _, node := range n { - wg.Add(1) - - go func(node Node) { - defer wg.Done() - node.Run(b) - }(node) - } - wg.Wait() - return nil -} - -// serialNode runs a set of build nodes in sequential order. -type serialNode []Node - -func (n serialNode) Run(b *B) error { - for _, node := range n { - err := node.Run(b) - if err != nil { - return err - } - if b.ExitCode() != 0 { - return nil - } - } - return nil -} - -// batchNode runs a container and blocks until complete. -type batchNode struct { - step *common.Step -} - -func (n *batchNode) Run(b *B) error { - - // switch { - // case n.step.Condition == nil: - // case n.step.Condition.MatchBranch(b.Commit.Branch) == false: - // return nil - // case n.step.Condition.MatchOwner(b.Repo.Owner) == false: - // return nil - // } - - // creates the container conf - conf := toContainerConfig(n.step) - if n.step.Config != nil { - conf.Cmd = toCommand(b, n.step) - } - - // inject environment vars - injectEnv(b, conf) - - name, err := b.Run(conf) - if err != nil { - return err - } - - // streams the logs to the build results - rc, err := b.Logs(name) - if err != nil { - return err - } - StdCopy(b, b, rc) - //io.Copy(b, rc) - - // inspects the results and writes the - // build result exit code - info, err := b.Inspect(name) - if err != nil { - return err - } - b.Exit(info.State.ExitCode) - return nil -} - -// serviceNode runs a container, blocking, writes output, uses config section -type serviceNode struct { - step *common.Step -} - -func (n *serviceNode) Run(b *B) error { - conf := toContainerConfig(n.step) - _, err := b.Run(conf) - return err -} diff --git a/builder/util.go b/builder/util.go deleted file mode 100644 index ba36a0f09..000000000 --- a/builder/util.go +++ /dev/null @@ -1,98 +0,0 @@ -package builder - -import ( - "encoding/json" - "fmt" - "strings" - - "github.com/drone/drone/common" - "github.com/samalba/dockerclient" -) - -// helper function that converts the build step to -// a containerConfig for use with the dockerclient -func toContainerConfig(step *common.Step) *dockerclient.ContainerConfig { - config := &dockerclient.ContainerConfig{ - Image: step.Image, - Env: step.Environment, - Cmd: step.Command, - Entrypoint: step.Entrypoint, - WorkingDir: step.WorkingDir, - HostConfig: dockerclient.HostConfig{ - Privileged: step.Privileged, - NetworkMode: step.NetworkMode, - }, - } - - config.Volumes = map[string]struct{}{} - for _, path := range step.Volumes { - if strings.Index(path, ":") == -1 { - continue - } - parts := strings.Split(path, ":") - config.Volumes[parts[1]] = struct{}{} - config.HostConfig.Binds = append(config.HostConfig.Binds, path) - } - - return config -} - -// helper function to inject drone-specific environment -// variables into the container. -func injectEnv(b *B, conf *dockerclient.ContainerConfig) { - var branch string - var commit string - if b.Build.Commit != nil { - branch = b.Build.Commit.Ref - commit = b.Build.Commit.Sha - } else { - branch = b.Build.PullRequest.Target.Ref - commit = b.Build.PullRequest.Target.Sha - } - - conf.Env = append(conf.Env, "DRONE=true") - conf.Env = append(conf.Env, fmt.Sprintf("DRONE_BRANCH=%s", branch)) - conf.Env = append(conf.Env, fmt.Sprintf("DRONE_COMMIT=%s", commit)) - - // for jenkins campatibility - conf.Env = append(conf.Env, "CI=true") - conf.Env = append(conf.Env, fmt.Sprintf("WORKSPACE=%s", b.Clone.Dir)) - conf.Env = append(conf.Env, fmt.Sprintf("JOB_NAME=%s/%s", b.Repo.Owner, b.Repo.Name)) - conf.Env = append(conf.Env, fmt.Sprintf("BUILD_ID=%d", b.Build.Number)) - conf.Env = append(conf.Env, fmt.Sprintf("BUILD_DIR=%s", b.Clone.Dir)) - conf.Env = append(conf.Env, fmt.Sprintf("GIT_BRANCH=%s", branch)) - conf.Env = append(conf.Env, fmt.Sprintf("GIT_COMMIT=%s", commit)) - -} - -// helper function to encode the build step to -// a json string. Primarily used for plugins, which -// expect a json encoded string in stdin or arg[1]. -func toCommand(b *B, step *common.Step) []string { - p := payload{ - b.Repo, - b.Build, - b.Task, - b.Clone, - step.Config, - } - return []string{p.Encode()} -} - -// payload represents the payload of a plugin -// that is serialized and sent to the plugin in JSON -// format via stdin or arg[1]. -type payload struct { - Repo *common.Repo `json:"repo"` - Build *common.Build `json:"build"` - Task *common.Task `json:"task"` - Clone *common.Clone `json:"clone"` - - Config map[string]interface{} `json:"vargs"` -} - -// Encode encodes the payload in JSON format. -func (p *payload) Encode() string { - out, _ := json.Marshal(p) - return string(out) -} diff --git a/common/build.go b/common/build.go index 78033b11d..22d109824 100644 --- a/common/build.go +++ b/common/build.go @@ -1,93 +1,19 @@ package common -const ( - StatePending = "pending" - StateRunning = "running" - StateSuccess = "success" - StateFailure = "failure" - StateKilled = "killed" - StateError = "error" -) - type Build struct { - Number int `json:"number"` - State string `json:"state"` - Duration int64 `json:"duration"` - Started int64 `json:"started_at"` - Finished int64 `json:"finished_at"` - Created int64 `json:"created_at"` - Updated int64 `json:"updated_at"` + ID int64 `meddler:"build_id,pk" json:"-"` + CommitID int64 `meddler:"commit_id" json:"-"` + State string `meddler:"build_state" json:"state"` + ExitCode int `meddler:"build_exit" json:"exit_code"` + Sequence int `meddler:"build_seq" json:"sequence"` + Duration int64 `meddler:"build_duration" json:"duration"` + Started int64 `meddler:"build_started" json:"started_at"` + Finished int64 `meddler:"build_finished" json:"finished_at"` + Created int64 `meddler:"build_created" json:"created_at"` + Updated int64 `meddler:"build_updated" json:"updated_at"` - // Tasks int `json:"task_count"` - - // Commit represents the commit data send in the - // post-commit hook. This will not be populated when - // a pull requests. - Commit *Commit `json:"head_commit,omitempty"` - - // PullRequest represents the pull request data sent - // in the post-commit hook. This will only be populated - // when a pull request. - PullRequest *PullRequest `json:"pull_request,omitempty"` - - // Statuses represents a list of build statuses used - // to annotate the build. - Statuses []*Status `json:"statuses,omitempty"` - - // Tasks represents a list of build tasks. A build is - // comprised of one or many tasks. - Tasks []*Task `json:"tasks,omitempty"` + Environment map[string]string `meddler:"build_env,json" json:"environment"` } -type Status struct { - State string `json:"state"` - Link string `json:"target_url"` - Desc string `json:"description"` - Context string `json:"context"` -} - -type Commit struct { - Sha string `json:"sha,omitempty"` - Ref string `json:"ref,omitempty"` - Message string `json:"message,omitempty"` - Timestamp string `json:"timestamp,omitempty"` - Author *Author `json:"author,omitempty"` - Remote *Remote `json:"repo,omitempty"` -} - -type PullRequest struct { - Number int `json:"number,omitempty"` - Title string `json:"title,omitempty"` - Source *Commit `json:"source,omitempty"` - Target *Commit `json:"target,omitempty"` -} - -type Author struct { - Name string `json:"name,omitempty"` - Login string `json:"login,omitempty"` - Email string `json:"email,omitempty"` - Gravatar string `json:"gravatar_id,omitempty"` -} - -type Remote struct { - Name string `json:"name,omitempty"` - FullName string `json:"full_name,omitempty"` - Clone string `json:"clone_url,omitempty"` -} - -type Clone struct { - Origin string `json:"origin"` - Remote string `json:"remote"` - Branch string `json:"branch"` - Sha string `json:"sha"` - Ref string `json:"ref"` - Dir string `json:"dir"` - Netrc *Netrc `json:"netrc"` - Keypair *Keypair `json:"keypair"` -} - -type Netrc struct { - Machine string `json:"machine"` - Login string `json:"login"` - Password string `json:"user"` -} +// QUESTION: should we track if it was oom killed? +// OOMKill bool `meddler:"build_oom" json:"oom_kill"` diff --git a/common/ccmenu/ccmenu.go b/common/ccmenu/ccmenu.go index 55eaf3ad8..5b9148dc0 100644 --- a/common/ccmenu/ccmenu.go +++ b/common/ccmenu/ccmenu.go @@ -23,7 +23,7 @@ type CCProject struct { WebURL string `xml:"webUrl,attr"` } -func NewCC(r *common.Repo, b *common.Build, url string) *CCProjects { +func NewCC(r *common.Repo, c *common.Commit, url string) *CCProjects { proj := &CCProject{ Name: r.Owner + "/" + r.Name, WebURL: url, @@ -34,16 +34,16 @@ func NewCC(r *common.Repo, b *common.Build, url string) *CCProjects { // if the build is not currently running then // we can return the latest build status. - if b.State != common.StatePending && - b.State != common.StateRunning { + if c.State != common.StatePending && + c.State != common.StateRunning { proj.Activity = "Sleeping" - proj.LastBuildTime = time.Unix(b.Started, 0).Format(time.RFC3339) - proj.LastBuildLabel = strconv.Itoa(b.Number) + proj.LastBuildTime = time.Unix(c.Started, 0).Format(time.RFC3339) + proj.LastBuildLabel = strconv.Itoa(c.Sequence) } // ensure the last build state accepts a valid // ccmenu enumeration - switch b.State { + switch c.State { case common.StateError, common.StateKilled: proj.LastBuildStatus = "Exception" case common.StateSuccess: diff --git a/common/clone.go b/common/clone.go new file mode 100644 index 000000000..3ef821ef4 --- /dev/null +++ b/common/clone.go @@ -0,0 +1,26 @@ +package common + +type Clone struct { + Origin string `json:"origin"` + Remote string `json:"remote"` + Branch string `json:"branch"` + Sha string `json:"sha"` + Ref string `json:"ref"` + Dir string `json:"dir"` + Netrc *Netrc `json:"netrc"` + Keypair *Keypair `json:"keypair"` +} + +type Netrc struct { + Machine string `json:"machine"` + Login string `json:"login"` + Password string `json:"user"` +} + +// Keypair represents an RSA public and private key +// assigned to a repository. It may be used to clone +// private repositories, or as a deployment key. +type Keypair struct { + Public string `json:"public,omitempty"` + Private string `json:"private,omitempty"` +} diff --git a/common/commit.go b/common/commit.go new file mode 100644 index 000000000..8c832fd67 --- /dev/null +++ b/common/commit.go @@ -0,0 +1,34 @@ +package common + +const ( + StatePending = "pending" + StateRunning = "running" + StateSuccess = "success" + StateFailure = "failure" + StateKilled = "killed" + StateError = "error" +) + +type Commit struct { + ID int64 `meddler:"commit_id,pk" json:"-"` + RepoID int64 `meddler:"repo_id" json:"-"` + Sequence int `meddler:"commit_seq" json:"sequence"` + State string `meddler:"commit_state" json:"state"` + Started int64 `meddler:"commit_started" json:"started_at"` + Finished int64 `meddler:"commit_finished" json:"finished_at"` + Sha string `meddler:"commit_sha" json:"sha"` + Ref string `meddler:"commit_ref" json:"ref"` + PullRequest string `meddler:"commit_pr" json:"pull_request,omitempty"` + Branch string `meddler:"commit_branch" json:"branch"` + Author string `meddler:"commit_author" json:"author"` + Gravatar string `meddler:"commit_gravatar" json:"gravatar"` + Timestamp string `meddler:"commit_timestamp" json:"timestamp"` + Message string `meddler:"commit_message" json:"message"` + SourceRemote string `meddler:"commit_source_remote" json:"source_remote,omitempty"` + SourceBranch string `meddler:"commit_source_branch" json:"source_branch,omitempty"` + SourceSha string `meddler:"commit_source_sha" json:"source_sha,omitempty"` + Created int64 `meddler:"commit_created" json:"created_at"` + Updated int64 `meddler:"commit_updated" json:"updated_at"` + + Builds []*Build `meddler:"-" json:"builds,omitempty"` +} diff --git a/common/hook.go b/common/hook.go index a554aa568..4690ae3a8 100644 --- a/common/hook.go +++ b/common/hook.go @@ -1,7 +1,6 @@ package common type Hook struct { - Repo *Repo - Commit *Commit - PullRequest *PullRequest + Repo *Repo + Commit *Commit } diff --git a/common/repo.go b/common/repo.go index 97579d4bf..9ad843f7a 100644 --- a/common/repo.go +++ b/common/repo.go @@ -1,62 +1,54 @@ package common type Repo struct { - ID int64 `json:"id"` - Owner string `json:"owner"` - Name string `json:"name"` - FullName string `json:"full_name"` - Language string `json:"language"` - Private bool `json:"private"` - Link string `json:"link_url"` - Clone string `json:"clone_url"` - Branch string `json:"default_branch"` + ID int64 `meddler:"repo_id,pk" json:"id"` + UserID int64 `meddler:"user_id" json:"-"` + Owner string `meddler:"repo_owner" json:"owner"` + Name string `meddler:"repo_name" json:"name"` + FullName string `meddler:"repo_slug" json:"full_name"` + Token string `meddler:"repo_token" json:"-"` + Language string `meddler:"repo_lang" json:"language"` + Private bool `meddler:"repo_private" json:"private"` + Link string `meddler:"repo_link" json:"link_url"` + Clone string `meddler:"repo_clone" json:"clone_url"` + Branch string `meddler:"repo_branch" json:"default_branch"` + Timeout int64 `meddler:"repo_timeout" json:"timeout"` + Trusted bool `meddler:"repo_trusted" json:"trusted"` + PostCommit bool `meddler:"repo_push" json:"post_commits"` + PullRequest bool `meddler:"repo_pull" json:"pull_requests"` + PublicKey string `meddler:"repo_public_key" json:"-"` + PrivateKey string `meddler:"repo_private_key" json:"-"` + Created int64 `meddler:"repo_created" json:"created_at"` + Updated int64 `meddler:"repo_updated" json:"updated_at"` - Timeout int64 `json:"timeout"` - Trusted bool `json:"trusted"` - Disabled bool `json:"disabled"` - DisablePR bool `json:"disable_prs"` - DisableTag bool `json:"disable_tags"` - - Created int64 `json:"created_at"` - Updated int64 `json:"updated_at"` - - User *Owner `json:"user,omitempty"` - Last *Build `json:"last_build,omitempty"` + Params map[string]string `meddler:"repo_params,json" json:"-"` } -// Keypair represents an RSA public and private key -// assigned to a repository. It may be used to clone -// private repositories, or as a deployment key. -type Keypair struct { - Public string `json:"public"` - Private string `json:"private"` +type RepoLite struct { + ID int64 `meddler:"repo_id,pk" json:"id"` + UserID int64 `meddler:"user_id" json:"-"` + Owner string `meddler:"repo_owner" json:"owner"` + Name string `meddler:"repo_name" json:"name"` + FullName string `meddler:"repo_slug" json:"full_name"` + Language string `meddler:"repo_lang" json:"language"` + Private bool `meddler:"repo_private" json:"private"` + Created int64 `meddler:"repo_created" json:"created_at"` + Updated int64 `meddler:"repo_updated" json:"updated_at"` } -// Owner represents the owner of a repository. -type Owner struct { - Login string `json:"login"` +type RepoCommit struct { + ID int64 `meddler:"repo_id,pk" json:"id"` + Owner string `meddler:"repo_owner" json:"owner"` + Name string `meddler:"repo_name" json:"name"` + FullName string `meddler:"repo_slug" json:"full_name"` + Number int `meddler:"commit_seq" json:"number"` + State string `meddler:"commit_state" json:"state"` + Started int64 `meddler:"commit_started" json:"started_at"` + Finished int64 `meddler:"commit_finished" json:"finished_at"` } -// Subscriber represents a user's subscription -// to a repository. This determines if the repository -// is displayed on the user dashboard and in the user -// event feed. -type Subscriber struct { - // Determines if notifications should be - // received from this repository. - Subscribed bool `json:"subscribed"` - - // Determines if all notifications should be - // blocked from this repository. - Ignored bool `json:"ignored"` -} - -// Perm represents a user's permissiont to access -// a repository. Pull indicates read-only access. Push -// indiates write access. Admin indicates god access. type Perm struct { - Login string `json:"login,omitempty"` - Pull bool `json:"pull"` - Push bool `json:"push"` - Admin bool `json:"admin"` + Pull bool + Push bool + Admin bool } diff --git a/common/status.go b/common/status.go new file mode 100644 index 000000000..0990be005 --- /dev/null +++ b/common/status.go @@ -0,0 +1,10 @@ +package common + +type Status struct { + ID int64 `meddler:"status_id,pk" json:"-"` + CommitID int64 `meddler:"commit_id" json:"-"` + State string `meddler:"status_state" json:"state"` + Link string `meddler:"status_link" json:"target_url"` + Desc string `meddler:"status_desc" json:"description"` + Context string `meddler:"status_context" json:"context"` +} diff --git a/common/token.go b/common/token.go index 482f01ad1..c98f47c85 100644 --- a/common/token.go +++ b/common/token.go @@ -1,18 +1,18 @@ package common +type Token struct { + ID int64 `meddler:"token_id,pk" json:"-"` + UserID int64 `meddler:"user_id" json:"-"` + Login string `meddler:"-" json:"-"` + Kind string `meddler:"token_kind" json:"kind,omitempty"` + Label string `meddler:"token_label" json:"label,omitempty"` + Expiry int64 `meddler:"token_expiry" json:"expiry,omitempty"` + Issued int64 `meddler:"token_issued" json:"issued_at,omitempty"` +} + const ( TokenUser = "u" TokenSess = "s" TokenHook = "h" TokenAgent = "a" ) - -type Token struct { - Kind string `json:"kind"` - Login string `json:"-"` - Label string `json:"label"` - Repos []string `json:"repos,omitempty"` - Scopes []string `json:"scopes,omitempty"` - Expiry int64 `json:"expiry,omitempty"` - Issued int64 `json:"issued_at,omitempty"` -} diff --git a/common/user.go b/common/user.go index 22801e544..1f2e7530a 100644 --- a/common/user.go +++ b/common/user.go @@ -1,13 +1,15 @@ package common type User struct { - Login string `json:"login,omitempty"` - Token string `json:"-"` - Secret string `json:"-"` - Name string `json:"name,omitempty"` - Email string `json:"email,omitempty"` - Gravatar string `json:"gravatar_id,omitempty"` - Admin bool `json:"admin,omitempty"` - Created int64 `json:"created_at,omitempty"` - Updated int64 `json:"updated_at,omitempty"` + ID int64 `meddler:"user_id,pk" json:"-"` + Login string `meddler:"user_login" json:"login,omitempty"` + Token string `meddler:"user_token" json:"-"` + Secret string `meddler:"user_secret" json:"-"` + Name string `meddler:"user_name" json:"name,omitempty"` + Email string `meddler:"user_email" json:"email,omitempty"` + Gravatar string `meddler:"user_gravatar" json:"gravatar_id,omitempty"` + Admin bool `meddler:"user_admin" json:"admin,omitempty"` + Active bool `meddler:"user_active" json:"active,omitempty"` + Created int64 `meddler:"user_created" json:"created_at,omitempty"` + Updated int64 `meddler:"user_updated" json:"updated_at,omitempty"` } diff --git a/datastore/builtin/blob.go b/datastore/builtin/blob.go new file mode 100644 index 000000000..c989a87b2 --- /dev/null +++ b/datastore/builtin/blob.go @@ -0,0 +1,75 @@ +package builtin + +import ( + "bytes" + "io" + "io/ioutil" + + "github.com/russross/meddler" +) + +type blob struct { + ID int64 `meddler:"blob_id,pk"` + Path string `meddler:"blob_path"` + Data string `meddler:"blob_data,gobgzip"` +} + +type Blobstore struct { + meddler.DB +} + +// Del removes an object from the blobstore. +func (db *Blobstore) DelBlob(path string) error { + var _, err = db.Exec(rebind(blobDeleteStmt), path) + return err +} + +// Get retrieves an object from the blobstore. +func (db *Blobstore) GetBlob(path string) ([]byte, error) { + var blob = blob{} + var err = meddler.QueryRow(db, &blob, rebind(blobQuery), path) + return []byte(blob.Data), err +} + +// GetBlobReader retrieves an object from the blobstore. +// It is the caller's responsibility to call Close on +// the ReadCloser when finished reading. +func (db *Blobstore) GetBlobReader(path string) (io.ReadCloser, error) { + var blob, err = db.GetBlob(path) + var buf = bytes.NewBuffer(blob) + return ioutil.NopCloser(buf), err +} + +// SetBlob inserts an object into the blobstore. +func (db *Blobstore) SetBlob(path string, data []byte) error { + var blob = blob{} + meddler.QueryRow(db, &blob, rebind(blobQuery), path) + blob.Path = path + blob.Data = string(data) + return meddler.Save(db, blobTable, &blob) +} + +// SetBlobReader inserts an object into the blobstore by +// consuming data from r until EOF. +func (db *Blobstore) SetBlobReader(path string, r io.Reader) error { + var data, _ = ioutil.ReadAll(r) + return db.SetBlob(path, data) +} + +func NewBlobstore(db meddler.DB) *Blobstore { + return &Blobstore{db} +} + +// Blob table name in database. +const blobTable = "blobs" + +const blobQuery = ` +SELECT * +FROM blobs +WHERE blob_path = ?; +` + +const blobDeleteStmt = ` +DELETE FROM blobs +WHERE blob_path = ?; +` diff --git a/datastore/builtin/blob_test.go b/datastore/builtin/blob_test.go new file mode 100644 index 000000000..87f8462de --- /dev/null +++ b/datastore/builtin/blob_test.go @@ -0,0 +1,66 @@ +package builtin + +import ( + "bytes" + "io/ioutil" + "testing" + + "github.com/franela/goblin" +) + +func TestBlobstore(t *testing.T) { + db := mustConnectTest() + bs := NewBlobstore(db) + defer db.Close() + + g := goblin.Goblin(t) + g.Describe("Blobstore", func() { + + // before each test be sure to purge the package + // table data from the database. + g.BeforeEach(func() { + db.Exec("DELETE FROM blobs") + }) + + g.It("Should Set a Blob", func() { + err := bs.SetBlob("foo", []byte("bar")) + g.Assert(err == nil).IsTrue() + }) + + g.It("Should Set a Blob reader", func() { + var buf bytes.Buffer + buf.Write([]byte("bar")) + err := bs.SetBlobReader("foo", &buf) + g.Assert(err == nil).IsTrue() + }) + + g.It("Should Overwrite a Blob", func() { + bs.SetBlob("foo", []byte("bar")) + bs.SetBlob("foo", []byte("baz")) + blob, err := bs.GetBlob("foo") + g.Assert(err == nil).IsTrue() + g.Assert(string(blob)).Equal("baz") + }) + + g.It("Should Get a Blob", func() { + bs.SetBlob("foo", []byte("bar")) + blob, err := bs.GetBlob("foo") + g.Assert(err == nil).IsTrue() + g.Assert(string(blob)).Equal("bar") + }) + + g.It("Should Get a Blob reader", func() { + bs.SetBlob("foo", []byte("bar")) + r, _ := bs.GetBlobReader("foo") + blob, _ := ioutil.ReadAll(r) + g.Assert(string(blob)).Equal("bar") + }) + + g.It("Should Del a Blob", func() { + bs.SetBlob("foo", []byte("bar")) + err := bs.DelBlob("foo") + g.Assert(err == nil).IsTrue() + }) + + }) +} diff --git a/datastore/builtin/bolt.go b/datastore/builtin/bolt.go deleted file mode 100644 index ab225399b..000000000 --- a/datastore/builtin/bolt.go +++ /dev/null @@ -1,67 +0,0 @@ -package builtin - -import ( - "errors" - - "github.com/boltdb/bolt" -) - -var ( - ErrKeyNotFound = errors.New("Key not found") - ErrKeyExists = errors.New("Key exists") -) - -var ( - bucketUser = []byte("user") - bucketUserRepos = []byte("user_repos") - bucketUserTokens = []byte("user_tokens") - bucketTokens = []byte("token") - bucketRepo = []byte("repo") - bucketRepoKeys = []byte("repo_keys") - bucketRepoParams = []byte("repo_params") - bucketRepoUsers = []byte("repo_users") - bucketBuild = []byte("build") - bucketBuildAgent = []byte("build_agents") - bucketBuildStatus = []byte("build_status") - bucketBuildLogs = []byte("build_logs") - bucketBuildSeq = []byte("build_seq") -) - -type DB struct { - *bolt.DB -} - -func New(path string) (*DB, error) { - db, err := bolt.Open(path, 0600, nil) - if err != nil { - return nil, err - } - - // Initialize all the required buckets. - db.Update(func(tx *bolt.Tx) error { - tx.CreateBucketIfNotExists(bucketUser) - tx.CreateBucketIfNotExists(bucketUserRepos) - tx.CreateBucketIfNotExists(bucketUserTokens) - tx.CreateBucketIfNotExists(bucketTokens) - tx.CreateBucketIfNotExists(bucketRepo) - tx.CreateBucketIfNotExists(bucketRepoKeys) - tx.CreateBucketIfNotExists(bucketRepoParams) - tx.CreateBucketIfNotExists(bucketRepoUsers) - tx.CreateBucketIfNotExists(bucketBuild) - tx.CreateBucketIfNotExists(bucketBuildAgent) - tx.CreateBucketIfNotExists(bucketBuildStatus) - tx.CreateBucketIfNotExists(bucketBuildLogs) - tx.CreateBucketIfNotExists(bucketBuildSeq) - return nil - }) - - return &DB{db}, nil -} - -func Must(path string) *DB { - db, err := New(path) - if err != nil { - panic(err) - } - return db -} diff --git a/datastore/builtin/build.go b/datastore/builtin/build.go index 776fbc92c..21d522499 100644 --- a/datastore/builtin/build.go +++ b/datastore/builtin/build.go @@ -1,195 +1,62 @@ package builtin import ( - "encoding/binary" - "strconv" "time" - "github.com/boltdb/bolt" "github.com/drone/drone/common" + "github.com/russross/meddler" ) -// Build gets the specified build number for the -// named repository and build number. -func (db *DB) Build(repo string, build int) (*common.Build, error) { - build_ := &common.Build{} - key := []byte(repo + "/" + strconv.Itoa(build)) - - err := db.View(func(t *bolt.Tx) error { - return get(t, bucketBuild, key, build_) - }) - - return build_, err +type Buildstore struct { + meddler.DB } -// BuildList gets a list of recent builds for the -// named repository. -func (db *DB) BuildList(repo string) ([]*common.Build, error) { - // TODO (bradrydzewski) we can do this more efficiently - var builds []*common.Build - build, err := db.BuildLast(repo) - if err == ErrKeyNotFound { - return builds, nil - } else if err != nil { - return nil, err - } - - err = db.View(func(t *bolt.Tx) error { - pos := build.Number - 25 - if pos < 1 { - pos = 1 - } - for i := pos; i <= build.Number; i++ { - key := []byte(repo + "/" + strconv.Itoa(i)) - build := &common.Build{} - err = get(t, bucketBuild, key, build) - if err != nil { - return err - } - builds = append(builds, build) - } - return nil - }) - return builds, err +func NewBuildstore(db meddler.DB) *Buildstore { + return &Buildstore{db} } -// BuildLast gets the last executed build for the -// named repository. -func (db *DB) BuildLast(repo string) (*common.Build, error) { - key := []byte(repo) - build := &common.Build{} - err := db.View(func(t *bolt.Tx) error { - raw := t.Bucket(bucketBuildSeq).Get(key) - if raw == nil { - return ErrKeyNotFound - } - num := binary.LittleEndian.Uint32(raw) - key = []byte(repo + "/" + strconv.FormatUint(uint64(num), 10)) - return get(t, bucketBuild, key, build) - }) +// Build returns a build by ID. +func (db *Buildstore) Build(id int64) (*common.Build, error) { + var build = new(common.Build) + var err = meddler.Load(db, buildTable, build, id) return build, err } -// BuildAgent gets the agent that is currently executing -// a build. If no agent exists ErrKeyNotFound is returned. -func (db *DB) BuildAgent(repo string, build int) (*common.Agent, error) { - key := []byte(repo + "/" + strconv.Itoa(build)) - agent := &common.Agent{} - err := db.View(func(t *bolt.Tx) error { - return get(t, bucketBuildAgent, key, agent) - }) - return agent, err +// BuildSeq returns a build by sequence number. +func (db *Buildstore) BuildSeq(commit *common.Commit, seq int) (*common.Build, error) { + var build = new(common.Build) + var err = meddler.QueryRow(db, build, rebind(buildNumberQuery), commit.ID, seq) + return build, err } -// SetBuild inserts or updates a build for the named -// repository. The build number is incremented and -// assigned to the provided build. -func (db *DB) SetBuild(repo string, build *common.Build) error { - repokey := []byte(repo) +// BuildList returns a list of all commit builds +func (db *Buildstore) BuildList(commit *common.Commit) ([]*common.Build, error) { + var builds []*common.Build + var err = meddler.QueryAll(db, &builds, rebind(buildListQuery), commit.ID) + return builds, err +} + +// SetBuild updates an existing build. +func (db *Buildstore) SetBuild(build *common.Build) error { build.Updated = time.Now().UTC().Unix() - if build.Created == 0 { - build.Created = build.Updated - } - - return db.Update(func(t *bolt.Tx) error { - - if build.Number == 0 { - raw, err := raw(t, bucketBuildSeq, repokey) - - var next_seq uint32 - switch err { - case ErrKeyNotFound: - next_seq = 1 - case nil: - next_seq = 1 + binary.LittleEndian.Uint32(raw) - default: - return err - } - - // covert our seqno to raw value - raw = make([]byte, 4) // TODO(benschumacher) replace magic number 4 (uint32) - binary.LittleEndian.PutUint32(raw, next_seq) - err = t.Bucket(bucketBuildSeq).Put(repokey, raw) - if err != nil { - return err - } - - // fill out the build structure - build.Number = int(next_seq) - build.Created = time.Now().UTC().Unix() - } - - key := []byte(repo + "/" + strconv.Itoa(build.Number)) - return update(t, bucketBuild, key, build) - }) + return meddler.Update(db, buildTable, build) } -// Experimental -func (db *DB) SetBuildState(repo string, build *common.Build) error { - key := []byte(repo + "/" + strconv.Itoa(build.Number)) +// Build table name in database. +const buildTable = "builds" - return db.Update(func(t *bolt.Tx) error { - build_ := &common.Build{} - err := get(t, bucketBuild, key, build_) - if err != nil { - return err - } - build_.Updated = time.Now().UTC().Unix() - build_.Duration = build.Duration - build_.Started = build.Started - build_.Finished = build.Finished - build_.State = build.State - return update(t, bucketBuild, key, build_) - }) -} +// SQL query to retrieve a token by label. +const buildListQuery = ` +SELECT * +FROM builds +WHERE commit_id = ? +ORDER BY build_seq ASC +` -func (db *DB) SetBuildStatus(repo string, build int, status *common.Status) error { - key := []byte(repo + "/" + strconv.Itoa(build)) - - return db.Update(func(t *bolt.Tx) error { - build_ := &common.Build{} - err := get(t, bucketBuild, key, build_) - if err != nil { - return err - } - build_.Updated = time.Now().UTC().Unix() - build_.Statuses = append(build_.Statuses, status) - return update(t, bucketBuild, key, build_) - }) -} - -func (db *DB) SetBuildTask(repo string, build int, task *common.Task) error { - key := []byte(repo + "/" + strconv.Itoa(build)) - - return db.Update(func(t *bolt.Tx) error { - build_ := &common.Build{} - err := get(t, bucketBuild, key, build_) - if err != nil { - return err - } - // check index to prevent nil pointer / panic - if task.Number > len(build_.Tasks) { - return ErrKeyNotFound - } - build_.Updated = time.Now().UTC().Unix() - //assuming task number is 1-based. - build_.Tasks[task.Number-1] = task - return update(t, bucketBuild, key, build_) - }) -} - -// SetBuildAgent insert or updates the agent that is -// running a build. -func (db *DB) SetBuildAgent(repo string, build int, agent *common.Agent) error { - key := []byte(repo + "/" + strconv.Itoa(build)) - return db.Update(func(t *bolt.Tx) error { - return update(t, bucketBuildAgent, key, agent) - }) -} - -func (db *DB) DelBuildAgent(repo string, build int) error { - key := []byte(repo + "/" + strconv.Itoa(build)) - return db.Update(func(t *bolt.Tx) error { - return delete(t, bucketBuildAgent, key) - }) -} +const buildNumberQuery = ` +SELECT * +FROM builds +WHERE commit_id = ? + AND build_seq = ? +LIMIT 1; +` diff --git a/datastore/builtin/build_test.go b/datastore/builtin/build_test.go deleted file mode 100644 index 8a417b0ee..000000000 --- a/datastore/builtin/build_test.go +++ /dev/null @@ -1,132 +0,0 @@ -package builtin - -import ( - "github.com/drone/drone/common" - . "github.com/franela/goblin" - "os" - "testing" -) - -func TestBuild(t *testing.T) { - g := Goblin(t) - g.Describe("Build", func() { - var db *DB // temporary database - repo := string("github.com/octopod/hq") - //testUser := &common.User{Login: "octocat"} - //testRepo := &common.Repo{FullName: "github.com/octopod/hq"} - testUser := "octocat" - testRepo := "github.com/octopod/hq" - //testBuild := 1 - - // create a new database before each unit - // test and destroy afterwards. - g.BeforeEach(func() { - db = Must("/tmp/drone.test.db") - }) - g.AfterEach(func() { - os.Remove(db.Path()) - }) - - g.It("Should sequence builds", func() { - err := db.SetBuild(repo, &common.Build{State: "pending"}) - g.Assert(err).Equal(nil) - - // the first build should always be numero 1 - build, err := db.Build(repo, 1) - g.Assert(err).Equal(nil) - g.Assert(build.State).Equal("pending") - - // add another build, just for fun - err = db.SetBuild(repo, &common.Build{State: "success"}) - g.Assert(err).Equal(nil) - - // get the next build - build, err = db.Build(repo, 2) - g.Assert(err).Equal(nil) - g.Assert(build.State).Equal("success") - }) - - g.It("Should get the latest builds", func() { - db.SetBuild(repo, &common.Build{State: "success"}) - db.SetBuild(repo, &common.Build{State: "success"}) - db.SetBuild(repo, &common.Build{State: "pending"}) - - build, err := db.BuildLast(repo) - g.Assert(err).Equal(nil) - g.Assert(build.State).Equal("pending") - g.Assert(build.Number).Equal(3) - }) - - g.It("Should get the recent list of builds", func() { - db.SetBuild(repo, &common.Build{State: "success"}) - db.SetBuild(repo, &common.Build{State: "success"}) - db.SetBuild(repo, &common.Build{State: "pending"}) - - builds, err := db.BuildList(repo) - g.Assert(err).Equal(nil) - g.Assert(len(builds)).Equal(3) - }) - - g.It("Should set build status: SetBuildStatus()", func() { - //err := db.SetRepoNotExists(testUser, testRepo) - err := db.SetRepoNotExists(&common.User{Login: testUser}, &common.Repo{FullName: testRepo}) - g.Assert(err).Equal(nil) - - db.SetBuild(repo, &common.Build{State: "error"}) - db.SetBuild(repo, &common.Build{State: "pending"}) - db.SetBuild(repo, &common.Build{State: "success"}) - err_ := db.SetBuildStatus(repo, 1, &common.Status{Context: "pending"}) - g.Assert(err_).Equal(nil) - err_ = db.SetBuildStatus(repo, 2, &common.Status{Context: "running"}) - g.Assert(err_).Equal(nil) - err_ = db.SetBuildStatus(repo, 3, &common.Status{Context: "success"}) - g.Assert(err_).Equal(nil) - }) - - g.It("Should set build state: SetBuildState()", func() { - err := db.SetRepoNotExists(&common.User{Login: testUser}, &common.Repo{FullName: testRepo}) - g.Assert(err).Equal(nil) - - db.SetBuild(repo, &common.Build{State: "error"}) - db.SetBuild(repo, &common.Build{State: "pending"}) - db.SetBuild(repo, &common.Build{State: "success"}) - err_ := db.SetBuildState(repo, &common.Build{Number: 1}) - g.Assert(err_).Equal(nil) - err_ = db.SetBuildState(repo, &common.Build{Number: 2}) - g.Assert(err_).Equal(nil) - err_ = db.SetBuildState(repo, &common.Build{Number: 3}) - g.Assert(err_).Equal(nil) - }) - - g.It("Should set build task: SetBuildTask()", func() { - err_ := db.SetRepoNotExists(&common.User{Login: testUser}, &common.Repo{FullName: testRepo}) - g.Assert(err_).Equal(nil) - // setting up tasks. - tasks := []*common.Task{ - &common.Task{ - Number: 1, - State: "pending", - ExitCode: 0, - }, - &common.Task{ - Number: 2, - State: "running", - ExitCode: 0, - }, - &common.Task{ - Number: 3, - State: "success", - ExitCode: 0, - }, - } - // setting up builds. - err_ = db.SetBuild(repo, &common.Build{Number: 1, State: "failed", Tasks: tasks}) - g.Assert(err_).Equal(nil) - err_ = db.SetBuildTask(repo, 1, &common.Task{Number: 1, State: "error", ExitCode: -1}) - g.Assert(err_).Equal(nil) - db.SetBuild(repo, &common.Build{Number: 2, State: "success", Tasks: tasks}) - err_ = db.SetBuildTask(repo, 2, &common.Task{Number: 1, State: "success", ExitCode: 0}) - g.Assert(err_).Equal(nil) - }) - }) -} diff --git a/datastore/builtin/commit.go b/datastore/builtin/commit.go new file mode 100644 index 000000000..9ffeccff7 --- /dev/null +++ b/datastore/builtin/commit.go @@ -0,0 +1,169 @@ +package builtin + +import ( + "database/sql" + "time" + + "github.com/drone/drone/common" + "github.com/russross/meddler" +) + +type Commitstore struct { + *sql.DB +} + +func NewCommitstore(db *sql.DB) *Commitstore { + return &Commitstore{db} +} + +// Commit gets a commit by ID +func (db *Commitstore) Commit(id int64) (*common.Commit, error) { + var commit = new(common.Commit) + var err = meddler.Load(db, commitTable, commit, id) + return commit, err +} + +// CommitSeq gets the specified commit sequence for the +// named repository and commit number +func (db *Commitstore) CommitSeq(repo *common.Repo, seq int) (*common.Commit, error) { + var commit = new(common.Commit) + var err = meddler.QueryRow(db, commit, rebind(commitNumberQuery), repo.ID, seq) + return commit, err +} + +// CommitLast gets the last executed commit for the +// named repository. +func (db *Commitstore) CommitLast(repo *common.Repo, branch string) (*common.Commit, error) { + var commit = new(common.Commit) + var err = meddler.QueryRow(db, commit, rebind(commitLastQuery), repo.ID, branch) + return commit, err +} + +// CommitList gets a list of recent commits for the +// named repository. +func (db *Commitstore) CommitList(repo *common.Repo, limit, offset int) ([]*common.Commit, error) { + var commits []*common.Commit + var err = meddler.QueryAll(db, &commits, rebind(commitListQuery), repo.ID, limit, offset) + return commits, err +} + +// AddCommit inserts a new commit in the datastore. +func (db *Commitstore) AddCommit(commit *common.Commit) error { + tx, err := db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + // extract the next commit number from the database + row := tx.QueryRow(rebind(commitNumberLast), commit.RepoID) + if row != nil { + row.Scan(&commit.Sequence) + } + + commit.Sequence = commit.Sequence + 1 // increment + commit.Created = time.Now().UTC().Unix() + commit.Updated = time.Now().UTC().Unix() + err = meddler.Insert(tx, commitTable, commit) + if err != nil { + return err + } + + for _, build := range commit.Builds { + build.CommitID = commit.ID + build.Created = commit.Created + build.Updated = commit.Updated + err := meddler.Insert(tx, buildTable, build) + if err != nil { + return err + } + } + return tx.Commit() +} + +// SetCommit updates an existing commit and commit tasks. +func (db *Commitstore) SetCommit(commit *common.Commit) error { + tx, err := db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + commit.Updated = time.Now().UTC().Unix() + err = meddler.Update(tx, commitTable, commit) + if err != nil { + return err + } + + for _, build := range commit.Builds { + build.Updated = commit.Updated + err := meddler.Update(tx, buildTable, build) + if err != nil { + return err + } + } + return tx.Commit() +} + +// KillCommits updates all pending or started commits +// in the datastore settings the status to killed. +func (db *Commitstore) KillCommits() error { + var _, err1 = db.Exec(rebind(buildKillStmt)) + if err1 != nil { + return err1 + } + var _, err2 = db.Exec(rebind(commitKillStmt)) + return err2 +} + +// Commit table name in database. +const commitTable = "commits" + +// SQL query to retrieve the latest commits across all branches. +const commitListQuery = ` +SELECT * +FROM commits +WHERE repo_id = ? +ORDER BY commit_seq DESC +LIMIT ? OFFSET ? +` + +// SQL query to retrieve a commit by number. +const commitNumberQuery = ` +SELECT * +FROM commits +WHERE repo_id = ? + AND commit_seq = ? +LIMIT 1 +` + +// SQL query to retrieve the most recent commit. +// TODO exclude pull requests +const commitLastQuery = ` +SELECT * +FROM commits +WHERE repo_id = ? + AND commit_branch = ? +ORDER BY commit_seq DESC +LIMIT 1 +` + +// SQL statement to cancel all running commits. +const commitKillStmt = ` +UPDATE commits SET commit_state = 'killed' +WHERE commit_state IN ('pending', 'running'); +` + +// SQL statement to cancel all running commits. +const buildKillStmt = ` +UPDATE builds SET build_state = 'killed' +WHERE build_state IN ('pending', 'running'); +` + +// SQL statement to retrieve the commit number for +// a commit +const commitNumberLast = ` +SELECT MAX(commit_seq) +FROM commits +WHERE repo_id = ? +` diff --git a/datastore/builtin/commit_test.go b/datastore/builtin/commit_test.go new file mode 100644 index 000000000..2a49d790f --- /dev/null +++ b/datastore/builtin/commit_test.go @@ -0,0 +1,178 @@ +package builtin + +import ( + "testing" + + "github.com/drone/drone/common" + "github.com/franela/goblin" +) + +func TestCommitstore(t *testing.T) { + db := mustConnectTest() + bs := NewCommitstore(db) + defer db.Close() + + g := goblin.Goblin(t) + g.Describe("Commitstore", func() { + + // before each test be sure to purge the package + // table data from the database. + g.BeforeEach(func() { + db.Exec("DELETE FROM commits") + db.Exec("DELETE FROM tasks") + }) + + g.It("Should Post a Commit", func() { + commit := common.Commit{ + RepoID: 1, + State: common.StateSuccess, + Ref: "refs/heads/master", + Sha: "85f8c029b902ed9400bc600bac301a0aadb144ac", + } + err := bs.AddCommit(&commit) + g.Assert(err == nil).IsTrue() + g.Assert(commit.ID != 0).IsTrue() + g.Assert(commit.Sequence).Equal(1) + }) + + g.It("Should Put a Commit", func() { + commit := common.Commit{ + RepoID: 1, + Sequence: 5, + State: common.StatePending, + Ref: "refs/heads/master", + Sha: "85f8c029b902ed9400bc600bac301a0aadb144ac", + } + bs.AddCommit(&commit) + commit.State = common.StateRunning + err1 := bs.SetCommit(&commit) + getcommit, err2 := bs.Commit(commit.ID) + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsTrue() + g.Assert(commit.ID).Equal(getcommit.ID) + g.Assert(commit.RepoID).Equal(getcommit.RepoID) + g.Assert(commit.State).Equal(getcommit.State) + g.Assert(commit.Sequence).Equal(getcommit.Sequence) + }) + + g.It("Should Get a Commit", func() { + commit := common.Commit{ + RepoID: 1, + State: common.StateSuccess, + } + bs.AddCommit(&commit) + getcommit, err := bs.Commit(commit.ID) + g.Assert(err == nil).IsTrue() + g.Assert(commit.ID).Equal(getcommit.ID) + g.Assert(commit.RepoID).Equal(getcommit.RepoID) + g.Assert(commit.State).Equal(getcommit.State) + }) + + g.It("Should Get a Commit by Sequence", func() { + commit1 := &common.Commit{ + RepoID: 1, + State: common.StatePending, + Ref: "refs/heads/master", + Sha: "85f8c029b902ed9400bc600bac301a0aadb144ac", + } + commit2 := &common.Commit{ + RepoID: 1, + State: common.StatePending, + Ref: "refs/heads/dev", + Sha: "85f8c029b902ed9400bc600bac301a0aadb144ac", + } + err1 := bs.AddCommit(commit1) + err2 := bs.AddCommit(commit2) + getcommit, err3 := bs.CommitSeq(&common.Repo{ID: 1}, commit2.Sequence) + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsTrue() + g.Assert(err3 == nil).IsTrue() + g.Assert(commit2.ID).Equal(getcommit.ID) + g.Assert(commit2.RepoID).Equal(getcommit.RepoID) + g.Assert(commit2.Sequence).Equal(getcommit.Sequence) + }) + + g.It("Should Kill Pending or Started Commits", func() { + commit1 := &common.Commit{ + RepoID: 1, + State: common.StateRunning, + Ref: "refs/heads/master", + Sha: "85f8c029b902ed9400bc600bac301a0aadb144ac", + } + commit2 := &common.Commit{ + RepoID: 1, + State: common.StatePending, + Ref: "refs/heads/dev", + Sha: "85f8c029b902ed9400bc600bac301a0aadb144ac", + } + bs.AddCommit(commit1) + bs.AddCommit(commit2) + err1 := bs.KillCommits() + getcommit1, err2 := bs.Commit(commit1.ID) + getcommit2, err3 := bs.Commit(commit2.ID) + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsTrue() + g.Assert(err3 == nil).IsTrue() + g.Assert(getcommit1.State).Equal(common.StateKilled) + g.Assert(getcommit2.State).Equal(common.StateKilled) + }) + + g.It("Should get recent Commits", func() { + commit1 := &common.Commit{ + RepoID: 1, + State: common.StateFailure, + Ref: "refs/heads/master", + Sha: "85f8c029b902ed9400bc600bac301a0aadb144ac", + } + commit2 := &common.Commit{ + RepoID: 1, + State: common.StateSuccess, + Ref: "refs/heads/dev", + Sha: "85f8c029b902ed9400bc600bac301a0aadb144ac", + } + bs.AddCommit(commit1) + bs.AddCommit(commit2) + commits, err := bs.CommitList(&common.Repo{ID: 1}, 20, 0) + g.Assert(err == nil).IsTrue() + g.Assert(len(commits)).Equal(2) + g.Assert(commits[0].ID).Equal(commit2.ID) + g.Assert(commits[0].RepoID).Equal(commit2.RepoID) + g.Assert(commits[0].State).Equal(commit2.State) + }) + + g.It("Should get the last Commit", func() { + commit1 := &common.Commit{ + RepoID: 1, + State: common.StateFailure, + Branch: "master", + Ref: "refs/heads/master", + Sha: "85f8c029b902ed9400bc600bac301a0aadb144ac", + } + commit2 := &common.Commit{ + RepoID: 1, + State: common.StateFailure, + Branch: "master", + Ref: "refs/heads/master", + Sha: "8d6a233744a5dcacbf2605d4592a4bfe8b37320d", + } + commit3 := &common.Commit{ + RepoID: 1, + State: common.StateSuccess, + Branch: "dev", + Ref: "refs/heads/dev", + Sha: "85f8c029b902ed9400bc600bac301a0aadb144ac", + } + err1 := bs.AddCommit(commit1) + err2 := bs.AddCommit(commit2) + err3 := bs.AddCommit(commit3) + last, err4 := bs.CommitLast(&common.Repo{ID: 1}, "master") + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsTrue() + g.Assert(err3 == nil).IsTrue() + g.Assert(err4 == nil).IsTrue() + g.Assert(last.ID).Equal(commit2.ID) + g.Assert(last.RepoID).Equal(commit2.RepoID) + g.Assert(last.Sequence).Equal(commit2.Sequence) + }) + }) +} diff --git a/datastore/builtin/datastore.go b/datastore/builtin/datastore.go new file mode 100644 index 000000000..fec2f01fd --- /dev/null +++ b/datastore/builtin/datastore.go @@ -0,0 +1,92 @@ +package builtin + +import ( + "database/sql" + "os" + + "github.com/drone/drone/datastore" + "github.com/drone/drone/datastore/builtin/migrate" + + "github.com/BurntSushi/migration" + _ "github.com/lib/pq" + _ "github.com/mattn/go-sqlite3" + "github.com/russross/meddler" +) + +const ( + driverPostgres = "postgres" + driverSqlite = "sqlite3" + driverMysql = "mysql" +) + +// Connect is a helper function that establishes a new +// database connection and auto-generates the database +// schema. If the database already exists, it will perform +// and update as needed. +func Connect(driver, datasource string) (*sql.DB, error) { + switch driver { + case driverPostgres: + meddler.Default = meddler.PostgreSQL + case driverSqlite: + meddler.Default = meddler.SQLite + } + + migration.DefaultGetVersion = migrate.GetVersion + migration.DefaultSetVersion = migrate.SetVersion + var migrations = []migration.Migrator{ + migrate.Setup, + } + return migration.Open(driver, datasource, migrations) +} + +// MustConnect is a helper function that creates a +// new database connection and auto-generates the +// database schema. An error causes a panic. +func MustConnect(driver, datasource string) *sql.DB { + db, err := Connect(driver, datasource) + if err != nil { + panic(err) + } + return db +} + +// mustConnectTest is a helper function that creates a +// new database connection using environment variables. +// If not environment varaibles are found, the default +// in-memory SQLite database is used. +func mustConnectTest() *sql.DB { + var ( + driver = os.Getenv("TEST_DRIVER") + datasource = os.Getenv("TEST_DATASOURCE") + ) + if len(driver) == 0 { + driver = driverSqlite + datasource = ":memory:" + } + db, err := Connect(driver, datasource) + if err != nil { + panic(err) + } + return db +} + +// New returns a new Datastore +func New(db *sql.DB) datastore.Datastore { + return struct { + *Userstore + *Repostore + *Commitstore + *Buildstore + *Blobstore + *Starstore + *Tokenstore + }{ + NewUserstore(db), + NewRepostore(db), + NewCommitstore(db), + NewBuildstore(db), + NewBlobstore(db), + NewStarstore(db), + NewTokenstore(db), + } +} diff --git a/datastore/builtin/migrate/helper.go b/datastore/builtin/migrate/helper.go new file mode 100644 index 000000000..dfe242c60 --- /dev/null +++ b/datastore/builtin/migrate/helper.go @@ -0,0 +1,47 @@ +package migrate + +import ( + "strconv" + "strings" + + "github.com/russross/meddler" +) + +// transform is a helper function that transforms sql +// statements to work with multiple database types. +func transform(stmt string) string { + switch meddler.Default { + case meddler.MySQL: + stmt = strings.Replace(stmt, "AUTOINCREMENT", "AUTO_INCREMENT", -1) + stmt = strings.Replace(stmt, "BLOB", "MEDIUMBLOB", -1) + case meddler.PostgreSQL: + stmt = strings.Replace(stmt, "INTEGER PRIMARY KEY AUTOINCREMENT", "SERIAL PRIMARY KEY", -1) + stmt = strings.Replace(stmt, "BLOB", "BYTEA", -1) + } + return stmt +} + +// rebind is a helper function that changes the sql +// bind type from ? to $ for postgres queries. +func rebind(query string) string { + if meddler.Default != meddler.PostgreSQL { + return query + } + + qb := []byte(query) + // Add space enough for 10 params before we have to allocate + rqb := make([]byte, 0, len(qb)+10) + j := 1 + for _, b := range qb { + if b == '?' { + rqb = append(rqb, '$') + for _, b := range strconv.Itoa(j) { + rqb = append(rqb, byte(b)) + } + j++ + } else { + rqb = append(rqb, b) + } + } + return string(rqb) +} diff --git a/datastore/builtin/migrate/migrate.go b/datastore/builtin/migrate/migrate.go new file mode 100644 index 000000000..971b3d3a1 --- /dev/null +++ b/datastore/builtin/migrate/migrate.go @@ -0,0 +1,213 @@ +package migrate + +import ( + "github.com/BurntSushi/migration" +) + +// Setup is the database migration function that +// will setup the initial SQL database structure. +func Setup(tx migration.LimitedTx) error { + var stmts = []string{ + userTable, + starTable, + repoTable, + repoKeyTable, + repoKeyIndex, + repoParamTable, + repoParamsIndex, + repoUserIndex, + commitTable, + commitRepoIndex, + tokenTable, + buildTable, + buildCommitIndex, + statusTable, + statusCommitIndex, + blobTable, + } + for _, stmt := range stmts { + _, err := tx.Exec(transform(stmt)) + if err != nil { + return err + } + } + return nil +} + +var userTable = ` +CREATE TABLE IF NOT EXISTS users ( + user_id INTEGER PRIMARY KEY AUTOINCREMENT + ,user_login VARCHAR(255) + ,user_token VARCHAR(255) + ,user_secret VARCHAR(255) + ,user_name VARCHAR(255) + ,user_email VARCHAR(255) + ,user_gravatar VARCHAR(255) + ,user_admin BOOLEAN + ,user_active BOOLEAN + ,user_created INTEGER + ,user_updated INTEGER + ,UNIQUE(user_token) + ,UNIQUE(user_login) +); +` + +var repoTable = ` +CREATE TABLE IF NOT EXISTS repos ( + repo_id INTEGER PRIMARY KEY AUTOINCREMENT + ,user_id INTEGER + ,repo_owner VARCHAR(255) + ,repo_name VARCHAR(255) + ,repo_slug VARCHAR(1024) + ,repo_token VARCHAR(255) + ,repo_lang VARCHAR(255) + ,repo_branch VARCHAR(255) + ,repo_private BOOLEAN + ,repo_trusted BOOLEAN + ,repo_link VARCHAR(1024) + ,repo_clone VARCHAR(1024) + ,repo_push BOOLEAN + ,repo_pull BOOLEAN + ,repo_public_key BLOB + ,repo_private_key BLOB + ,repo_params BLOB + ,repo_timeout INTEGER + ,repo_created INTEGER + ,repo_updated INTEGER + ,UNIQUE(repo_owner, repo_name) + ,UNIQUE(repo_slug) +); +` + +var repoUserIndex = ` +CREATE INDEX repos_user_idx ON repos (user_id); +` + +var repoKeyTable = ` +CREATE TABLE IF NOT EXISTS repo_keys ( + keys_id INTEGER PRIMARY KEY AUTOINCREMENT + ,repo_id INTEGER + ,keys_public BLOB + ,keys_private BLOB + ,UNIQUE(repo_id) +); +` + +var repoKeyIndex = ` +CREATE INDEX keys_repo_idx ON repo_keys (repo_id); +` + +var repoParamTable = ` +CREATE TABLE IF NOT EXISTS repo_params ( + param_id INTEGER PRIMARY KEY AUTOINCREMENT + ,repo_id INTEGER + ,param_map BLOB + ,UNIQUE(repo_id) +); +` + +var repoParamsIndex = ` +CREATE INDEX params_repo_idx ON repo_params (repo_id); +` + +var starTable = ` +CREATE TABLE IF NOT EXISTS stars ( + star_id INTEGER PRIMARY KEY AUTOINCREMENT + ,user_id INTEGER + ,repo_id INTEGER + ,UNIQUE (repo_id, user_id) +); +` + +var commitTable = ` +CREATE TABLE IF NOT EXISTS commits ( + commit_id INTEGER PRIMARY KEY AUTOINCREMENT + ,repo_id INTEGER + ,commit_seq INTEGER + ,commit_state VARCHAR(255) + ,commit_started INTEGER + ,commit_finished INTEGER + ,commit_sha VARCHAR(255) + ,commit_ref VARCHAR(255) + ,commit_branch VARCHAR(255) + ,commit_pr VARCHAR(255) + ,commit_author VARCHAR(255) + ,commit_gravatar VARCHAR(255) + ,commit_timestamp VARCHAR(255) + ,commit_message VARCHAR(1000) + ,commit_source_remote VARCHAR(255) + ,commit_source_branch VARCHAR(255) + ,commit_source_sha VARCHAR(255) + ,commit_created INTEGER + ,commit_updated INTEGER + ,UNIQUE(repo_id, commit_seq) + ,UNIQUE(repo_id, commit_sha, commit_ref) +); +` + +var commitRepoIndex = ` +CREATE INDEX commits_repo_idx ON commits (repo_id); +` + +var tokenTable = ` +CREATE TABLE IF NOT EXISTS tokens ( + token_id INTEGER PRIMARY KEY AUTOINCREMENT + ,user_id INTEGER + ,token_kind VARCHAR(255) + ,token_label VARCHAR(255) + ,token_expiry INTEGER + ,token_issued INTEGER + ,UNIQUE(user_id, token_label) +); +` + +var tokenUserIndex = ` +CREATE INDEX tokens_user_idx ON tokens (user_id); +` + +var buildTable = ` +CREATE TABLE IF NOT EXISTS builds ( + build_id INTEGER PRIMARY KEY AUTOINCREMENT + ,commit_id INTEGER + ,build_seq INTEGER + ,build_state VARCHAR(255) + ,build_exit INTEGER + ,build_duration INTEGER + ,build_started INTEGER + ,build_finished INTEGER + ,build_created INTEGER + ,build_updated INTEGER + ,build_env BLOB + ,UNIQUE(commit_id, build_seq) +); +` + +var buildCommitIndex = ` +CREATE INDEX builds_commit_idx ON builds (commit_id); +` + +var statusTable = ` +CREATE TABLE IF NOT EXISTS status ( + status_id INTEGER PRIMARY KEY AUTOINCREMENT + ,commit_id INTEGER + ,status_state VARCHAR(255) + ,status_desc VARCHAR(2000) + ,status_link VARCHAR(2000) + ,status_context INTEGER + ,status_attachment BOOL + ,UNIQUE(commit_id, status_context) +); +` + +var statusCommitIndex = ` +CREATE INDEX status_commit_idx ON status (commit_id); +` + +var blobTable = ` +CREATE TABLE IF NOT EXISTS blobs ( + blob_id INTEGER PRIMARY KEY AUTOINCREMENT + ,blob_path VARCHAR(255) + ,blob_data BLOB + ,UNIQUE(blob_path) +); +` diff --git a/datastore/builtin/migrate/version.go b/datastore/builtin/migrate/version.go new file mode 100644 index 000000000..ad96209e7 --- /dev/null +++ b/datastore/builtin/migrate/version.go @@ -0,0 +1,57 @@ +package migrate + +import ( + "github.com/BurntSushi/migration" +) + +// GetVersion gets the migration version from the database, +// creating the migration table if it does not already exist. +func GetVersion(tx migration.LimitedTx) (int, error) { + v, err := getVersion(tx) + if err != nil { + if err := createVersionTable(tx); err != nil { + return 0, err + } + return getVersion(tx) + } + return v, nil +} + +// SetVersion sets the migration version in the database, +// creating the migration table if it does not already exist. +func SetVersion(tx migration.LimitedTx, version int) error { + if err := setVersion(tx, version); err != nil { + if err := createVersionTable(tx); err != nil { + return err + } + return setVersion(tx, version) + } + return nil +} + +// setVersion updates the migration version in the database. +func setVersion(tx migration.LimitedTx, version int) error { + _, err := tx.Exec(rebind("UPDATE migration_version SET version = ?"), version) + return err +} + +// getVersion gets the migration version in the database. +func getVersion(tx migration.LimitedTx) (int, error) { + var version int + row := tx.QueryRow("SELECT version FROM migration_version") + if err := row.Scan(&version); err != nil { + return 0, err + } + return version, nil +} + +// createVersionTable creates the version table and inserts the +// initial value (0) into the database. +func createVersionTable(tx migration.LimitedTx) error { + _, err := tx.Exec("CREATE TABLE migration_version ( version INTEGER )") + if err != nil { + return err + } + _, err = tx.Exec("INSERT INTO migration_version (version) VALUES (0)") + return err +} diff --git a/datastore/builtin/repo.go b/datastore/builtin/repo.go index 2ccee704c..14a3cd18b 100644 --- a/datastore/builtin/repo.go +++ b/datastore/builtin/repo.go @@ -1,197 +1,138 @@ package builtin import ( - "bytes" + "database/sql" "time" - "github.com/boltdb/bolt" "github.com/drone/drone/common" + "github.com/russross/meddler" ) -// Repo returns the repository with the given name. -func (db *DB) Repo(repo string) (*common.Repo, error) { - repo_ := &common.Repo{} - key := []byte(repo) - - err := db.View(func(t *bolt.Tx) error { - return get(t, bucketRepo, key, repo_) - }) - - return repo_, err +type Repostore struct { + *sql.DB } -// RepoList returns a list of repositories for the -// given user account. -func (db *DB) RepoList(login string) ([]*common.Repo, error) { - repos := []*common.Repo{} - err := db.View(func(t *bolt.Tx) error { - // get the index of user tokens and unmarshal - // to a string array. - var keys [][]byte - err := get(t, bucketUserRepos, []byte(login), &keys) - if err != nil && err != ErrKeyNotFound { - return err - } +func NewRepostore(db *sql.DB) *Repostore { + return &Repostore{db} +} - // for each item in the index, get the repository - // and append to the array - for _, key := range keys { - repo := &common.Repo{} - err := get(t, bucketRepo, key, repo) - if err == ErrKeyNotFound { - // TODO if we come across ErrKeyNotFound it means - // we need to re-build the index - continue - } else if err != nil { - return err - } - repos = append(repos, repo) - } - return nil - }) +// Repo retrieves a specific repo from the +// datastore for the given ID. +func (db *Repostore) Repo(id int64) (*common.Repo, error) { + var repo = new(common.Repo) + var err = meddler.Load(db, repoTable, repo, id) + return repo, err +} +// RepoName retrieves a repo from the datastore +// for the specified name. +func (db *Repostore) RepoName(owner, name string) (*common.Repo, error) { + var repo = new(common.Repo) + var err = meddler.QueryRow(db, repo, rebind(repoNameQuery), owner, name) + return repo, err +} + +// RepoList retrieves a list of all repos from +// the datastore accessible by the given user ID. +func (db *Repostore) RepoList(user *common.User) ([]*common.Repo, error) { + var repos []*common.Repo + var err = meddler.QueryAll(db, &repos, rebind(repoListQuery), user.ID) return repos, err } -// RepoParams returns the private environment parameters -// for the given repository. -func (db *DB) RepoParams(repo string) (map[string]string, error) { - params := map[string]string{} - key := []byte(repo) +// // RepoKeys retrieves a set of repository keys from +// // the datastore for the specified name. +// func (db *Repostore) RepoKeypair(repo *common.Repo) (*common.Keypair, error) { +// var keypair = new(common.Keypair) +// var err = meddler.QueryRow(db, keypair, rebind(repoKeysQuery), repo.ID) +// return keypair, err +// } - err := db.View(func(t *bolt.Tx) error { - return get(t, bucketRepoParams, key, ¶ms) - }) +// // RepoParams retrieves a set of repository params from +// // the datastore for the specified name. +// func (db *Repostore) RepoParams(repo *common.Repo) (*common.Params, error) { +// var params = new(common.Params) +// var err = meddler.QueryRow(db, params, rebind(repoParamsQuery), repo.ID) +// return params, err +// } - return params, err -} - -// RepoKeypair returns the private and public rsa keys -// for the given repository. -func (db *DB) RepoKeypair(repo string) (*common.Keypair, error) { - keypair := &common.Keypair{} - key := []byte(repo) - - err := db.View(func(t *bolt.Tx) error { - return get(t, bucketRepoKeys, key, keypair) - }) - - return keypair, err -} - -// SetRepo inserts or updates a repository. -func (db *DB) SetRepo(repo *common.Repo) error { - key := []byte(repo.FullName) - repo.Updated = time.Now().UTC().Unix() - - return db.Update(func(t *bolt.Tx) error { - return update(t, bucketRepo, key, repo) - }) -} - -// SetRepoNotExists updates a repository. If the repository -// already exists ErrConflict is returned. -func (db *DB) SetRepoNotExists(user *common.User, repo *common.Repo) error { - repokey := []byte(repo.FullName) +// AddRepo inserts a repo in the datastore. +func (db *Repostore) AddRepo(repo *common.Repo) error { repo.Created = time.Now().UTC().Unix() repo.Updated = time.Now().UTC().Unix() - - return db.Update(func(t *bolt.Tx) error { - userkey := []byte(user.Login) - err := push(t, bucketUserRepos, userkey, repokey) - if err != nil { - return err - } - return insert(t, bucketRepo, repokey, repo) - }) + return meddler.Insert(db, repoTable, repo) } -// SetRepoParams inserts or updates the private -// environment parameters for the named repository. -func (db *DB) SetRepoParams(repo string, params map[string]string) error { - key := []byte(repo) - - return db.Update(func(t *bolt.Tx) error { - return update(t, bucketRepoParams, key, params) - }) +// SetRepo updates a repo in the datastore. +func (db *Repostore) SetRepo(repo *common.Repo) error { + repo.Updated = time.Now().UTC().Unix() + return meddler.Update(db, repoTable, repo) } -// SetRepoKeypair inserts or updates the private and -// public keypair for the named repository. -func (db *DB) SetRepoKeypair(repo string, keypair *common.Keypair) error { - key := []byte(repo) +// // SetRepoKeypair upserts a keypair in the datastore. +// func (db *Repostore) SetRepoKeypair(keys *common.Keypair) error { +// return meddler.Save(db, repoKeyTable, keys) +// } - return db.Update(func(t *bolt.Tx) error { - return update(t, bucketRepoKeys, key, keypair) - }) +// // SetRepoKeypair upserts a param set in the datastore. +// func (db *Repostore) SetRepoParams(params *common.Params) error { +// return meddler.Save(db, repoParamTable, params) +// } + +// DelRepo removes the repo from the datastore. +func (db *Repostore) DelRepo(repo *common.Repo) error { + var _, err = db.Exec(rebind(repoDeleteStmt), repo.ID) + return err } -// DelRepo deletes the repository. -func (db *DB) DelRepo(repo *common.Repo) error { - key := []byte(repo.FullName) +// Repo table names in database. +const ( + repoTable = "repos" + repoKeyTable = "repo_keys" + repoParamTable = "repo_params" +) - return db.Update(func(t *bolt.Tx) error { - err := t.Bucket(bucketRepo).Delete(key) - if err != nil { - return err - } - t.Bucket(bucketRepoKeys).Delete(key) - t.Bucket(bucketRepoParams).Delete(key) - t.Bucket(bucketBuildSeq).Delete(key) - deleteWithPrefix(t, bucketBuild, append(key, '/')) - deleteWithPrefix(t, bucketBuildLogs, append(key, '/')) - deleteWithPrefix(t, bucketBuildStatus, append(key, '/')) +// SQL statement to retrieve a Repo by name. +const repoNameQuery = ` +SELECT * +FROM repos +WHERE repo_owner = ? + AND repo_name = ? +LIMIT 1; +` - return err - }) -} +// SQL statement to retrieve a list of Repos +// with permissions for the given User ID. +const repoListQuery = ` +SELECT r.* +FROM + repos r +,stars s +WHERE r.repo_id = s.repo_id + AND s.user_id = ? +` -// Subscribed returns true if the user is subscribed -// to the named repository. -// -// TODO (bradrydzewski) we are currently storing the subscription -// data in a wrapper element called common.Subscriber. This is -// no longer necessary. -func (db *DB) Subscribed(login, repo string) (bool, error) { - sub := &common.Subscriber{} - err := db.View(func(t *bolt.Tx) error { - repokey := []byte(repo) +// SQL statement to retrieve a keypair for +// a Repository. +const repoKeysQuery = ` +SELECT * +FROM repo_keys +WHERE repo_id = ? +LIMIT 1; +` - // get the index of user tokens and unmarshal - // to a string array. - var keys [][]byte - err := get(t, bucketUserRepos, []byte(login), &keys) - if err != nil && err != ErrKeyNotFound { - return err - } +// SQL statement to retrieve a keypair for +// a Repository. +const repoParamsQuery = ` +SELECT * +FROM repo_params +WHERE repo_id = ? +LIMIT 1; +` - for _, key := range keys { - if bytes.Equal(repokey, key) { - sub.Subscribed = true - return nil - } - } - return nil - }) - return sub.Subscribed, err -} - -// SetSubscriber inserts a subscriber for the named -// repository. -func (db *DB) SetSubscriber(login, repo string) error { - return db.Update(func(t *bolt.Tx) error { - userkey := []byte(login) - repokey := []byte(repo) - return push(t, bucketUserRepos, userkey, repokey) - }) -} - -// DelSubscriber removes the subscriber by login for the -// named repository. -func (db *DB) DelSubscriber(login, repo string) error { - return db.Update(func(t *bolt.Tx) error { - userkey := []byte(login) - repokey := []byte(repo) - return splice(t, bucketUserRepos, userkey, repokey) - }) -} +// SQL statement to delete a User by ID. +const ( + repoDeleteStmt = `DELETE FROM repos WHERE repo_id = ?` + repoKeypairDeleteStmt = `DELETE FROM repo_params WHERE repo_id = ?` + repoParamsDeleteStmt = `DELETE FROM repo_keys WHERE repo_id = ?` +) diff --git a/datastore/builtin/repo_test.go b/datastore/builtin/repo_test.go index 998aadb0d..1be077248 100644 --- a/datastore/builtin/repo_test.go +++ b/datastore/builtin/repo_test.go @@ -1,161 +1,169 @@ package builtin import ( - "bytes" - "github.com/drone/drone/common" - . "github.com/franela/goblin" - "io/ioutil" - "os" "testing" + + "github.com/drone/drone/common" + "github.com/franela/goblin" ) -func TestRepo(t *testing.T) { - g := Goblin(t) - g.Describe("Repo", func() { - testUser := "octocat" - testRepo := "github.com/octopod/hq" - testRepo2 := "github.com/octopod/avengers" - commUser := &common.User{Login: "freya"} - var db *DB // Temp database +func TestRepostore(t *testing.T) { + db := mustConnectTest() + rs := NewRepostore(db) + ss := NewStarstore(db) + defer db.Close() - // create a new database before each unit test and destroy afterwards. + g := goblin.Goblin(t) + g.Describe("Repostore", func() { + + // before each test be sure to purge the package + // table data from the database. g.BeforeEach(func() { - file, err := ioutil.TempFile(os.TempDir(), "drone-bolt") - if err != nil { - panic(err) + db.Exec("DELETE FROM stars") + db.Exec("DELETE FROM repos") + db.Exec("DELETE FROM users") + }) + + g.It("Should Set a Repo", func() { + repo := common.Repo{ + UserID: 1, + Owner: "bradrydzewski", + Name: "drone", } - - db = Must(file.Name()) - }) - g.AfterEach(func() { - os.Remove(db.Path()) + err1 := rs.AddRepo(&repo) + err2 := rs.SetRepo(&repo) + getrepo, err3 := rs.Repo(repo.ID) + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsTrue() + g.Assert(err3 == nil).IsTrue() + g.Assert(repo.ID).Equal(getrepo.ID) }) - g.It("Should set Repo", func() { - err := db.SetRepo(&common.Repo{FullName: testRepo}) - g.Assert(err).Equal(nil) - - repo, err := db.Repo(testRepo) - g.Assert(err).Equal(nil) - g.Assert(repo.FullName).Equal(testRepo) + g.It("Should Add a Repo", func() { + repo := common.Repo{ + UserID: 1, + Owner: "bradrydzewski", + Name: "drone", + } + err := rs.AddRepo(&repo) + g.Assert(err == nil).IsTrue() + g.Assert(repo.ID != 0).IsTrue() }) - g.It("Should get Repo", func() { - db.SetRepo(&common.Repo{FullName: testRepo}) + // g.It("Should Add a Repos Keypair", func() { + // keypair := common.Keypair{ + // RepoID: 1, + // Public: []byte("-----BEGIN RSA PRIVATE KEY----- ..."), + // Private: []byte("ssh-rsa AAAAE1BzbF1xc2EABAvVA6Z ..."), + // } + // err := rs.SetRepoKeypair(&keypair) + // g.Assert(err == nil).IsTrue() + // g.Assert(keypair.ID != 0).IsTrue() + // getkeypair, err := rs.RepoKeypair(&common.Repo{ID: 1}) + // g.Assert(err == nil).IsTrue() + // g.Assert(keypair.ID).Equal(getkeypair.ID) + // g.Assert(keypair.RepoID).Equal(getkeypair.RepoID) + // g.Assert(keypair.Public).Equal(getkeypair.Public) + // g.Assert(keypair.Private).Equal(getkeypair.Private) + // }) - repo, err := db.Repo(testRepo) - g.Assert(err).Equal(nil) - g.Assert(repo.FullName).Equal(testRepo) + // g.It("Should Add a Repos Private Params", func() { + // params := common.Params{ + // RepoID: 1, + // Map: map[string]string{"foo": "bar"}, + // } + // err := rs.SetRepoParams(¶ms) + // g.Assert(err == nil).IsTrue() + // g.Assert(params.ID != 0).IsTrue() + // getparams, err := rs.RepoParams(&common.Repo{ID: 1}) + // g.Assert(err == nil).IsTrue() + // g.Assert(params.ID).Equal(getparams.ID) + // g.Assert(params.RepoID).Equal(getparams.RepoID) + // g.Assert(params.Map).Equal(getparams.Map) + // }) + + g.It("Should Get a Repo by ID", func() { + repo := common.Repo{ + UserID: 1, + Owner: "bradrydzewski", + Name: "drone", + } + rs.AddRepo(&repo) + getrepo, err := rs.Repo(repo.ID) + g.Assert(err == nil).IsTrue() + g.Assert(repo.ID).Equal(getrepo.ID) + g.Assert(repo.UserID).Equal(getrepo.UserID) + g.Assert(repo.Owner).Equal(getrepo.Owner) + g.Assert(repo.Name).Equal(getrepo.Name) }) - g.It("Should be deletable", func() { - db.SetRepo(&common.Repo{FullName: testRepo}) - - db.Repo(testRepo) - err_ := db.DelRepo((&common.Repo{FullName: testRepo})) - g.Assert(err_).Equal(nil) + g.It("Should Get a Repo by Name", func() { + repo := common.Repo{ + UserID: 1, + Owner: "bradrydzewski", + Name: "drone", + } + rs.AddRepo(&repo) + getrepo, err := rs.RepoName(repo.Owner, repo.Name) + g.Assert(err == nil).IsTrue() + g.Assert(repo.ID).Equal(getrepo.ID) + g.Assert(repo.UserID).Equal(getrepo.UserID) + g.Assert(repo.Owner).Equal(getrepo.Owner) + g.Assert(repo.Name).Equal(getrepo.Name) }) - g.It("Should cleanup builds when deleted", func() { - repo := &common.Repo{FullName: testRepo} - err := db.SetRepoNotExists(commUser, repo) - g.Assert(err).Equal(nil) - - db.SetBuild(testRepo, &common.Build{State: "success"}) - db.SetBuild(testRepo, &common.Build{State: "success"}) - db.SetBuild(testRepo, &common.Build{State: "pending"}) - db.SetLogs(testRepo, 1, 1, (bytes.NewBuffer([]byte("foo")))) - - // first a little sanity to validate our test conditions - _, err = db.BuildLast(testRepo) - g.Assert(err).Equal(nil) - - // now run our specific test suite - // 1. ensure that we can delete the repo - err = db.DelRepo(repo) - g.Assert(err).Equal(nil) - - // 2. ensure that deleting the repo cleans up other references - _, err = db.Build(testRepo, 1) - g.Assert(err).Equal(ErrKeyNotFound) + g.It("Should Get a Repo List by User", func() { + repo1 := common.Repo{ + UserID: 1, + Owner: "bradrydzewski", + Name: "drone", + } + repo2 := common.Repo{ + UserID: 1, + Owner: "bradrydzewski", + Name: "drone-dart", + } + rs.AddRepo(&repo1) + rs.AddRepo(&repo2) + ss.AddStar(&common.User{ID: 1}, &repo1) + repos, err := rs.RepoList(&common.User{ID: 1}) + g.Assert(err == nil).IsTrue() + g.Assert(len(repos)).Equal(1) + g.Assert(repos[0].UserID).Equal(repo1.UserID) + g.Assert(repos[0].Owner).Equal(repo1.Owner) + g.Assert(repos[0].Name).Equal(repo1.Name) }) - g.It("Should get Repo list", func() { - db.SetRepoNotExists(&common.User{Login: testUser}, &common.Repo{FullName: testRepo}) - db.SetRepoNotExists(&common.User{Login: testUser}, &common.Repo{FullName: testRepo2}) - - repos, err := db.RepoList(testUser) - g.Assert(err).Equal(nil) - g.Assert(len(repos)).Equal(2) + g.It("Should Delete a Repo", func() { + repo := common.Repo{ + UserID: 1, + Owner: "bradrydzewski", + Name: "drone", + } + rs.AddRepo(&repo) + _, err1 := rs.Repo(repo.ID) + err2 := rs.DelRepo(&repo) + _, err3 := rs.Repo(repo.ID) + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsTrue() + g.Assert(err3 == nil).IsFalse() }) - g.It("Should set Repo parameters", func() { - db.SetRepo(&common.Repo{FullName: testRepo}) - err := db.SetRepoParams(testRepo, map[string]string{"A": "Alpha"}) - g.Assert(err).Equal(nil) + g.It("Should Enforce Unique Repo Name", func() { + repo1 := common.Repo{ + UserID: 1, + Owner: "bradrydzewski", + Name: "drone", + } + repo2 := common.Repo{ + UserID: 2, + Owner: "bradrydzewski", + Name: "drone", + } + err1 := rs.AddRepo(&repo1) + err2 := rs.AddRepo(&repo2) + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsFalse() }) - - g.It("Should get Repo parameters", func() { - db.SetRepo(&common.Repo{FullName: testRepo}) - err := db.SetRepoParams(testRepo, map[string]string{"A": "Alpha", "B": "Beta"}) - params, err := db.RepoParams(testRepo) - g.Assert(err).Equal(nil) - g.Assert(len(params)).Equal(2) - g.Assert(params["A"]).Equal("Alpha") - g.Assert(params["B"]).Equal("Beta") - }) - - // we test again with same repo/user already existing - // to see if it will return "ErrConflict" - g.It("Should set SetRepoNotExists", func() { - err := db.SetRepoNotExists(&common.User{Login: testUser}, &common.Repo{FullName: testRepo}) - g.Assert(err).Equal(nil) - // We should get ErrConflict now, trying to add the same repo again. - err_ := db.SetRepoNotExists(&common.User{Login: testUser}, &common.Repo{FullName: testRepo}) - g.Assert(err_).Equal(ErrKeyExists) - }) - - g.It("Should set Repo keypair", func() { - db.SetRepo(&common.Repo{FullName: testRepo}) - - err := db.SetRepoKeypair(testRepo, &common.Keypair{Private: "A", Public: "Alpha"}) - g.Assert(err).Equal(nil) - }) - - g.It("Should get Repo keypair", func() { - db.SetRepo(&common.Repo{FullName: testRepo}) - err := db.SetRepoKeypair(testRepo, &common.Keypair{Private: "A", Public: "Alpha"}) - - keypair, err := db.RepoKeypair(testRepo) - g.Assert(err).Equal(nil) - g.Assert(keypair.Public).Equal("Alpha") - g.Assert(keypair.Private).Equal("A") - }) - - g.It("Should set subscriber", func() { - db.SetRepo(&common.Repo{FullName: testRepo}) - err := db.SetSubscriber(testUser, testRepo) - g.Assert(err).Equal(nil) - }) - - g.It("Should get subscribed", func() { - db.SetRepo(&common.Repo{FullName: testRepo}) - err := db.SetSubscriber(testUser, testRepo) - subscribed, err := db.Subscribed(testUser, testRepo) - g.Assert(err).Equal(nil) - g.Assert(subscribed).Equal(true) - }) - - g.It("Should del subscriber", func() { - db.SetRepo(&common.Repo{FullName: testRepo}) - db.SetSubscriber(testUser, testRepo) - err := db.DelSubscriber(testUser, testRepo) - g.Assert(err).Equal(nil) - - subscribed, err := db.Subscribed(testUser, testRepo) - g.Assert(subscribed).Equal(false) - - }) - }) } diff --git a/datastore/builtin/star.go b/datastore/builtin/star.go new file mode 100644 index 000000000..d015abd51 --- /dev/null +++ b/datastore/builtin/star.go @@ -0,0 +1,60 @@ +package builtin + +import ( + "github.com/drone/drone/common" + "github.com/russross/meddler" +) + +type Starstore struct { + meddler.DB +} + +func NewStarstore(db meddler.DB) *Starstore { + return &Starstore{db} +} + +// Starred returns true if the user starred +// the given repository. +func (db *Starstore) Starred(user *common.User, repo *common.Repo) (bool, error) { + var star = new(star) + err := meddler.QueryRow(db, star, rebind(starQuery), user.ID, repo.ID) + return (err == nil), err +} + +// AddStar inserts a starred repo / user in the datastore. +func (db *Starstore) AddStar(user *common.User, repo *common.Repo) error { + var star = &star{UserID: user.ID, RepoID: repo.ID} + return meddler.Insert(db, starTable, star) +} + +// DelStar removes starred repo / user from the datastore. +func (db *Starstore) DelStar(user *common.User, repo *common.Repo) error { + var _, err = db.Exec(rebind(starDeleteStmt), user.ID, repo.ID) + return err +} + +type star struct { + ID int64 `meddler:"star_id,pk"` + UserID int64 `meddler:"user_id"` + RepoID int64 `meddler:"repo_id"` +} + +// Stars table name in database. +const starTable = "stars" + +// SQL query to retrieve a user's stars to +// access a repository. +const starQuery = ` +SELECT * +FROM stars +WHERE user_id=? +AND repo_id=? +LIMIT 1 +` + +// SQL statement to delete a star by ID. +const starDeleteStmt = ` +DELETE FROM stars +WHERE user_id=? + AND repo_id=? +` diff --git a/datastore/builtin/star_test.go b/datastore/builtin/star_test.go new file mode 100644 index 000000000..dbc8b571d --- /dev/null +++ b/datastore/builtin/star_test.go @@ -0,0 +1,60 @@ +package builtin + +import ( + "testing" + + "github.com/drone/drone/common" + "github.com/franela/goblin" +) + +func TestStarstore(t *testing.T) { + db := mustConnectTest() + ss := NewStarstore(db) + defer db.Close() + + g := goblin.Goblin(t) + g.Describe("Starstore", func() { + + // before each test be sure to purge the package + // table data from the database. + g.BeforeEach(func() { + db.Exec("DELETE FROM stars") + }) + + g.It("Should Add a Star", func() { + user := common.User{ID: 1} + repo := common.Repo{ID: 2} + err := ss.AddStar(&user, &repo) + g.Assert(err == nil).IsTrue() + }) + + g.It("Should Get Starred", func() { + user := common.User{ID: 1} + repo := common.Repo{ID: 2} + ss.AddStar(&user, &repo) + ok, err := ss.Starred(&user, &repo) + g.Assert(err == nil).IsTrue() + g.Assert(ok).IsTrue() + }) + + g.It("Should Not Get Starred", func() { + user := common.User{ID: 1} + repo := common.Repo{ID: 2} + ok, err := ss.Starred(&user, &repo) + g.Assert(err != nil).IsTrue() + g.Assert(ok).IsFalse() + }) + + g.It("Should Del a Star", func() { + user := common.User{ID: 1} + repo := common.Repo{ID: 2} + ss.AddStar(&user, &repo) + _, err1 := ss.Starred(&user, &repo) + err2 := ss.DelStar(&user, &repo) + _, err3 := ss.Starred(&user, &repo) + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsTrue() + g.Assert(err3 == nil).IsFalse() + }) + }) +} diff --git a/datastore/builtin/task.go b/datastore/builtin/task.go deleted file mode 100644 index 79bdd4209..000000000 --- a/datastore/builtin/task.go +++ /dev/null @@ -1,47 +0,0 @@ -package builtin - -import ( - "bytes" - "io" - "io/ioutil" - "strconv" - - "github.com/boltdb/bolt" -) - -// SetLogs inserts or updates a task logs for the -// named repository and build number. -func (db *DB) SetLogs(repo string, build int, task int, rd io.Reader) error { - key := []byte(repo + "/" + strconv.Itoa(build) + "/" + strconv.Itoa(task)) - t, err := db.Begin(true) - if err != nil { - return err - } - - log, err := ioutil.ReadAll(rd) - if err != nil { - return err - } - - err = t.Bucket(bucketBuildLogs).Put(key, log) - if err != nil { - t.Rollback() - return err - } - return t.Commit() -} - -// LogReader gets the task logs at index N for -// the named repository and build number. -func (db *DB) LogReader(repo string, build int, task int) (io.Reader, error) { - key := []byte(repo + "/" + strconv.Itoa(build) + "/" + strconv.Itoa(task)) - - var log []byte - err := db.View(func(t *bolt.Tx) error { - var err error - log, err = raw(t, bucketBuildLogs, key) - return err - }) - buf := bytes.NewBuffer(log) - return buf, err -} diff --git a/datastore/builtin/task_test.go b/datastore/builtin/task_test.go deleted file mode 100644 index 4d4a144d9..000000000 --- a/datastore/builtin/task_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package builtin - -import ( - "bytes" - "github.com/drone/drone/common" - . "github.com/franela/goblin" - "io/ioutil" - "os" - "testing" -) - -func TestTask(t *testing.T) { - g := Goblin(t) - g.Describe("Tasks", func() { - - testRepo := "octopod/hq" - testBuild := 1 - testTask := 0 - - testLogInfo := []byte("Log Info for SetLogs()") - var db *DB // Temp database - - // create a new database before each unit - // test and destroy afterwards. - g.BeforeEach(func() { - db = Must("/tmp/drone.test.db") - }) - g.AfterEach(func() { - os.Remove(db.Path()) - }) - - g.It("Should set Logs", func() { - db.SetRepo(&common.Repo{FullName: testRepo}) - err := db.SetLogs(testRepo, testBuild, testTask, (bytes.NewBuffer(testLogInfo))) - g.Assert(err).Equal(nil) - }) - - g.It("Should get logs", func() { - db.SetRepo(&common.Repo{FullName: testRepo}) - db.SetLogs(testRepo, testBuild, testTask, (bytes.NewBuffer(testLogInfo))) - buf, err := db.LogReader(testRepo, testBuild, testTask) - g.Assert(err).Equal(nil) - logInfo, err := ioutil.ReadAll(buf) - g.Assert(logInfo).Equal(testLogInfo) - }) - }) -} diff --git a/datastore/builtin/token.go b/datastore/builtin/token.go index 5775aefb2..3f21fd87a 100644 --- a/datastore/builtin/token.go +++ b/datastore/builtin/token.go @@ -1,70 +1,74 @@ package builtin import ( - "github.com/boltdb/bolt" "github.com/drone/drone/common" + "github.com/russross/meddler" ) -// Token returns the token for the given user and label. -func (db *DB) Token(user, label string) (*common.Token, error) { - token := &common.Token{} - key := []byte(user + "/" + label) +type Tokenstore struct { + meddler.DB +} - err := db.View(func(t *bolt.Tx) error { - return get(t, bucketTokens, key, token) - }) +func NewTokenstore(db meddler.DB) *Tokenstore { + return &Tokenstore{db} +} + +// Token returns a token by ID. +func (db *Tokenstore) Token(id int64) (*common.Token, error) { + var token = new(common.Token) + var err = meddler.Load(db, tokenTable, token, id) return token, err } -// TokenList returns a list of all tokens for the given -// user login. -func (db *DB) TokenList(login string) ([]*common.Token, error) { - tokens := []*common.Token{} - userkey := []byte(login) - err := db.Update(func(t *bolt.Tx) error { - // get the index of user tokens and unmarshal - // to a string array. - var keys [][]byte - err := get(t, bucketUserTokens, userkey, &keys) - if err != nil && err != ErrKeyNotFound { - return err - } - // for each item in the index, get the repository - // and append to the array - for _, key := range keys { - token := &common.Token{} - raw := t.Bucket(bucketTokens).Get(key) - err = decode(raw, token) - if err != nil { - return err - } - tokens = append(tokens, token) - } - return nil - }) +// TokenLabel returns a token by label +func (db *Tokenstore) TokenLabel(user *common.User, label string) (*common.Token, error) { + var token = new(common.Token) + var err = meddler.QueryRow(db, token, rebind(tokenLabelQuery), user.ID, label) + return token, err +} + +// TokenList returns a list of all user tokens. +func (db *Tokenstore) TokenList(user *common.User) ([]*common.Token, error) { + var tokens []*common.Token + var err = meddler.QueryAll(db, &tokens, rebind(tokenListQuery), user.ID) return tokens, err } -// SetToken inserts a new user token in the datastore. -func (db *DB) SetToken(token *common.Token) error { - key := []byte(token.Login + "/" + token.Label) - return db.Update(func(t *bolt.Tx) error { - err := push(t, bucketUserTokens, []byte(token.Login), key) - if err != nil { - return err - } - return insert(t, bucketTokens, key, token) - }) +// AddToken inserts a new token into the datastore. +// If the token label already exists for the user +// an error is returned. +func (db *Tokenstore) AddToken(token *common.Token) error { + return meddler.Insert(db, tokenTable, token) } -// DelToken deletes the token. -func (db *DB) DelToken(token *common.Token) error { - key := []byte(token.Login + "/" + token.Label) - return db.Update(func(t *bolt.Tx) error { - err := splice(t, bucketUserTokens, []byte(token.Login), key) - if err != nil { - return err - } - return delete(t, bucketTokens, key) - }) +// DelToken removes the DelToken from the datastore. +func (db *Tokenstore) DelToken(token *common.Token) error { + var _, err = db.Exec(rebind(tokenDeleteStmt), token.ID) + return err } + +// Token table name in database. +const tokenTable = "tokens" + +// SQL query to retrieve a token by label. +const tokenLabelQuery = ` +SELECT * +FROM tokens +WHERE user_id = ? + AND token_label = ? +LIMIT 1 +` + +// SQL query to retrieve a list of user tokens. +const tokenListQuery = ` +SELECT * +FROM tokens +WHERE user_id = ? +ORDER BY token_label ASC +` + +// SQL statement to delete a Token by ID. +const tokenDeleteStmt = ` +DELETE FROM tokens +WHERE token_id=? +` diff --git a/datastore/builtin/token_test.go b/datastore/builtin/token_test.go index 5afe57a86..eddf700df 100644 --- a/datastore/builtin/token_test.go +++ b/datastore/builtin/token_test.go @@ -1,65 +1,149 @@ package builtin import ( - "os" "testing" + "time" "github.com/drone/drone/common" - . "github.com/franela/goblin" + "github.com/franela/goblin" ) -func TestToken(t *testing.T) { - g := Goblin(t) - g.Describe("Tokens", func() { - var db *DB // temporary database +func TestTokenstore(t *testing.T) { + db := mustConnectTest() + ts := NewTokenstore(db) + defer db.Close() - // create a new database before each unit - // test and destroy afterwards. + g := goblin.Goblin(t) + g.Describe("Tokenstore", func() { + + // before each test be sure to purge the package + // table data from the database. g.BeforeEach(func() { - db = Must("/tmp/drone.test.db") - }) - g.AfterEach(func() { - os.Remove(db.Path()) + db.Exec("DELETE FROM tokens") }) - g.It("Should list for user", func() { - db.SetUserNotExists(&common.User{Login: "octocat"}) - err1 := db.SetToken(&common.Token{Login: "octocat", Label: "gist"}) - err2 := db.SetToken(&common.Token{Login: "octocat", Label: "github"}) - g.Assert(err1).Equal(nil) - g.Assert(err2).Equal(nil) - - list, err := db.TokenList("octocat") - g.Assert(err).Equal(nil) - g.Assert(len(list)).Equal(2) + g.It("Should Add a new Token", func() { + token := common.Token{ + UserID: 1, + Label: "foo", + Kind: common.TokenUser, + Issued: time.Now().Unix(), + Expiry: time.Now().Unix() + 1000, + } + err := ts.AddToken(&token) + g.Assert(err == nil).IsTrue() + g.Assert(token.ID != 0).IsTrue() }) - g.It("Should insert", func() { - db.SetUserNotExists(&common.User{Login: "octocat"}) - err := db.SetToken(&common.Token{Login: "octocat", Label: "gist"}) - g.Assert(err).Equal(nil) - - token, err := db.Token("octocat", "gist") - g.Assert(err).Equal(nil) - g.Assert(token.Label).Equal("gist") - g.Assert(token.Login).Equal("octocat") + g.It("Should get a Token", func() { + token := common.Token{ + UserID: 1, + Label: "foo", + Kind: common.TokenUser, + Issued: time.Now().Unix(), + Expiry: time.Now().Unix() + 1000, + } + err1 := ts.AddToken(&token) + gettoken, err2 := ts.Token(token.ID) + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsTrue() + g.Assert(token.ID).Equal(gettoken.ID) + g.Assert(token.Label).Equal(gettoken.Label) + g.Assert(token.Kind).Equal(gettoken.Kind) + g.Assert(token.Issued).Equal(gettoken.Issued) + g.Assert(token.Expiry).Equal(gettoken.Expiry) }) - g.It("Should delete", func() { - db.SetUserNotExists(&common.User{Login: "octocat"}) - err := db.SetToken(&common.Token{Login: "octocat", Label: "gist"}) - g.Assert(err).Equal(nil) + g.It("Should Get a Token By Label", func() { + token := common.Token{ + UserID: 1, + Label: "foo", + Kind: common.TokenUser, + Issued: time.Now().Unix(), + Expiry: time.Now().Unix() + 1000, + } + err1 := ts.AddToken(&token) + gettoken, err2 := ts.TokenLabel(&common.User{ID: 1}, "foo") + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsTrue() + g.Assert(token.ID).Equal(gettoken.ID) + g.Assert(token.Label).Equal(gettoken.Label) + g.Assert(token.Kind).Equal(gettoken.Kind) + g.Assert(token.Issued).Equal(gettoken.Issued) + g.Assert(token.Expiry).Equal(gettoken.Expiry) + }) - token, err := db.Token("octocat", "gist") - g.Assert(err).Equal(nil) - g.Assert(token.Label).Equal("gist") - g.Assert(token.Login).Equal("octocat") + g.It("Should Enforce Unique Token Label", func() { + token1 := common.Token{ + UserID: 1, + Label: "foo", + Kind: common.TokenUser, + Issued: time.Now().Unix(), + Expiry: time.Now().Unix() + 1000, + } + token2 := common.Token{ + UserID: 1, + Label: "foo", + Kind: common.TokenUser, + Issued: time.Now().Unix(), + Expiry: time.Now().Unix() + 1000, + } + err1 := ts.AddToken(&token1) + err2 := ts.AddToken(&token2) + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsFalse() + }) - err = db.DelToken(token) - g.Assert(err).Equal(nil) + g.It("Should Get a User Token List", func() { + token1 := common.Token{ + UserID: 1, + Label: "bar", + Kind: common.TokenUser, + Issued: time.Now().Unix(), + Expiry: time.Now().Unix() + 1000, + } + token2 := common.Token{ + UserID: 1, + Label: "foo", + Kind: common.TokenUser, + Issued: time.Now().Unix(), + Expiry: time.Now().Unix() + 1000, + } + token3 := common.Token{ + UserID: 2, + Label: "foo", + Kind: common.TokenUser, + Issued: time.Now().Unix(), + Expiry: time.Now().Unix() + 1000, + } + ts.AddToken(&token1) + ts.AddToken(&token2) + ts.AddToken(&token3) + tokens, err := ts.TokenList(&common.User{ID: 1}) + g.Assert(err == nil).IsTrue() + g.Assert(len(tokens)).Equal(2) + g.Assert(tokens[0].ID).Equal(token1.ID) + g.Assert(tokens[0].Label).Equal(token1.Label) + g.Assert(tokens[0].Kind).Equal(token1.Kind) + g.Assert(tokens[0].Issued).Equal(token1.Issued) + g.Assert(tokens[0].Expiry).Equal(token1.Expiry) + }) - token, err = db.Token("octocat", "gist") - g.Assert(err != nil).IsTrue() + g.It("Should Del a Token", func() { + token := common.Token{ + UserID: 1, + Label: "foo", + Kind: common.TokenUser, + Issued: time.Now().Unix(), + Expiry: time.Now().Unix() + 1000, + } + ts.AddToken(&token) + _, err1 := ts.Token(token.ID) + err2 := ts.DelToken(&token) + _, err3 := ts.Token(token.ID) + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsTrue() + g.Assert(err3 == nil).IsFalse() }) }) } diff --git a/datastore/builtin/user.go b/datastore/builtin/user.go index 4c9c5b668..7ebcd0ec1 100644 --- a/datastore/builtin/user.go +++ b/datastore/builtin/user.go @@ -3,85 +3,123 @@ package builtin import ( "time" - "github.com/boltdb/bolt" "github.com/drone/drone/common" + "github.com/russross/meddler" ) -// User returns a user by user login. -func (db *DB) User(login string) (*common.User, error) { - user := &common.User{} - key := []byte(login) - - err := db.View(func(t *bolt.Tx) error { - return get(t, bucketUser, key, user) - }) - - return user, err +type Userstore struct { + meddler.DB } -// UserCount returns a count of all registered users. -func (db *DB) UserCount() (int, error) { - var out int - var err = db.View(func(t *bolt.Tx) error { - out = t.Bucket(bucketUser).Stats().KeyN - return nil - }) - return out, err +func NewUserstore(db meddler.DB) *Userstore { + return &Userstore{db} +} + +// User returns a user by user ID. +func (db *Userstore) User(id int64) (*common.User, error) { + var usr = new(common.User) + var err = meddler.Load(db, userTable, usr, id) + return usr, err +} + +// UserLogin returns a user by user login. +func (db *Userstore) UserLogin(login string) (*common.User, error) { + var usr = new(common.User) + var err = meddler.QueryRow(db, usr, rebind(userLoginQuery), login) + return usr, err } // UserList returns a list of all registered users. -func (db *DB) UserList() ([]*common.User, error) { - users := []*common.User{} - err := db.View(func(t *bolt.Tx) error { - return t.Bucket(bucketUser).ForEach(func(key, raw []byte) error { - user := &common.User{} - err := decode(raw, user) - if err != nil { - return err - } - users = append(users, user) - return nil - }) - }) +func (db *Userstore) UserList() ([]*common.User, error) { + var users []*common.User + var err = meddler.QueryAll(db, &users, rebind(userListQuery)) return users, err } -// SetUser inserts or updates a user. -func (db *DB) SetUser(user *common.User) error { - key := []byte(user.Login) - user.Updated = time.Now().UTC().Unix() - - return db.Update(func(t *bolt.Tx) error { - return update(t, bucketUser, key, user) - }) +// UserFeed retrieves a digest of recent builds +// from the datastore accessible to the specified user. +func (db *Userstore) UserFeed(user *common.User, limit, offset int) ([]*common.RepoCommit, error) { + var builds []*common.RepoCommit + var err = meddler.QueryAll(db, &builds, rebind(userFeedQuery), user.ID, limit, offset) + return builds, err } -// SetUserNotExists inserts a new user into the datastore. -// If the user login already exists ErrConflict is returned. -func (db *DB) SetUserNotExists(user *common.User) error { - key := []byte(user.Login) +// UserCount returns a count of all registered users. +func (db *Userstore) UserCount() (int, error) { + var count = struct{ Count int }{} + var err = meddler.QueryRow(db, &count, rebind(userCountQuery)) + return count.Count, err +} + +// AddUser inserts a new user into the datastore. +// If the user login already exists an error is returned. +func (db *Userstore) AddUser(user *common.User) error { user.Created = time.Now().UTC().Unix() user.Updated = time.Now().UTC().Unix() - - return db.Update(func(t *bolt.Tx) error { - return insert(t, bucketUser, key, user) - }) + return meddler.Insert(db, userTable, user) } -// DelUser deletes the user. -func (db *DB) DelUser(user *common.User) error { - key := []byte(user.Login) - return db.Update(func(t *bolt.Tx) error { - err := delete(t, bucketUserTokens, key) - if err != nil { - return err - } - err = delete(t, bucketUserRepos, key) - if err != nil { - return err - } - // IDEA: deleteKeys(t, bucketTokens, keys) - deleteWithPrefix(t, bucketTokens, append(key, '/')) - return delete(t, bucketUser, key) - }) +// SetUser updates an existing user. +func (db *Userstore) SetUser(user *common.User) error { + user.Updated = time.Now().UTC().Unix() + return meddler.Update(db, userTable, user) } + +// DelUser removes the user from the datastore. +func (db *Userstore) DelUser(user *common.User) error { + var _, err = db.Exec(rebind(userDeleteStmt), user.ID) + return err +} + +// User table name in database. +const userTable = "users" + +// SQL query to retrieve a User by remote login. +const userLoginQuery = ` +SELECT * +FROM users +WHERE user_login=? +LIMIT 1 +` + +// SQL query to retrieve a list of all users. +const userListQuery = ` +SELECT * +FROM users +ORDER BY user_name ASC +` + +// SQL query to retrieve a list of all users. +const userCountQuery = ` +SELECT count(1) as "Count" +FROM users +` + +// SQL statement to delete a User by ID. +const userDeleteStmt = ` +DELETE FROM users +WHERE user_id=? +` + +// SQL query to retrieve a build feed for the given +// user account. +const userFeedQuery = ` +SELECT + r.repo_id +,r.repo_owner +,r.repo_name +,r.repo_slug +,c.commit_seq +,c.commit_state +,c.commit_started +,c.commit_finished +FROM + commits c +,repos r +,stars s +WHERE c.repo_id = r.repo_id + AND r.repo_id = s.repo_id + AND s.user_id = ? +ORDER BY c.commit_seq DESC +LIMIT ? OFFSET ? +` diff --git a/datastore/builtin/user_test.go b/datastore/builtin/user_test.go deleted file mode 100644 index 2fbaab598..000000000 --- a/datastore/builtin/user_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package builtin - -import ( - "os" - "testing" - - "github.com/drone/drone/common" - . "github.com/franela/goblin" -) - -func TestUser(t *testing.T) { - g := Goblin(t) - g.Describe("Users", func() { - var db *DB // temporary database - - // create a new database before each unit - // test and destroy afterwards. - g.BeforeEach(func() { - db = Must("/tmp/drone.test.db") - }) - g.AfterEach(func() { - os.Remove(db.Path()) - }) - - g.It("Should find", func() { - db.SetUserNotExists(&common.User{Login: "octocat"}) - user, err := db.User("octocat") - g.Assert(err).Equal(nil) - g.Assert(user.Login).Equal("octocat") - }) - - g.It("Should insert", func() { - err := db.SetUserNotExists(&common.User{Login: "octocat"}) - g.Assert(err).Equal(nil) - - user, err := db.User("octocat") - g.Assert(err).Equal(nil) - g.Assert(user.Login).Equal("octocat") - g.Assert(user.Created != 0).IsTrue() - g.Assert(user.Updated != 0).IsTrue() - }) - - g.It("Should not insert if exists", func() { - db.SetUser(&common.User{Login: "octocat"}) - err := db.SetUserNotExists(&common.User{Login: "octocat"}) - g.Assert(err).Equal(ErrKeyExists) - }) - - g.It("Should update", func() { - db.SetUserNotExists(&common.User{Login: "octocat"}) - user, err := db.User("octocat") - g.Assert(err).Equal(nil) - - user.Email = "octocat@github.com" - err = db.SetUser(user) - g.Assert(err).Equal(nil) - - user_, err := db.User("octocat") - g.Assert(err).Equal(nil) - g.Assert(user_.Login).Equal(user.Login) - g.Assert(user_.Email).Equal(user.Email) - }) - - g.It("Should delete", func() { - db.SetUserNotExists(&common.User{Login: "octocat"}) - user, err := db.User("octocat") - g.Assert(err).Equal(nil) - - err = db.DelUser(user) - g.Assert(err).Equal(nil) - - _, err = db.User("octocat") - g.Assert(err).Equal(ErrKeyNotFound) - }) - - g.It("Should list", func() { - db.SetUserNotExists(&common.User{Login: "bert"}) - db.SetUserNotExists(&common.User{Login: "ernie"}) - users, err := db.UserList() - g.Assert(err).Equal(nil) - g.Assert(len(users)).Equal(2) - }) - - g.It("Should count", func() { - db.SetUserNotExists(&common.User{Login: "bert"}) - db.SetUserNotExists(&common.User{Login: "ernie"}) - count, err := db.UserCount() - g.Assert(err).Equal(nil) - g.Assert(count).Equal(2) - }) - }) -} diff --git a/datastore/builtin/users_test.go b/datastore/builtin/users_test.go new file mode 100644 index 000000000..0eab41f11 --- /dev/null +++ b/datastore/builtin/users_test.go @@ -0,0 +1,224 @@ +package builtin + +import ( + "testing" + + "github.com/drone/drone/common" + "github.com/franela/goblin" +) + +func TestUserstore(t *testing.T) { + db := mustConnectTest() + us := NewUserstore(db) + cs := NewCommitstore(db) + rs := NewRepostore(db) + ss := NewStarstore(db) + defer db.Close() + + g := goblin.Goblin(t) + g.Describe("Userstore", func() { + + // before each test be sure to purge the package + // table data from the database. + g.BeforeEach(func() { + db.Exec("DELETE FROM users") + db.Exec("DELETE FROM stars") + db.Exec("DELETE FROM repos") + db.Exec("DELETE FROM builds") + db.Exec("DELETE FROM tasks") + }) + + g.It("Should Update a User", func() { + user := common.User{ + Login: "joe", + Name: "Joe Sixpack", + Email: "foo@bar.com", + Token: "e42080dddf012c718e476da161d21ad5", + } + err1 := us.AddUser(&user) + err2 := us.SetUser(&user) + getuser, err3 := us.User(user.ID) + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsTrue() + g.Assert(err3 == nil).IsTrue() + g.Assert(user.ID).Equal(getuser.ID) + }) + + g.It("Should Add a new User", func() { + user := common.User{ + Login: "joe", + Name: "Joe Sixpack", + Email: "foo@bar.com", + Token: "e42080dddf012c718e476da161d21ad5", + } + err := us.AddUser(&user) + g.Assert(err == nil).IsTrue() + g.Assert(user.ID != 0).IsTrue() + }) + + g.It("Should Get a User", func() { + user := common.User{ + Login: "joe", + Token: "f0b461ca586c27872b43a0685cbc2847", + Secret: "976f22a5eef7caacb7e678d6c52f49b1", + Name: "Joe Sixpack", + Email: "foo@bar.com", + Gravatar: "b9015b0857e16ac4d94a0ffd9a0b79c8", + Active: true, + Admin: true, + Created: 1398065343, + Updated: 1398065344, + } + us.AddUser(&user) + getuser, err := us.User(user.ID) + g.Assert(err == nil).IsTrue() + g.Assert(user.ID).Equal(getuser.ID) + g.Assert(user.Login).Equal(getuser.Login) + g.Assert(user.Token).Equal(getuser.Token) + g.Assert(user.Secret).Equal(getuser.Secret) + g.Assert(user.Name).Equal(getuser.Name) + g.Assert(user.Email).Equal(getuser.Email) + g.Assert(user.Gravatar).Equal(getuser.Gravatar) + g.Assert(user.Active).Equal(getuser.Active) + g.Assert(user.Admin).Equal(getuser.Admin) + g.Assert(user.Created).Equal(getuser.Created) + g.Assert(user.Updated).Equal(getuser.Updated) + }) + + g.It("Should Get a User By Login", func() { + user := common.User{ + Login: "joe", + Name: "Joe Sixpack", + Email: "foo@bar.com", + Token: "e42080dddf012c718e476da161d21ad5", + } + us.AddUser(&user) + getuser, err := us.UserLogin(user.Login) + g.Assert(err == nil).IsTrue() + g.Assert(user.ID).Equal(getuser.ID) + g.Assert(user.Login).Equal(getuser.Login) + }) + + g.It("Should Enforce Unique User Login", func() { + user1 := common.User{ + Login: "joe", + Name: "Joe Sixpack", + Email: "foo@bar.com", + Token: "e42080dddf012c718e476da161d21ad5", + } + user2 := common.User{ + Login: "joe", + Name: "Joe Sixpack", + Email: "foo@bar.com", + Token: "ab20g0ddaf012c744e136da16aa21ad9", + } + err1 := us.AddUser(&user1) + err2 := us.AddUser(&user2) + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsFalse() + }) + + g.It("Should Get a User List", func() { + user1 := common.User{ + Login: "jane", + Name: "Jane Doe", + Email: "foo@bar.com", + Token: "ab20g0ddaf012c744e136da16aa21ad9", + } + user2 := common.User{ + Login: "joe", + Name: "Joe Sixpack", + Email: "foo@bar.com", + Token: "e42080dddf012c718e476da161d21ad5", + } + us.AddUser(&user1) + us.AddUser(&user2) + users, err := us.UserList() + g.Assert(err == nil).IsTrue() + g.Assert(len(users)).Equal(2) + g.Assert(users[0].Login).Equal(user1.Login) + g.Assert(users[0].Name).Equal(user1.Name) + g.Assert(users[0].Email).Equal(user1.Email) + g.Assert(users[0].Token).Equal(user1.Token) + }) + + g.It("Should Get a User Count", func() { + user1 := common.User{ + Login: "jane", + Name: "Jane Doe", + Email: "foo@bar.com", + Token: "ab20g0ddaf012c744e136da16aa21ad9", + } + user2 := common.User{ + Login: "joe", + Name: "Joe Sixpack", + Email: "foo@bar.com", + Token: "e42080dddf012c718e476da161d21ad5", + } + us.AddUser(&user1) + us.AddUser(&user2) + count, err := us.UserCount() + g.Assert(err == nil).IsTrue() + g.Assert(count).Equal(2) + }) + + g.It("Should Del a User", func() { + user := common.User{ + Login: "joe", + Name: "Joe Sixpack", + Email: "foo@bar.com", + Token: "e42080dddf012c718e476da161d21ad5", + } + us.AddUser(&user) + _, err1 := us.User(user.ID) + err2 := us.DelUser(&user) + _, err3 := us.User(user.ID) + g.Assert(err1 == nil).IsTrue() + g.Assert(err2 == nil).IsTrue() + g.Assert(err3 == nil).IsFalse() + }) + + g.It("Should get the Build feed for a User", func() { + repo1 := &common.Repo{ + UserID: 1, + Owner: "bradrydzewski", + Name: "drone", + } + repo2 := &common.Repo{ + UserID: 2, + Owner: "drone", + Name: "drone", + } + rs.AddRepo(repo1) + rs.AddRepo(repo2) + ss.AddStar(&common.User{ID: 1}, repo1) + commit1 := &common.Commit{ + RepoID: 1, + State: common.StateFailure, + Ref: "refs/heads/master", + Sha: "85f8c029b902ed9400bc600bac301a0aadb144ac", + } + commit2 := &common.Commit{ + RepoID: 1, + State: common.StateSuccess, + Ref: "refs/heads/dev", + Sha: "85f8c029b902ed9400bc600bac301a0aadb144ac", + } + commit3 := &common.Commit{ + RepoID: 2, + State: common.StateSuccess, + Ref: "refs/heads/dev", + Sha: "85f8c029b902ed9400bc600bac301a0aadb144ac", + } + cs.AddCommit(commit1) + cs.AddCommit(commit2) + cs.AddCommit(commit3) + commits, err := us.UserFeed(&common.User{ID: 1}, 20, 0) + g.Assert(err == nil).IsTrue() + g.Assert(len(commits)).Equal(2) + g.Assert(commits[0].State).Equal(commit2.State) + g.Assert(commits[0].Owner).Equal("bradrydzewski") + g.Assert(commits[0].Name).Equal("drone") + }) + }) +} diff --git a/datastore/builtin/util.go b/datastore/builtin/util.go index ebaa39c1f..7d647c5a4 100644 --- a/datastore/builtin/util.go +++ b/datastore/builtin/util.go @@ -1,115 +1,32 @@ package builtin import ( - "bytes" + "strconv" - "github.com/boltdb/bolt" - //"github.com/youtube/vitess/go/bson" - "encoding/gob" + "github.com/russross/meddler" ) -func encode(v interface{}) ([]byte, error) { - var buf bytes.Buffer - var err = gob.NewEncoder(&buf).Encode(v) - return buf.Bytes(), err - //return bson.Marshal(v) -} - -func decode(raw []byte, v interface{}) error { - var buf bytes.Buffer - buf.Write(raw) - var err = gob.NewDecoder(&buf).Decode(v) - return err - //return bson.Unmarshal(raw, v) -} - -func get(t *bolt.Tx, bucket, key []byte, v interface{}) error { - raw := t.Bucket(bucket).Get(key) - if raw == nil { - return ErrKeyNotFound +// rebind is a helper function that changes the sql +// bind type from ? to $ for postgres queries. +func rebind(query string) string { + if meddler.Default != meddler.PostgreSQL { + return query } - return decode(raw, v) -} -func raw(t *bolt.Tx, bucket, key []byte) ([]byte, error) { - raw := t.Bucket(bucket).Get(key) - if raw == nil { - return nil, ErrKeyNotFound - } - return raw, nil -} - -func update(t *bolt.Tx, bucket, key []byte, v interface{}) error { - raw, err := encode(v) - if err != nil { - t.Rollback() - return err - } - return t.Bucket(bucket).Put(key, raw) -} - -func insert(t *bolt.Tx, bucket, key []byte, v interface{}) error { - raw, err := encode(v) - if err != nil { - t.Rollback() - return err - } - // verify the key does not already exists - // in the bucket. If exists, fail - if t.Bucket(bucket).Get(key) != nil { - return ErrKeyExists - } - return t.Bucket(bucket).Put(key, raw) -} - -func delete(t *bolt.Tx, bucket, key []byte) error { - return t.Bucket(bucket).Delete(key) -} - -func push(t *bolt.Tx, bucket, index, value []byte) error { - var keys [][]byte - err := get(t, bucket, index, &keys) - if err != nil && err != ErrKeyNotFound { - return err - } - // we shouldn't add a key that already exists - for _, key := range keys { - if bytes.Equal(key, value) { - return nil + qb := []byte(query) + // Add space enough for 5 params before we have to allocate + rqb := make([]byte, 0, len(qb)+5) + j := 1 + for _, b := range qb { + if b == '?' { + rqb = append(rqb, '$') + for _, b := range strconv.Itoa(j) { + rqb = append(rqb, byte(b)) + } + j++ + } else { + rqb = append(rqb, b) } } - keys = append(keys, value) - return update(t, bucket, index, &keys) -} - -func splice(t *bolt.Tx, bucket, index, value []byte) error { - var keys [][]byte - err := get(t, bucket, index, &keys) - if err != nil && err != ErrKeyNotFound { - return err - } - - for i, key := range keys { - if bytes.Equal(key, value) { - keys = keys[:i+copy(keys[i:], keys[i+1:])] - break - } - } - - return update(t, bucket, index, &keys) -} - -func deleteWithPrefix(t *bolt.Tx, bucket, prefix []byte) error { - var err error - - c := t.Bucket(bucket).Cursor() - for k, _ := c.Seek(prefix); bytes.HasPrefix(k, prefix); k, _ = c.Next() { - err = c.Delete() - if err != nil { - break - } - } - - // only error here is if our Tx is read-only - return err + return string(rqb) } diff --git a/datastore/datastore.go b/datastore/datastore.go index 3cd8469ce..a811234f2 100644 --- a/datastore/datastore.go +++ b/datastore/datastore.go @@ -1,144 +1,149 @@ package datastore import ( - "errors" "io" "github.com/drone/drone/common" ) -var ( - ErrConflict = errors.New("Key not unique") - ErrKeyNotFound = errors.New("Key not found") -) - type Datastore interface { - // User returns a user by user login. - User(string) (*common.User, error) - // UserCount returns a count of all registered users. - UserCount() (int, error) + // User returns a user by user ID. + User(id int64) (*common.User, error) + + // UserLogin returns a user by user login. + UserLogin(string) (*common.User, error) // UserList returns a list of all registered users. UserList() ([]*common.User, error) - // SetUser inserts or updates a user. + // UserFeed retrieves a digest of recent builds + // from the datastore accessible to the specified user. + UserFeed(*common.User, int, int) ([]*common.RepoCommit, error) + + // UserCount returns a count of all registered users. + UserCount() (int, error) + + // AddUser inserts a new user into the datastore. + // If the user login already exists an error is returned. + AddUser(*common.User) error + + // SetUser updates an existing user. SetUser(*common.User) error - // SetUserNotExists inserts a new user into the datastore. - // If the user login already exists ErrConflict is returned. - SetUserNotExists(*common.User) error - - // Del deletes the user. + // DelUser removes the user from the datastore. DelUser(*common.User) error - // Token returns the token for the given user and label. - Token(string, string) (*common.Token, error) + // Token returns a token by ID. + Token(int64) (*common.Token, error) - // TokenList returns a list of all tokens for the given - // user login. - TokenList(string) ([]*common.Token, error) + // TokenLabel returns a token by label + TokenLabel(*common.User, string) (*common.Token, error) - // SetToken inserts a new user token in the datastore. - SetToken(*common.Token) error + // TokenList returns a list of all user tokens. + TokenList(*common.User) ([]*common.Token, error) - // DelToken deletes the token. + // AddToken inserts a new token into the datastore. + // If the token label already exists for the user + // an error is returned. + AddToken(*common.Token) error + + // DelToken removes the DelToken from the datastore. DelToken(*common.Token) error - // Subscribed returns true if the user is subscribed - // to the named repository. - Subscribed(string, string) (bool, error) + // - // SetSubscriber inserts a subscriber for the named - // repository. - SetSubscriber(string, string) error + // Starred returns true if the user starred + // the given repository. + Starred(*common.User, *common.Repo) (bool, error) - // DelSubscriber removes the subscriber by login for the - // named repository. - DelSubscriber(string, string) error + // AddStar stars a repository. + AddStar(*common.User, *common.Repo) error - // Repo returns the repository with the given name. - Repo(string) (*common.Repo, error) + // DelStar unstars a repository. + DelStar(*common.User, *common.Repo) error - // RepoList returns a list of repositories for the - // given user account. - RepoList(string) ([]*common.Repo, error) + // - // RepoParams returns the private environment parameters - // for the given repository. - RepoParams(string) (map[string]string, error) + // Repo retrieves a specific repo from the + // datastore for the given ID. + Repo(id int64) (*common.Repo, error) - // RepoKeypair returns the private and public rsa keys - // for the given repository. - RepoKeypair(string) (*common.Keypair, error) + // RepoName retrieves a repo from the datastore + // for the specified name. + RepoName(owner, name string) (*common.Repo, error) - // SetRepo inserts or updates a repository. + // RepoList retrieves a list of all repos from + // the datastore accessible by the given user ID. + RepoList(*common.User) ([]*common.Repo, error) + + // AddRepo inserts a repo in the datastore. + AddRepo(*common.Repo) error + + // SetRepo updates a repo in the datastore. SetRepo(*common.Repo) error - // SetRepo updates a repository. If the repository - // already exists ErrConflict is returned. - SetRepoNotExists(*common.User, *common.Repo) error - - // SetRepoParams inserts or updates the private - // environment parameters for the named repository. - SetRepoParams(string, map[string]string) error - - // SetRepoKeypair inserts or updates the private and - // public keypair for the named repository. - SetRepoKeypair(string, *common.Keypair) error - - // DelRepo deletes the repository. + // DelRepo removes the repo from the datastore. DelRepo(*common.Repo) error - // Build gets the specified build number for the - // named repository and build number - Build(string, int) (*common.Build, error) + // - // BuildList gets a list of recent builds for the + // Commit gets a commit by ID + Commit(int64) (*common.Commit, error) + + // CommitSeq gets the specified commit sequence for the + // named repository and commit number + CommitSeq(*common.Repo, int) (*common.Commit, error) + + // CommitLast gets the last executed commit for the + // named repository and branch + CommitLast(*common.Repo, string) (*common.Commit, error) + + // CommitList gets a list of recent commits for the // named repository. - BuildList(string) ([]*common.Build, error) + CommitList(*common.Repo, int, int) ([]*common.Commit, error) - // BuildLast gets the last executed build for the - // named repository. - BuildLast(string) (*common.Build, error) + // AddCommit inserts a new commit in the datastore. + AddCommit(*common.Commit) error - // BuildAgent returns the agent that is being - // used to execute the build. - BuildAgent(string, int) (*common.Agent, error) + // SetCommit updates an existing commit and commit tasks. + SetCommit(*common.Commit) error - // SetBuild inserts or updates a build for the named - // repository. The build number is incremented and - // assigned to the provided build. - SetBuild(string, *common.Build) error + // KillCommits updates all pending or started commits + // in the datastore settings the status to killed. + KillCommits() error - // SetBuildState updates an existing build's start time, - // finish time, duration and state. No other fields are - // updated. - SetBuildState(string, *common.Build) error + // - // SetBuildStatus appends a new build status to an - // existing build record. - SetBuildStatus(string, int, *common.Status) error + // Build returns a build by ID. + Build(int64) (*common.Build, error) - // SetBuildTask updates an existing build task. The build - // and task must already exist. If the task does not exist - // an error is returned. - SetBuildTask(string, int, *common.Task) error + // BuildSeq returns a build by sequence number. + BuildSeq(*common.Commit, int) (*common.Build, error) - // SetBuildAgent insert or updates the agent that is - // running a build. - SetBuildAgent(string, int, *common.Agent) error + // BuildList returns a list of all commit builds + BuildList(*common.Commit) ([]*common.Build, error) - // DelBuildAgent purges the referce to the agent - // that ran a build. - DelBuildAgent(string, int) error + // SetBuild updates an existing build. + SetBuild(*common.Build) error - // LogReader gets the task logs at index N for - // the named repository and build number. - LogReader(string, int, int) (io.Reader, error) + // - // SetLogs inserts or updates a task logs for the - // named repository and build number. - SetLogs(string, int, int, io.Reader) error + // Get retrieves an object from the blobstore. + GetBlob(path string) ([]byte, error) + + // GetBlobReader retrieves an object from the blobstore. + // It is the caller's responsibility to call Close on + // the ReadCloser when finished reading. + GetBlobReader(path string) (io.ReadCloser, error) + + // Set inserts an object into the blobstore. + SetBlob(path string, data []byte) error + + // SetBlobReader inserts an object into the blobstore by + // consuming data from r until EOF. + SetBlobReader(path string, r io.Reader) error + + // Del removes an object from the blobstore. + DelBlob(path string) error } - diff --git a/drone.go b/drone.go index 30b391059..dbab94d20 100644 --- a/drone.go +++ b/drone.go @@ -31,8 +31,9 @@ func main() { panic(err) } - store := store.Must(settings.Database.Path) - defer store.Close() + db := store.MustConnect(settings.Database.Driver, settings.Database.Datasource) + store := store.New(db) + defer db.Close() remote := github.New(settings.Service) session := session.New(settings.Session) @@ -101,12 +102,12 @@ func main() { repo.POST("/watch", server.Subscribe) repo.DELETE("/unwatch", server.Unsubscribe) - repo.GET("/builds", server.GetBuilds) - repo.GET("/builds/:number", server.GetBuild) + repo.GET("/builds", server.GetCommits) + repo.GET("/builds/:number", server.GetCommit) repo.POST("/builds/:number", server.RunBuild) repo.DELETE("/builds/:number", server.KillBuild) - repo.GET("/logs/:number/:task", server.GetBuildLogs) - repo.POST("/status/:number", server.PostBuildStatus) + repo.GET("/logs/:number/:task", server.GetLogs) + // repo.POST("/status/:number", server.PostBuildStatus) } } @@ -123,20 +124,20 @@ func main() { hooks.POST("", server.PostHook) } - queue := api.Group("/queue") - { - queue.Use(server.MustAgent()) - queue.GET("", server.GetQueue) - queue.POST("/pull", server.PollBuild) + // queue := api.Group("/queue") + // { + // queue.Use(server.MustAgent()) + // queue.GET("", server.GetQueue) + // queue.POST("/pull", server.PollBuild) - push := queue.Group("/push/:owner/:name") - { - push.Use(server.SetRepo()) - push.POST("", server.PushBuild) - push.POST("/:build", server.PushTask) - push.POST("/:build/:task/logs", server.PushLogs) - } - } + // push := queue.Group("/push/:owner/:name") + // { + // push.Use(server.SetRepo()) + // push.POST("", server.PushBuild) + // push.POST("/:build", server.PushTask) + // push.POST("/:build/:task/logs", server.PushLogs) + // } + // } stream := api.Group("/stream") { diff --git a/queue/worker.go b/queue/worker.go index 912ef8b57..b7acfad8e 100644 --- a/queue/worker.go +++ b/queue/worker.go @@ -9,12 +9,12 @@ import ( // Work represents an item for work to be // processed by a worker. type Work struct { - User *common.User `json:"user"` - Repo *common.Repo `json:"repo"` - Build *common.Build `json:"build"` - Keys *common.Keypair `json:"keypair"` - Netrc *common.Netrc `json:"netrc"` - Yaml []byte `json:"yaml"` + User *common.User `json:"user"` + Repo *common.Repo `json:"repo"` + Commit *common.Commit `json:"commit"` + Keys *common.Keypair `json:"keypair"` + Netrc *common.Netrc `json:"netrc"` + Yaml []byte `json:"yaml"` } // represents a worker that has connected diff --git a/remote/github/github.go b/remote/github/github.go index 181477099..d9d34dff8 100644 --- a/remote/github/github.go +++ b/remote/github/github.go @@ -5,7 +5,9 @@ import ( "fmt" "net/http" "net/url" + "strconv" "strings" + "time" "github.com/drone/drone/common" "github.com/drone/drone/settings" @@ -103,15 +105,18 @@ func (g *GitHub) Repo(u *common.User, owner, name string) (*common.Repo, error) repo := &common.Repo{} repo.Owner = owner repo.Name = name - - if repo_.Language != nil { - repo.Language = *repo_.Language - } repo.FullName = *repo_.FullName repo.Link = *repo_.HTMLURL repo.Private = *repo_.Private repo.Clone = *repo_.CloneURL + if repo_.Language != nil { + repo.Language = *repo_.Language + } + if repo_.DefaultBranch != nil { + repo.Branch = *repo_.DefaultBranch + } + if g.PrivateMode { repo.Private = true } @@ -132,7 +137,6 @@ func (g *GitHub) Perm(u *common.User, owner, name string) (*common.Perm, error) return nil, err } m := &common.Perm{} - m.Login = u.Login m.Admin = (*repo.Permissions)["admin"] m.Push = (*repo.Permissions)["push"] m.Pull = (*repo.Permissions)["pull"] @@ -142,13 +146,13 @@ func (g *GitHub) Perm(u *common.User, owner, name string) (*common.Perm, error) // Script fetches the build script (.drone.yml) from the remote // repository and returns in string format. -func (g *GitHub) Script(u *common.User, r *common.Repo, b *common.Build) ([]byte, error) { +func (g *GitHub) Script(u *common.User, r *common.Repo, c *common.Commit) ([]byte, error) { client := NewClient(g.API, u.Token, g.SkipVerify) var sha string - if b.Commit != nil { - sha = b.Commit.Sha + if len(c.SourceSha) == 0 { + sha = c.Sha } else { - sha = b.PullRequest.Source.Sha + sha = c.SourceSha } return GetFile(client, r.Owner, r.Name, ".drone.yml", sha) } @@ -208,23 +212,21 @@ func (g *GitHub) Deactivate(u *common.User, r *common.Repo, link string) error { return DeleteHook(client, r.Owner, r.Name, link) } -func (g *GitHub) Status(u *common.User, r *common.Repo, b *common.Build, link string) error { +func (g *GitHub) Status(u *common.User, r *common.Repo, c *common.Commit, link string) error { client := NewClient(g.API, u.Token, g.SkipVerify) - var ref string - if b.Commit != nil { - ref = b.Commit.Ref - } else { - ref = b.PullRequest.Source.Ref + if len(c.PullRequest) == 0 { + return nil } - status := getStatus(b.State) - desc := getDesc(b.State) + + status := getStatus(c.State) + desc := getDesc(c.State) data := github.RepoStatus{ Context: github.String("Drone"), State: github.String(status), Description: github.String(desc), TargetURL: github.String(link), } - _, _, err := client.Repositories.CreateStatus(r.Owner, r.Name, ref, &data) + _, _, err := client.Repositories.CreateStatus(r.Owner, r.Name, c.SourceSha, &data) return err } @@ -253,23 +255,27 @@ func (g *GitHub) push(r *http.Request) (*common.Hook, error) { repo := &common.Repo{} repo.Owner = hook.Repo.Owner.Login + if len(repo.Owner) == 0 { + repo.Owner = hook.Repo.Owner.Name + } repo.Name = hook.Repo.Name - repo.Language = hook.Repo.Language repo.FullName = hook.Repo.FullName repo.Link = hook.Repo.HTMLURL repo.Private = hook.Repo.Private repo.Clone = hook.Repo.CloneURL + repo.Language = hook.Repo.Language + repo.Branch = hook.Repo.DefaultBranch commit := &common.Commit{} commit.Sha = hook.Head.ID commit.Ref = hook.Ref + commit.Branch = strings.Replace(commit.Ref, "refs/heads/", "", -1) commit.Message = hook.Head.Message commit.Timestamp = hook.Head.Timestamp - - commit.Author = &common.Author{} - commit.Author.Name = hook.Head.Author.Name - commit.Author.Email = hook.Head.Author.Email - commit.Author.Login = hook.Head.Author.Username + commit.Author = hook.Head.Author.Username + // commit.Author.Name = hook.Head.Author.Name + // commit.Author.Email = hook.Head.Author.Email + // commit.Author.Login = hook.Head.Author.Username // we should ignore github pages if commit.Ref == "refs/heads/gh-pages" { @@ -301,32 +307,31 @@ func (g *GitHub) pullRequest(r *http.Request) (*common.Hook, error) { repo := &common.Repo{} repo.Owner = *hook.Repo.Owner.Login repo.Name = *hook.Repo.Name - repo.Language = *hook.Repo.Language repo.FullName = *hook.Repo.FullName repo.Link = *hook.Repo.HTMLURL repo.Private = *hook.Repo.Private repo.Clone = *hook.Repo.CloneURL + if hook.Repo.Language != nil { + repo.Language = *hook.Repo.Language + } + if hook.Repo.DefaultBranch != nil { + repo.Branch = *hook.Repo.DefaultBranch + } - pr := &common.PullRequest{} - pr.Number = *hook.PullRequest.Number - pr.Title = *hook.PullRequest.Title + c := &common.Commit{} + c.PullRequest = strconv.Itoa(*hook.PullRequest.Number) + c.Message = *hook.PullRequest.Title + c.Sha = *hook.PullRequest.Base.SHA + c.Ref = *hook.PullRequest.Base.Ref + c.Ref = fmt.Sprintf("refs/pull/%s/merge", c.PullRequest) + c.Branch = *hook.PullRequest.Base.Ref + c.Timestamp = time.Now().UTC().Format("2006-01-02 15:04:05.000000000 +0000 MST") + c.Author = *hook.PullRequest.Base.User.Login + c.SourceRemote = *hook.PullRequest.Head.Repo.CloneURL + c.SourceBranch = *hook.PullRequest.Head.Ref + c.SourceSha = *hook.PullRequest.Head.SHA - pr.Source = &common.Commit{} - pr.Source.Sha = *hook.PullRequest.Head.SHA - pr.Source.Ref = *hook.PullRequest.Head.Ref - pr.Source.Author = &common.Author{} - pr.Source.Author.Login = *hook.PullRequest.User.Login - - pr.Source.Remote = &common.Remote{} - pr.Source.Remote.Clone = *hook.PullRequest.Head.Repo.CloneURL - pr.Source.Remote.Name = *hook.PullRequest.Head.Repo.Name - pr.Source.Remote.FullName = *hook.PullRequest.Head.Repo.FullName - - pr.Target = &common.Commit{} - pr.Target.Sha = *hook.PullRequest.Base.SHA - pr.Target.Ref = *hook.PullRequest.Base.Ref - - return &common.Hook{Repo: repo, PullRequest: pr}, nil + return &common.Hook{Repo: repo, Commit: c}, nil } type pushHook struct { @@ -353,13 +358,15 @@ type pushHook struct { Repo struct { Owner struct { Login string `json:"login"` + Name string `json:"name"` } `json:"owner"` - Name string `json:"name"` - FullName string `json:"full_name"` - Language string `json:"language"` - Private bool `json:"private"` - HTMLURL string `json:"html_url"` - CloneURL string `json:"clone_url"` + Name string `json:"name"` + FullName string `json:"full_name"` + Language string `json:"language"` + Private bool `json:"private"` + HTMLURL string `json:"html_url"` + CloneURL string `json:"clone_url"` + DefaultBranch string `json:"default_branch"` } `json:"repository"` } diff --git a/remote/remote.go b/remote/remote.go index a1b15498d..e855f1d0a 100644 --- a/remote/remote.go +++ b/remote/remote.go @@ -23,11 +23,11 @@ type Remote interface { // Script fetches the build script (.drone.yml) from the remote // repository and returns in string format. - Script(u *common.User, r *common.Repo, b *common.Build) ([]byte, error) + Script(u *common.User, r *common.Repo, c *common.Commit) ([]byte, error) // Status sends the commit status to the remote system. // An example would be the GitHub pull request status. - Status(u *common.User, r *common.Repo, b *common.Build, link string) error + Status(u *common.User, r *common.Repo, c *common.Commit, link string) error // Netrc returns a .netrc file that can be used to clone // private repositories from a remote system. diff --git a/runner/builtin/runner.go b/runner/builtin/runner.go index c56ee7fc9..d08524b12 100644 --- a/runner/builtin/runner.go +++ b/runner/builtin/runner.go @@ -2,8 +2,6 @@ package builtin import ( "bytes" - "crypto/sha1" - "encoding/hex" "encoding/json" "fmt" "io" @@ -51,42 +49,41 @@ func (r *Runner) Run(w *queue.Work) error { worker.Remove() } - // if any part of the build fails and leaves + // if any part of the commit fails and leaves // behind orphan sub-builds we need to cleanup // after ourselves. - if w.Build.State == common.StateRunning { + if w.Commit.State == common.StateRunning { // if any tasks are running or pending // we should mark them as complete. - for _, t := range w.Build.Tasks { - if t.State == common.StateRunning { - t.State = common.StateError - t.Finished = time.Now().UTC().Unix() - t.Duration = t.Finished - t.Started + for _, b := range w.Commit.Builds { + if b.State == common.StateRunning { + b.State = common.StateError + b.Finished = time.Now().UTC().Unix() + b.Duration = b.Finished - b.Started } - if t.State == common.StatePending { - t.State = common.StateError - t.Started = time.Now().UTC().Unix() - t.Finished = time.Now().UTC().Unix() - t.Duration = 0 + if b.State == common.StatePending { + b.State = common.StateError + b.Started = time.Now().UTC().Unix() + b.Finished = time.Now().UTC().Unix() + b.Duration = 0 } - r.SetTask(w.Repo, w.Build, t) + r.SetBuild(w.Repo, w.Commit, b) } // must populate build start - if w.Build.Started == 0 { - w.Build.Started = time.Now().UTC().Unix() + if w.Commit.Started == 0 { + w.Commit.Started = time.Now().UTC().Unix() } // mark the build as complete (with error) - w.Build.State = common.StateError - w.Build.Finished = time.Now().UTC().Unix() - w.Build.Duration = w.Build.Finished - w.Build.Started - r.SetBuild(w.User, w.Repo, w.Build) + w.Commit.State = common.StateError + w.Commit.Finished = time.Now().UTC().Unix() + r.SetCommit(w.User, w.Repo, w.Commit) } }() // marks the build as running - w.Build.Started = time.Now().UTC().Unix() - w.Build.State = common.StateRunning - err := r.SetBuild(w.User, w.Repo, w.Build) + w.Commit.Started = time.Now().UTC().Unix() + w.Commit.State = common.StateRunning + err := r.SetCommit(w.User, w.Repo, w.Commit) if err != nil { return err } @@ -101,23 +98,23 @@ func (r *Runner) Run(w *queue.Work) error { // loop through and execute the build and // clone steps for each build task. - for _, task := range w.Build.Tasks { + for _, task := range w.Commit.Builds { // marks the task as running task.State = common.StateRunning task.Started = time.Now().UTC().Unix() - err = r.SetTask(w.Repo, w.Build, task) + err = r.SetBuild(w.Repo, w.Commit, task) if err != nil { return err } work := &work{ - Repo: w.Repo, - Build: w.Build, - Keys: w.Keys, - Netrc: w.Netrc, - Yaml: w.Yaml, - Task: task, + Repo: w.Repo, + Commit: w.Commit, + Keys: w.Keys, + Netrc: w.Netrc, + Yaml: w.Yaml, + Build: task, } in, err := json.Marshal(work) if err != nil { @@ -125,7 +122,7 @@ func (r *Runner) Run(w *queue.Work) error { } worker := newWorkerTimeout(client, w.Repo.Timeout+10) // 10 minute buffer workers = append(workers, worker) - cname := cname(w.Repo.FullName, w.Build.Number, task.Number) + cname := cname(task) state, builderr := worker.Build(cname, in) switch { @@ -154,7 +151,7 @@ func (r *Runner) Run(w *queue.Work) error { defer rc.Close() StdCopy(&buf, &buf, rc) } - err = r.SetLogs(w.Repo, w.Build, task, ioutil.NopCloser(&buf)) + err = r.SetLogs(w.Repo, w.Commit, task, ioutil.NopCloser(&buf)) if err != nil { return err } @@ -162,7 +159,7 @@ func (r *Runner) Run(w *queue.Work) error { // update the task in the datastore task.Finished = time.Now().UTC().Unix() task.Duration = task.Finished - task.Started - err = r.SetTask(w.Repo, w.Build, task) + err = r.SetBuild(w.Repo, w.Commit, task) if err != nil { return err } @@ -170,28 +167,28 @@ func (r *Runner) Run(w *queue.Work) error { // update the build state if any of the sub-tasks // had a non-success status - w.Build.State = common.StateSuccess - for _, task := range w.Build.Tasks { - if task.State != common.StateSuccess { - w.Build.State = task.State + w.Commit.State = common.StateSuccess + for _, build := range w.Commit.Builds { + if build.State != common.StateSuccess { + w.Commit.State = build.State break } } - err = r.SetBuild(w.User, w.Repo, w.Build) + err = r.SetCommit(w.User, w.Repo, w.Commit) if err != nil { return err } // loop through and execute the notifications and // the destroy all containers afterward. - for i, task := range w.Build.Tasks { + for i, build := range w.Commit.Builds { work := &work{ - Repo: w.Repo, - Build: w.Build, - Keys: w.Keys, - Netrc: w.Netrc, - Yaml: w.Yaml, - Task: task, + Repo: w.Repo, + Commit: w.Commit, + Keys: w.Keys, + Netrc: w.Netrc, + Yaml: w.Yaml, + Build: build, } in, err := json.Marshal(work) if err != nil { @@ -204,21 +201,21 @@ func (r *Runner) Run(w *queue.Work) error { return nil } -func (r *Runner) Cancel(repo string, build, task int) error { +func (r *Runner) Cancel(build *common.Build) error { client, err := dockerclient.NewDockerClient(DockerHost, nil) if err != nil { return err } - return client.StopContainer(cname(repo, build, task), 30) + return client.StopContainer(cname(build), 30) } -func (r *Runner) Logs(repo string, build, task int) (io.ReadCloser, error) { +func (r *Runner) Logs(build *common.Build) (io.ReadCloser, error) { client, err := dockerclient.NewDockerClient(DockerHost, nil) if err != nil { return nil, err } // make sure this container actually exists - info, err := client.InspectContainer(cname(repo, build, task)) + info, err := client.InspectContainer(cname(build)) if err != nil { return nil, err } @@ -252,12 +249,8 @@ func (r *Runner) Logs(repo string, build, task int) (io.ReadCloser, error) { return pr, nil } -func cname(repo string, number, task int) string { - s := fmt.Sprintf("%s/%d/%d", repo, number, task) - h := sha1.New() - h.Write([]byte(s)) - hash := hex.EncodeToString(h.Sum(nil))[:10] - return fmt.Sprintf("drone-%s", hash) +func cname(build *common.Build) string { + return fmt.Sprintf("drone-%d", build.ID) } func (r *Runner) Poll(q queue.Queue) { diff --git a/runner/builtin/updater.go b/runner/builtin/updater.go index a22460e97..c635d12ff 100644 --- a/runner/builtin/updater.go +++ b/runner/builtin/updater.go @@ -2,6 +2,7 @@ package builtin import ( "encoding/json" + "fmt" "io" "github.com/drone/drone/common" @@ -11,9 +12,9 @@ import ( ) type Updater interface { - SetBuild(*common.User, *common.Repo, *common.Build) error - SetTask(*common.Repo, *common.Build, *common.Task) error - SetLogs(*common.Repo, *common.Build, *common.Task, io.ReadCloser) error + SetCommit(*common.User, *common.Repo, *common.Commit) error + SetBuild(*common.Repo, *common.Commit, *common.Build) error + SetLogs(*common.Repo, *common.Commit, *common.Build, io.ReadCloser) error } // NewUpdater returns an implementation of the Updater interface @@ -28,29 +29,15 @@ type updater struct { remote remote.Remote } -func (u *updater) SetBuild(user *common.User, r *common.Repo, b *common.Build) error { - err := u.store.SetBuildState(r.FullName, b) +func (u *updater) SetCommit(user *common.User, r *common.Repo, c *common.Commit) error { + err := u.store.SetCommit(c) if err != nil { return err } - // if the build is complete we may need to update - if b.State != common.StatePending && b.State != common.StateRunning { - repo, err := u.store.Repo(r.FullName) - if err == nil { - if repo.Last == nil || b.Number >= repo.Last.Number { - repo.Last = b - u.store.SetRepo(repo) - } - } + // TODO invoke remote.Status to set the build status - // err = u.remote.Status(user, r, b, "") - // if err != nil { - // - // } - } - - msg, err := json.Marshal(b) + msg, err := json.Marshal(c) if err != nil { return err } @@ -63,13 +50,13 @@ func (u *updater) SetBuild(user *common.User, r *common.Repo, b *common.Build) e return nil } -func (u *updater) SetTask(r *common.Repo, b *common.Build, t *common.Task) error { - err := u.store.SetBuildTask(r.FullName, b.Number, t) +func (u *updater) SetBuild(r *common.Repo, c *common.Commit, b *common.Build) error { + err := u.store.SetBuild(b) if err != nil { return err } - msg, err := json.Marshal(b) + msg, err := json.Marshal(c) if err != nil { return err } @@ -82,12 +69,7 @@ func (u *updater) SetTask(r *common.Repo, b *common.Build, t *common.Task) error return nil } -func (u *updater) SetLogs(r *common.Repo, b *common.Build, t *common.Task, rc io.ReadCloser) error { - //defer rc.Close() - //out, err := ioutil.ReadAll(rc) - //if err != nil { - // return err - //} - //return u.store.SetLogs(r.FullName, b.Number, t.Number, out) - return u.store.SetLogs(r.FullName, b.Number, t.Number, rc) +func (u *updater) SetLogs(r *common.Repo, c *common.Commit, b *common.Build, rc io.ReadCloser) error { + path := fmt.Sprintf("/logs/%s/%v/%v", r.FullName, c.Sequence, b.Sequence) + return u.store.SetBlobReader(path, rc) } diff --git a/runner/builtin/worker.go b/runner/builtin/worker.go index da7626b86..66397e9e4 100644 --- a/runner/builtin/worker.go +++ b/runner/builtin/worker.go @@ -49,12 +49,12 @@ var ( ) type work struct { - Repo *common.Repo `json:"repo"` - Build *common.Build `json:"build"` - Task *common.Task `json:"task"` - Keys *common.Keypair `json:"keys"` - Netrc *common.Netrc `json:"netrc"` - Yaml []byte `json:"yaml"` + Repo *common.Repo `json:"repo"` + Commit *common.Commit `json:"commit"` + Build *common.Build `json:"build"` + Keys *common.Keypair `json:"keys"` + Netrc *common.Netrc `json:"netrc"` + Yaml []byte `json:"yaml"` } type worker struct { diff --git a/runner/runner.go b/runner/runner.go index 0411eaf21..ac2fb16ea 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -9,14 +9,14 @@ import ( type Runner interface { Run(work *queue.Work) error - Cancel(repo string, build, task int) error - Logs(repo string, build, task int) (io.ReadCloser, error) + Cancel(*common.Build) error + Logs(*common.Build) (io.ReadCloser, error) } // Updater defines a set of functions that are required for // the runner to sent Drone updates during a build. type Updater interface { - SetBuild(*common.Repo, *common.Build) error - SetTask(*common.Repo, *common.Build, *common.Task) error - SetLogs(*common.Repo, *common.Build, *common.Task, io.ReadCloser) error + SetCommit(*common.User, *common.Repo, *common.Commit) error + SetBuild(*common.Repo, *common.Commit, *common.Build) error + SetLogs(*common.Repo, *common.Commit, *common.Build, io.ReadCloser) error } diff --git a/server/badge.go b/server/badge.go index 623df7006..2551455b7 100644 --- a/server/badge.go +++ b/server/badge.go @@ -24,6 +24,11 @@ var ( // func GetBadge(c *gin.Context) { var repo = ToRepo(c) + var store = ToDatastore(c) + var branch = c.Request.FormValue("branch") + if len(branch) == 0 { + branch = repo.Branch + } // an SVG response is always served, even when error, so // we can go ahead and set the content type appropriately. @@ -32,12 +37,13 @@ func GetBadge(c *gin.Context) { // if no commit was found then display // the 'none' badge, instead of throwing // an error response - if repo.Last == nil { + commit, err := store.CommitLast(repo, branch) + if err != nil { c.Writer.Write(badgeNone) return } - switch repo.Last.State { + switch commit.State { case common.StateSuccess: c.Writer.Write(badgeSuccess) case common.StateFailure: @@ -61,14 +67,14 @@ func GetBadge(c *gin.Context) { func GetCC(c *gin.Context) { store := ToDatastore(c) repo := ToRepo(c) - last, err := store.BuildLast(repo.FullName) - if err != nil { - c.Fail(404, err) + list, err := store.CommitList(repo, 1, 0) + if err != nil || len(list) == 0 { + c.AbortWithStatus(404) return } link := httputil.GetURL(c.Request) + "/" + repo.FullName - cc := ccmenu.NewCC(repo, last, link) + cc := ccmenu.NewCC(repo, list[0], link) c.Writer.Header().Set("Content-Type", "application/xml") c.XML(200, cc) diff --git a/server/builds.go b/server/builds.go deleted file mode 100644 index 0e076319d..000000000 --- a/server/builds.go +++ /dev/null @@ -1,263 +0,0 @@ -package server - -import ( - "io" - "strconv" - "time" - - "github.com/drone/drone/common" - "github.com/drone/drone/parser/inject" - "github.com/drone/drone/queue" - "github.com/gin-gonic/gin" - "github.com/gin-gonic/gin/binding" -) - -// GetBuild accepts a request to retrieve a build -// from the datastore for the given repository and -// build number. -// -// GET /api/builds/:owner/:name/:number -// -func GetBuild(c *gin.Context) { - store := ToDatastore(c) - repo := ToRepo(c) - num, err := strconv.Atoi(c.Params.ByName("number")) - if err != nil { - c.Fail(400, err) - return - } - build, err := store.Build(repo.FullName, num) - if err != nil { - c.Fail(404, err) - } else { - c.JSON(200, build) - } -} - -// GetBuilds accepts a request to retrieve a list -// of builds from the datastore for the given repository. -// -// GET /api/builds/:owner/:name -// -func GetBuilds(c *gin.Context) { - store := ToDatastore(c) - repo := ToRepo(c) - builds, err := store.BuildList(repo.FullName) - if err != nil { - c.Fail(404, err) - } else { - c.JSON(200, builds) - } -} - -// GetBuildLogs accepts a request to retrieve logs from the -// datastore for the given repository, build and task -// number. -// -// GET /api/repos/:owner/:name/logs/:number/:task -// -func GetBuildLogs(c *gin.Context) { - store := ToDatastore(c) - repo := ToRepo(c) - full, _ := strconv.ParseBool(c.Params.ByName("full")) - build, _ := strconv.Atoi(c.Params.ByName("number")) - task, _ := strconv.Atoi(c.Params.ByName("task")) - - r, err := store.LogReader(repo.FullName, build, task) - if err != nil { - c.Fail(404, err) - } else if full { - io.Copy(c.Writer, r) - } else { - io.Copy(c.Writer, io.LimitReader(r, 2000000)) - } -} - -// PostBuildStatus accepts a request to create a new build -// status. The created user status is returned in JSON -// format if successful. -// -// POST /api/repos/:owner/:name/status/:number -// -func PostBuildStatus(c *gin.Context) { - store := ToDatastore(c) - repo := ToRepo(c) - num, err := strconv.Atoi(c.Params.ByName("number")) - if err != nil { - c.Fail(400, err) - return - } - in := &common.Status{} - if !c.BindWith(in, binding.JSON) { - c.AbortWithStatus(400) - return - } - if err := store.SetBuildStatus(repo.Name, num, in); err != nil { - c.Fail(400, err) - } else { - c.JSON(201, in) - } -} - -// RunBuild accepts a request to restart an existing build. -// -// POST /api/builds/:owner/:name/builds/:number -// -func RunBuild(c *gin.Context) { - remote := ToRemote(c) - store := ToDatastore(c) - queue_ := ToQueue(c) - repo := ToRepo(c) - num, err := strconv.Atoi(c.Params.ByName("number")) - if err != nil { - c.Fail(400, err) - return - } - build, err := store.Build(repo.FullName, num) - if err != nil { - c.Fail(404, err) - return - } - - keys, err := store.RepoKeypair(repo.FullName) - if err != nil { - c.Fail(404, err) - return - } - - user, err := store.User(repo.User.Login) - if err != nil { - c.Fail(404, err) - return - } - - // must not restart a running build - if build.State == common.StatePending || build.State == common.StateRunning { - c.AbortWithStatus(409) - return - } - - build.State = common.StatePending - build.Started = 0 - build.Finished = 0 - build.Duration = 0 - build.Statuses = []*common.Status{} - for _, task := range build.Tasks { - task.State = common.StatePending - task.Started = 0 - task.Finished = 0 - task.ExitCode = 0 - } - - err = store.SetBuild(repo.FullName, build) - if err != nil { - c.Fail(500, err) - return - } - - netrc, err := remote.Netrc(user) - if err != nil { - c.Fail(500, err) - return - } - - // featch the .drone.yml file from the database - raw, err := remote.Script(user, repo, build) - if err != nil { - c.Fail(404, err) - return - } - - // inject any private parameters into the .drone.yml - params, _ := store.RepoParams(repo.FullName) - if params != nil && len(params) != 0 { - raw = []byte(inject.InjectSafe(string(raw), params)) - } - - c.JSON(202, build) - - queue_.Publish(&queue.Work{ - User: user, - Repo: repo, - Build: build, - Keys: keys, - Netrc: netrc, - Yaml: raw, - }) -} - -// KillBuild accepts a request to kill a running build. -// -// DELETE /api/builds/:owner/:name/builds/:number -// -func KillBuild(c *gin.Context) { - runner := ToRunner(c) - queue := ToQueue(c) - store := ToDatastore(c) - repo := ToRepo(c) - num, err := strconv.Atoi(c.Params.ByName("number")) - if err != nil { - c.Fail(400, err) - return - } - build, err := store.Build(repo.FullName, num) - if err != nil { - c.Fail(404, err) - return - } - - // must not restart a running build - if build.State != common.StatePending && build.State != common.StateRunning { - c.Fail(409, err) - return - } - - // remove from the queue if exists - for _, item := range queue.Items() { - if item.Repo.FullName == repo.FullName && item.Build.Number == build.Number { - queue.Remove(item) - break - } - } - - build.State = common.StateKilled - build.Finished = time.Now().Unix() - if build.Started == 0 { - build.Started = build.Finished - } - build.Duration = build.Finished - build.Started - for _, task := range build.Tasks { - if task.State != common.StatePending && task.State != common.StateRunning { - continue - } - task.State = common.StateKilled - task.Started = build.Started - task.Finished = build.Finished - } - err = store.SetBuild(repo.FullName, build) - if err != nil { - c.Fail(500, err) - return - } - - for _, task := range build.Tasks { - runner.Cancel(repo.FullName, build.Number, task.Number) - } - // // get the agent from the repository so we can - // // notify the agent to kill the build. - // agent, err := store.BuildAgent(repo.FullName, build.Number) - // if err != nil { - // c.JSON(200, build) - // return - // } - // url_, _ := url.Parse("http://" + agent.Addr) - // url_.Path = fmt.Sprintf("/cancel/%s/%v", repo.FullName, build.Number) - // resp, err := http.Post(url_.String(), "application/json", nil) - // if err != nil { - // c.Fail(500, err) - // return - // } - // defer resp.Body.Close() - - c.JSON(200, build) -} diff --git a/server/commits.go b/server/commits.go new file mode 100644 index 000000000..5e8524825 --- /dev/null +++ b/server/commits.go @@ -0,0 +1,279 @@ +package server + +import ( + "fmt" + "io" + "strconv" + "time" + + "github.com/drone/drone/common" + "github.com/drone/drone/parser/inject" + "github.com/drone/drone/queue" + "github.com/gin-gonic/gin" + // "github.com/gin-gonic/gin/binding" +) + +// GetCommit accepts a request to retrieve a commit +// from the datastore for the given repository and +// commit sequence. +// +// GET /api/repos/:owner/:name/:number +// +func GetCommit(c *gin.Context) { + store := ToDatastore(c) + repo := ToRepo(c) + num, err := strconv.Atoi(c.Params.ByName("number")) + if err != nil { + c.Fail(400, err) + return + } + commit, err := store.CommitSeq(repo, num) + if err != nil { + c.Fail(404, err) + } + commit.Builds, err = store.BuildList(commit) + if err != nil { + c.Fail(404, err) + } else { + c.JSON(200, commit) + } +} + +// GetCommits accepts a request to retrieve a list +// of commits from the datastore for the given repository. +// +// GET /api/repos/:owner/:name/builds +// +func GetCommits(c *gin.Context) { + store := ToDatastore(c) + repo := ToRepo(c) + commits, err := store.CommitList(repo, 20, 0) + if err != nil { + c.Fail(404, err) + } else { + c.JSON(200, commits) + } +} + +// GetLogs accepts a request to retrieve logs from the +// datastore for the given repository, build and task +// number. +// +// GET /api/repos/:owner/:name/logs/:number/:task +// +func GetLogs(c *gin.Context) { + store := ToDatastore(c) + repo := ToRepo(c) + full, _ := strconv.ParseBool(c.Params.ByName("full")) + commit, _ := strconv.Atoi(c.Params.ByName("number")) + build, _ := strconv.Atoi(c.Params.ByName("task")) + + path := fmt.Sprintf("/logs/%s/%v/%v", repo.FullName, commit, build) + r, err := store.GetBlobReader(path) + if err != nil { + c.Fail(404, err) + } else if full { + io.Copy(c.Writer, r) + } else { + io.Copy(c.Writer, io.LimitReader(r, 2000000)) + } +} + +// // PostBuildStatus accepts a request to create a new build +// // status. The created user status is returned in JSON +// // format if successful. +// // +// // POST /api/repos/:owner/:name/status/:number +// // +// func PostBuildStatus(c *gin.Context) { +// store := ToDatastore(c) +// repo := ToRepo(c) +// num, err := strconv.Atoi(c.Params.ByName("number")) +// if err != nil { +// c.Fail(400, err) +// return +// } +// in := &common.Status{} +// if !c.BindWith(in, binding.JSON) { +// c.AbortWithStatus(400) +// return +// } +// if err := store.SetBuildStatus(repo.Name, num, in); err != nil { +// c.Fail(400, err) +// } else { +// c.JSON(201, in) +// } +// } + +// RunBuild accepts a request to restart an existing build. +// +// POST /api/builds/:owner/:name/builds/:number +// +func RunBuild(c *gin.Context) { + remote := ToRemote(c) + store := ToDatastore(c) + queue_ := ToQueue(c) + repo := ToRepo(c) + num, err := strconv.Atoi(c.Params.ByName("number")) + if err != nil { + c.Fail(400, err) + return + } + commit, err := store.CommitSeq(repo, num) + if err != nil { + c.Fail(404, err) + return + } + commit.Builds, err = store.BuildList(commit) + if err != nil { + c.Fail(404, err) + return + } + + keys := &common.Keypair{ + Public: repo.PublicKey, + Private: repo.PrivateKey, + } + + user, err := store.User(repo.UserID) + if err != nil { + c.Fail(404, err) + return + } + + // must not restart a running build + if commit.State == common.StatePending || commit.State == common.StateRunning { + c.AbortWithStatus(409) + return + } + + commit.State = common.StatePending + commit.Started = 0 + commit.Finished = 0 + for _, build := range commit.Builds { + build.State = common.StatePending + build.Started = 0 + build.Finished = 0 + build.Duration = 0 + build.ExitCode = 0 + } + + err = store.SetCommit(commit) + if err != nil { + c.Fail(500, err) + return + } + + netrc, err := remote.Netrc(user) + if err != nil { + c.Fail(500, err) + return + } + + // featch the .drone.yml file from the database + raw, err := remote.Script(user, repo, commit) + if err != nil { + c.Fail(404, err) + return + } + + // inject any private parameters into the .drone.yml + if repo.Params != nil && len(repo.Params) != 0 { + raw = []byte(inject.InjectSafe(string(raw), repo.Params)) + } + + c.JSON(202, commit) + + queue_.Publish(&queue.Work{ + User: user, + Repo: repo, + Commit: commit, + Keys: keys, + Netrc: netrc, + Yaml: raw, + }) +} + +// KillBuild accepts a request to kill a running build. +// +// DELETE /api/builds/:owner/:name/builds/:number +// +func KillBuild(c *gin.Context) { + runner := ToRunner(c) + queue := ToQueue(c) + store := ToDatastore(c) + repo := ToRepo(c) + num, err := strconv.Atoi(c.Params.ByName("number")) + if err != nil { + c.Fail(400, err) + return + } + commit, err := store.CommitSeq(repo, num) + if err != nil { + c.Fail(404, err) + return + } + commit.Builds, err = store.BuildList(commit) + if err != nil { + c.Fail(404, err) + return + } + + // must not restart a running build + if commit.State != common.StatePending && commit.State != common.StateRunning { + c.Fail(409, err) + return + } + + // remove from the queue if exists + // + // TODO(bradrydzewski) this could yield a race condition + // because other threads may also be accessing these items. + for _, item := range queue.Items() { + if item.Repo.FullName == repo.FullName && item.Commit.Sequence == commit.Sequence { + queue.Remove(item) + break + } + } + + commit.State = common.StateKilled + commit.Finished = time.Now().Unix() + if commit.Started == 0 { + commit.Started = commit.Finished + } + for _, build := range commit.Builds { + if build.State != common.StatePending && build.State != common.StateRunning { + continue + } + build.State = common.StateKilled + build.Started = commit.Started + build.Finished = commit.Finished + build.Duration = commit.Finished - commit.Started + } + err = store.SetCommit(commit) + if err != nil { + c.Fail(500, err) + return + } + + for _, build := range commit.Builds { + runner.Cancel(build) + } + // // get the agent from the repository so we can + // // notify the agent to kill the build. + // agent, err := store.BuildAgent(repo.FullName, build.Number) + // if err != nil { + // c.JSON(200, build) + // return + // } + // url_, _ := url.Parse("http://" + agent.Addr) + // url_.Path = fmt.Sprintf("/cancel/%s/%v", repo.FullName, build.Number) + // resp, err := http.Post(url_.String(), "application/json", nil) + // if err != nil { + // c.Fail(500, err) + // return + // } + // defer resp.Body.Close() + + c.JSON(200, commit) +} diff --git a/server/hooks.go b/server/hooks.go index 49e52aed1..347cfe998 100644 --- a/server/hooks.go +++ b/server/hooks.go @@ -57,52 +57,49 @@ func PostHook(c *gin.Context) { return } - repo, err := store.Repo(hook.Repo.FullName) + repo, err := store.RepoName(hook.Repo.Owner, hook.Repo.Name) if err != nil { - log.Errorf("failure to find repo %s from hook. %s", hook.Repo.FullName, err) + log.Errorf("failure to find repo %s/%s from hook. %s", hook.Repo.Owner, hook.Repo.Name, err) c.Fail(404, err) return } switch { - case repo.Disabled: - log.Infof("ignoring hook. repo %s is disabled.", repo.FullName) - c.Writer.WriteHeader(204) - return - case repo.User == nil: + case repo.UserID == 0: log.Warnf("ignoring hook. repo %s has no owner.", repo.FullName) c.Writer.WriteHeader(204) return - case repo.DisablePR && hook.PullRequest != nil: + case !repo.PostCommit && hook.Commit.PullRequest != "": + log.Infof("ignoring hook. repo %s is disabled.", repo.FullName) + c.Writer.WriteHeader(204) + return + case !repo.PullRequest && hook.Commit.PullRequest == "": log.Warnf("ignoring hook. repo %s is disabled for pull requests.", repo.FullName) c.Writer.WriteHeader(204) return } - user, err := store.User(repo.User.Login) + user, err := store.User(repo.UserID) if err != nil { - log.Errorf("failure to find repo owner %s. %s", repo.User.Login, err) + log.Errorf("failure to find repo owner %s. %s", repo.FullName, err) c.Fail(500, err) return } - params, _ := store.RepoParams(repo.FullName) - - build := &common.Build{} - build.State = common.StatePending - build.Commit = hook.Commit - build.PullRequest = hook.PullRequest + commit := hook.Commit + commit.State = common.StatePending + commit.RepoID = repo.ID // featch the .drone.yml file from the database - raw, err := remote.Script(user, repo, build) + raw, err := remote.Script(user, repo, commit) if err != nil { log.Errorf("failure to get .drone.yml for %s. %s", repo.FullName, err) c.Fail(404, err) return } // inject any private parameters into the .drone.yml - if params != nil && len(params) != 0 { - raw = []byte(inject.InjectSafe(string(raw), params)) + if repo.Params != nil && len(repo.Params) != 0 { + raw = []byte(inject.InjectSafe(string(raw), repo.Params)) } axes, err := matrix.Parse(string(raw)) if err != nil { @@ -114,58 +111,59 @@ func PostHook(c *gin.Context) { axes = append(axes, matrix.Axis{}) } for num, axis := range axes { - build.Tasks = append(build.Tasks, &common.Task{ - Number: num + 1, + commit.Builds = append(commit.Builds, &common.Build{ + CommitID: commit.ID, + Sequence: num + 1, State: common.StatePending, Environment: axis, }) } - keys, err := store.RepoKeypair(repo.FullName) - if err != nil { - log.Errorf("failure to fetch keypair for %s. %s", repo.FullName, err) - c.Fail(404, err) - return + keys := &common.Keypair{ + Public: repo.PublicKey, + Private: repo.PrivateKey, } netrc, err := remote.Netrc(user) if err != nil { + log.Errorf("failure to generate netrc for %s. %s", repo.FullName, err) c.Fail(500, err) return } // verify the branches can be built vs skipped when, _ := parser.ParseCondition(string(raw)) - if build.Commit != nil && when != nil && !when.MatchBranch(build.Commit.Ref) { - log.Infof("ignoring hook. yaml file excludes repo and branch %s %s", repo.FullName, build.Commit.Ref) + if commit.PullRequest != "" && when != nil && !when.MatchBranch(commit.Branch) { + log.Infof("ignoring hook. yaml file excludes repo and branch %s %s", repo.FullName, commit.Branch) c.AbortWithStatus(200) return } - err = store.SetBuild(repo.FullName, build) + err = store.AddCommit(commit) if err != nil { + log.Errorf("failure to save commit for %s. %s", repo.FullName, err) c.Fail(500, err) return } - c.JSON(200, build) + c.JSON(200, commit) link := fmt.Sprintf( "%s/%s/%d", httputil.GetURL(c.Request), repo.FullName, - build.Number, + commit.Sequence, ) - err = remote.Status(user, repo, build, link) + err = remote.Status(user, repo, commit, link) if err != nil { - log.Errorf("error setting commit status for %s/%d", repo.FullName, build.Number) + log.Errorf("error setting commit status for %s/%d", repo.FullName, commit.Sequence) } queue_.Publish(&queue.Work{ - User: user, - Repo: repo, - Build: build, - Keys: keys, - Netrc: netrc, - Yaml: raw, + User: user, + Repo: repo, + Commit: commit, + Keys: keys, + Netrc: netrc, + Yaml: raw, }) } diff --git a/server/login.go b/server/login.go index f34c3702d..208265238 100644 --- a/server/login.go +++ b/server/login.go @@ -61,7 +61,7 @@ func GetLogin(c *gin.Context) { } // get the user from the database - u, err := store.User(login.Login) + u, err := store.UserLogin(login.Login) if err != nil { count, err := store.UserCount() if err != nil { @@ -89,7 +89,7 @@ func GetLogin(c *gin.Context) { u.Gravatar = gravatar.Hash(u.Email) // insert the user into the database - if err := store.SetUserNotExists(u); err != nil { + if err := store.AddUser(u); err != nil { log.Errorf("cannot insert %s. %s", login.Login, err) c.Redirect(303, "/login#error=internal_error") return diff --git a/server/queue.go b/server/queue.go index fb0f4aab5..c9a4f9914 100644 --- a/server/queue.go +++ b/server/queue.go @@ -1,182 +1,182 @@ package server -import ( - "encoding/json" - "io" - "net" - "strconv" +// import ( +// "encoding/json" +// "io" +// "net" +// "strconv" - log "github.com/Sirupsen/logrus" - "github.com/gin-gonic/gin" - "github.com/gin-gonic/gin/binding" +// log "github.com/Sirupsen/logrus" +// "github.com/gin-gonic/gin" +// "github.com/gin-gonic/gin/binding" - "github.com/drone/drone/common" - "github.com/drone/drone/eventbus" -) +// "github.com/drone/drone/common" +// "github.com/drone/drone/eventbus" +// ) -// TODO (bradrydzewski) the callback URL should be signed. -// TODO (bradrydzewski) we shouldn't need to fetch the Repo if specified in the URL path -// TODO (bradrydzewski) use SetRepoLast to update the last repository +// // TODO (bradrydzewski) the callback URL should be signed. +// // TODO (bradrydzewski) we shouldn't need to fetch the Repo if specified in the URL path +// // TODO (bradrydzewski) use SetRepoLast to update the last repository -// GET /queue/pull -func PollBuild(c *gin.Context) { - queue := ToQueue(c) - store := ToDatastore(c) - agent := &common.Agent{ - Addr: c.Request.RemoteAddr, - } +// // GET /queue/pull +// func PollBuild(c *gin.Context) { +// queue := ToQueue(c) +// store := ToDatastore(c) +// agent := &common.Agent{ +// Addr: c.Request.RemoteAddr, +// } - // extact the host port and name and - // replace with the default agent port (1999) - host, _, err := net.SplitHostPort(agent.Addr) - if err == nil { - agent.Addr = host - } - agent.Addr = net.JoinHostPort(agent.Addr, "1999") +// // extact the host port and name and +// // replace with the default agent port (1999) +// host, _, err := net.SplitHostPort(agent.Addr) +// if err == nil { +// agent.Addr = host +// } +// agent.Addr = net.JoinHostPort(agent.Addr, "1999") - log.Infof("agent connected and polling builds at %s", agent.Addr) +// log.Infof("agent connected and polling builds at %s", agent.Addr) - work := queue.PullClose(c.Writer) - if work == nil { - c.AbortWithStatus(500) - return - } +// work := queue.PullClose(c.Writer) +// if work == nil { +// c.AbortWithStatus(500) +// return +// } - // TODO (bradrydzewski) decide how we want to handle a failure here - // still not sure exact behavior we want ... - err = store.SetBuildAgent(work.Repo.FullName, work.Build.Number, agent) - if err != nil { - log.Errorf("error persisting build agent. %s", err) - } +// // TODO (bradrydzewski) decide how we want to handle a failure here +// // still not sure exact behavior we want ... +// err = store.SetBuildAgent(work.Repo.FullName, work.Build.Number, agent) +// if err != nil { +// log.Errorf("error persisting build agent. %s", err) +// } - c.JSON(200, work) +// c.JSON(200, work) - // acknowledge work received by the client - queue.Ack(work) -} +// // acknowledge work received by the client +// queue.Ack(work) +// } -// GET /queue/push/:owner/:repo -func PushBuild(c *gin.Context) { - store := ToDatastore(c) - repo := ToRepo(c) - bus := ToBus(c) - in := &common.Build{} - if !c.BindWith(in, binding.JSON) { - return - } - build, err := store.Build(repo.FullName, in.Number) - if err != nil { - c.Fail(404, err) - return - } +// // GET /queue/push/:owner/:repo +// func PushBuild(c *gin.Context) { +// store := ToDatastore(c) +// repo := ToRepo(c) +// bus := ToBus(c) +// in := &common.Build{} +// if !c.BindWith(in, binding.JSON) { +// return +// } +// build, err := store.Build(repo.FullName, in.Number) +// if err != nil { +// c.Fail(404, err) +// return +// } - if in.State != common.StatePending && in.State != common.StateRunning { - store.DelBuildAgent(repo.FullName, build.Number) - } +// if in.State != common.StatePending && in.State != common.StateRunning { +// store.DelBuildAgent(repo.FullName, build.Number) +// } - build.Duration = in.Duration - build.Started = in.Started - build.Finished = in.Finished - build.State = in.State - err = store.SetBuildState(repo.FullName, build) - if err != nil { - c.Fail(500, err) - return - } +// build.Duration = in.Duration +// build.Started = in.Started +// build.Finished = in.Finished +// build.State = in.State +// err = store.SetBuildState(repo.FullName, build) +// if err != nil { +// c.Fail(500, err) +// return +// } - if build.State != common.StatePending && build.State != common.StateRunning { - if repo.Last == nil || build.Number >= repo.Last.Number { - repo.Last = build - store.SetRepo(repo) - } - } +// if build.State != common.StatePending && build.State != common.StateRunning { +// if repo.Last == nil || build.Number >= repo.Last.Number { +// repo.Last = build +// store.SetRepo(repo) +// } +// } - // <-- FIXME - // for some reason the Repo and Build fail to marshal to JSON. - // It has something to do with memory / pointers. So it goes away - // if I just refetch these items. Needs to be fixed in the future, - // but for now should be ok - repo, err = store.Repo(repo.FullName) - if err != nil { - c.Fail(500, err) - return - } - build, err = store.Build(repo.FullName, in.Number) - if err != nil { - c.Fail(404, err) - return - } - // END FIXME --> +// // <-- FIXME +// // for some reason the Repo and Build fail to marshal to JSON. +// // It has something to do with memory / pointers. So it goes away +// // if I just refetch these items. Needs to be fixed in the future, +// // but for now should be ok +// repo, err = store.Repo(repo.FullName) +// if err != nil { +// c.Fail(500, err) +// return +// } +// build, err = store.Build(repo.FullName, in.Number) +// if err != nil { +// c.Fail(404, err) +// return +// } +// // END FIXME --> - msg, err := json.Marshal(build) - if err == nil { - c.String(200, err.Error()) // we can ignore this error - return - } +// msg, err := json.Marshal(build) +// if err == nil { +// c.String(200, err.Error()) // we can ignore this error +// return +// } - bus.Send(&eventbus.Event{ - Name: repo.FullName, - Kind: eventbus.EventRepo, - Msg: msg, - }) +// bus.Send(&eventbus.Event{ +// Name: repo.FullName, +// Kind: eventbus.EventRepo, +// Msg: msg, +// }) - c.Writer.WriteHeader(200) -} +// c.Writer.WriteHeader(200) +// } -// POST /queue/push/:owner/:repo/:build -func PushTask(c *gin.Context) { - store := ToDatastore(c) - repo := ToRepo(c) - bus := ToBus(c) - num, _ := strconv.Atoi(c.Params.ByName("build")) - in := &common.Task{} - if !c.BindWith(in, binding.JSON) { - return - } - err := store.SetBuildTask(repo.FullName, num, in) - if err != nil { - c.Fail(404, err) - return - } - build, err := store.Build(repo.FullName, num) - if err != nil { - c.Fail(404, err) - return - } +// // POST /queue/push/:owner/:repo/:build +// func PushTask(c *gin.Context) { +// store := ToDatastore(c) +// repo := ToRepo(c) +// bus := ToBus(c) +// num, _ := strconv.Atoi(c.Params.ByName("build")) +// in := &common.Task{} +// if !c.BindWith(in, binding.JSON) { +// return +// } +// err := store.SetBuildTask(repo.FullName, num, in) +// if err != nil { +// c.Fail(404, err) +// return +// } +// build, err := store.Build(repo.FullName, num) +// if err != nil { +// c.Fail(404, err) +// return +// } - msg, err := json.Marshal(build) - if err == nil { - c.String(200, err.Error()) // we can ignore this error - return - } +// msg, err := json.Marshal(build) +// if err == nil { +// c.String(200, err.Error()) // we can ignore this error +// return +// } - bus.Send(&eventbus.Event{ - Name: repo.FullName, - Kind: eventbus.EventRepo, - Msg: msg, - }) +// bus.Send(&eventbus.Event{ +// Name: repo.FullName, +// Kind: eventbus.EventRepo, +// Msg: msg, +// }) - c.Writer.WriteHeader(200) -} +// c.Writer.WriteHeader(200) +// } -// POST /queue/push/:owner/:repo/:build/:task/logs -func PushLogs(c *gin.Context) { - store := ToDatastore(c) - repo := ToRepo(c) - bnum, _ := strconv.Atoi(c.Params.ByName("build")) - tnum, _ := strconv.Atoi(c.Params.ByName("task")) +// // POST /queue/push/:owner/:repo/:build/:task/logs +// func PushLogs(c *gin.Context) { +// store := ToDatastore(c) +// repo := ToRepo(c) +// bnum, _ := strconv.Atoi(c.Params.ByName("build")) +// tnum, _ := strconv.Atoi(c.Params.ByName("task")) - const maxBuffToRead int64 = 5000000 // 5MB. - err := store.SetLogs(repo.FullName, bnum, tnum, io.LimitReader(c.Request.Body, maxBuffToRead)) - if err != nil { - c.Fail(500, err) - return - } - c.Writer.WriteHeader(200) -} +// const maxBuffToRead int64 = 5000000 // 5MB. +// err := store.SetLogs(repo.FullName, bnum, tnum, io.LimitReader(c.Request.Body, maxBuffToRead)) +// if err != nil { +// c.Fail(500, err) +// return +// } +// c.Writer.WriteHeader(200) +// } -func GetQueue(c *gin.Context) { - queue := ToQueue(c) - items := queue.Items() - c.JSON(200, items) -} +// func GetQueue(c *gin.Context) { +// queue := ToQueue(c) +// items := queue.Items() +// c.JSON(200, items) +// } diff --git a/server/repos.go b/server/repos.go index cf6ea3bf4..2589caa07 100644 --- a/server/repos.go +++ b/server/repos.go @@ -17,10 +17,10 @@ import ( // additional repository meta-data. type repoResp struct { *common.Repo - Perms *common.Perm `json:"permissions,omitempty"` - Watch *common.Subscriber `json:"subscription,omitempty"` - Keypair *common.Keypair `json:"keypair,omitempty"` - Params map[string]string `json:"params,omitempty"` + Perms *common.Perm `json:"permissions,omitempty"` + Keypair *common.Keypair `json:"keypair,omitempty"` + Params map[string]string `json:"params,omitempty"` + Starred bool `json:"starred,omitempty"` } // repoReq is a data structure used for receiving @@ -31,11 +31,10 @@ type repoResp struct { // accept null values, effectively patching an existing // repository object with only the supplied fields. type repoReq struct { - Disabled *bool `json:"disabled"` - DisablePR *bool `json:"disable_prs"` - DisableTag *bool `json:"disable_tags"` - Trusted *bool `json:"privileged"` - Timeout *int64 `json:"timeout"` + PostCommit *bool `json:"post_commits"` + PullRequest *bool `json:"pull_requests"` + Trusted *bool `json:"privileged"` + Timeout *int64 `json:"timeout"` // optional private parameters can only be // supplied by the repository admin. @@ -53,19 +52,8 @@ func GetRepo(c *gin.Context) { repo := ToRepo(c) user := ToUser(c) perm := ToPerm(c) - data := repoResp{repo, perm, nil, nil, nil} - // if the user is an administrator of the project - // we should display the private parameter data - // and keypair data. - if perm.Push { - data.Params, _ = store.RepoParams(repo.FullName) + data := repoResp{repo, perm, nil, nil, false} - // note that we should only display the public key - keypair, err := store.RepoKeypair(repo.FullName) - if err == nil { - data.Keypair = &common.Keypair{Public: keypair.Public} - } - } // if the user is authenticated, we should display // if she is watching the current repository. if user == nil { @@ -73,9 +61,17 @@ func GetRepo(c *gin.Context) { return } + // if the user is an administrator of the project + // we should display the private parameter data + // and keypair data. + if perm.Push { + data.Params = repo.Params + data.Keypair = &common.Keypair{ + Public: repo.PublicKey, + } + } // check to see if the user is subscribing to the repo - data.Watch = &common.Subscriber{} - data.Watch.Subscribed, _ = store.Subscribed(user.Login, repo.FullName) + data.Starred, _ = store.Starred(user, repo) c.JSON(200, data) } @@ -98,20 +94,14 @@ func PutRepo(c *gin.Context) { } if in.Params != nil { - err := store.SetRepoParams(repo.FullName, *in.Params) - if err != nil { - c.Fail(400, err) - return - } + repo.Params = *in.Params } - if in.Disabled != nil { - repo.Disabled = *in.Disabled + + if in.PostCommit != nil { + repo.PullRequest = *in.PullRequest } - if in.DisablePR != nil { - repo.DisablePR = *in.DisablePR - } - if in.DisableTag != nil { - repo.DisableTag = *in.DisableTag + if in.PullRequest != nil { + repo.PullRequest = *in.PullRequest } if in.Trusted != nil && user.Admin { repo.Trusted = *in.Trusted @@ -126,20 +116,12 @@ func PutRepo(c *gin.Context) { return } - data := repoResp{repo, perm, nil, nil, nil} - data.Params, _ = store.RepoParams(repo.FullName) - data.Keypair, _ = store.RepoKeypair(repo.FullName) - - // check to see if the user is subscribing to the repo - data.Watch = &common.Subscriber{} - data.Watch.Subscribed, _ = store.Subscribed(user.Login, repo.FullName) - - // scrub the private key from the keypair - if data.Keypair != nil { - data.Keypair = &common.Keypair{ - Public: data.Keypair.Public, - } + data := repoResp{repo, perm, nil, nil, false} + data.Params = repo.Params + data.Keypair = &common.Keypair{ + Public: repo.PublicKey, } + data.Starred, _ = store.Starred(user, repo) c.JSON(200, data) } @@ -184,12 +166,6 @@ func PostRepo(c *gin.Context) { owner := c.Params.ByName("owner") name := c.Params.ByName("name") - _, err := store.Repo(owner + "/" + name) - if err == nil { - c.String(409, "Repository already exists") - return - } - // get the repository and user permissions // from the remote system. remote := ToRemote(c) @@ -207,6 +183,13 @@ func PostRepo(c *gin.Context) { return } + // error if the repository already exists + _, err = store.RepoName(owner, name) + if err == nil { + c.String(409, "Repository already exists") + return + } + token := &common.Token{} token.Kind = common.TokenHook token.Label = r.FullName @@ -224,7 +207,9 @@ func PostRepo(c *gin.Context) { // set the repository owner to the // currently authenticated user. - r.User = &common.Owner{Login: user.Login} + r.UserID = user.ID + r.PostCommit = true + r.PullRequest = true // generate an RSA key and add to the repo key, err := sshutil.GeneratePrivateKey() @@ -232,9 +217,12 @@ func PostRepo(c *gin.Context) { c.Fail(400, err) return } - keypair := &common.Keypair{} - keypair.Public = sshutil.MarshalPublicKey(&key.PublicKey) - keypair.Private = sshutil.MarshalPrivateKey(key) + r.PublicKey = sshutil.MarshalPublicKey(&key.PublicKey) + r.PrivateKey = sshutil.MarshalPrivateKey(key) + keypair := &common.Keypair{ + Public: r.PublicKey, + Private: r.PrivateKey, + } // activate the repository before we make any // local changes to the database. @@ -245,18 +233,13 @@ func PostRepo(c *gin.Context) { } // persist the repository - err = store.SetRepoNotExists(user, r) + err = store.AddRepo(r) if err != nil { c.Fail(500, err) return } - // persisty the repository key pair - err = store.SetRepoKeypair(r.FullName, keypair) - if err != nil { - c.Fail(500, err) - return - } + store.AddStar(user, r) c.JSON(200, r) } @@ -271,7 +254,7 @@ func Unsubscribe(c *gin.Context) { repo := ToRepo(c) user := ToUser(c) - err := store.DelSubscriber(user.Login, repo.FullName) + err := store.DelStar(user, repo) if err != nil { c.Fail(400, err) } else { @@ -289,11 +272,11 @@ func Subscribe(c *gin.Context) { repo := ToRepo(c) user := ToUser(c) - err := store.SetSubscriber(user.Login, repo.FullName) + err := store.AddStar(user, repo) if err != nil { c.Fail(400, err) } else { - c.JSON(200, &common.Subscriber{Subscribed: true}) + c.Writer.WriteHeader(200) } } diff --git a/server/server.go b/server/server.go index 4437f612a..c3a1a92b3 100644 --- a/server/server.go +++ b/server/server.go @@ -146,9 +146,9 @@ func SetUser(s session.Session) gin.HandlerFunc { return } - u, err := ds.User(token.Login) + user, err := ds.UserLogin(token.Login) if err == nil { - c.Set("user", u) + c.Set("user", user) } // if session token we can proceed, otherwise @@ -162,7 +162,7 @@ func SetUser(s session.Session) gin.HandlerFunc { // to verify the token we fetch from the datastore // and check to see if the token issued date matches // what we found in the jwt (in case the label is re-used) - t, err := ds.Token(token.Login, token.Label) + t, err := ds.TokenLabel(user, token.Label) if err != nil || t.Issued != token.Issued { c.AbortWithStatus(403) return @@ -178,7 +178,7 @@ func SetRepo() gin.HandlerFunc { u := ToUser(c) owner := c.Params.ByName("owner") name := c.Params.ByName("name") - r, err := ds.Repo(owner + "/" + name) + r, err := ds.RepoName(owner, name) switch { case err != nil && u != nil: c.Fail(404, err) diff --git a/server/static/images/logo.svg b/server/static/images/logo.svg new file mode 100644 index 000000000..af16f2873 --- /dev/null +++ b/server/static/images/logo.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/server/static/index.html b/server/static/index.html index 8780c53e1..9f1696029 100644 --- a/server/static/index.html +++ b/server/static/index.html @@ -1,43 +1,43 @@ - + - - - - - - - - - - - -
+ + + + + + + + + + + +
- - - - - + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - + + + - + diff --git a/server/static/scripts/controllers/builds.js b/server/static/scripts/controllers/builds.js index e99bc171b..617823e5a 100644 --- a/server/static/scripts/controllers/builds.js +++ b/server/static/scripts/controllers/builds.js @@ -31,13 +31,13 @@ $scope.watch = function(repo) { repos.watch(repo.full_name).then(function(payload) { - $scope.repo.subscription = payload.data; + $scope.repo.starred = true; }); } $scope.unwatch = function(repo) { repos.unwatch(repo.full_name).then(function() { - delete $scope.repo.subscription; + $scope.repo.starred = false; }); } @@ -45,7 +45,7 @@ var added = false; for (var i=0;i<$scope.builds.length;i++) { var build = $scope.builds[i]; - if (event.number !== build.number) { + if (event.sequence !== build.sequence) { continue; // ignore } // update the build status @@ -66,7 +66,7 @@ */ function BuildCtrl($scope, $routeParams, $window, logs, builds, repos, users) { - var step = parseInt($routeParams.step) || 1; + var step = parseInt($routeParams.step); var number = $routeParams.number; var owner = $routeParams.owner; var name = $routeParams.name; @@ -110,7 +110,18 @@ // Gets the build builds.get(fullName, number).then(function(payload){ $scope.build = payload.data; - $scope.task = payload.data.tasks[step-1]; + + // if only 1 build, select first + if (!step && payload.data.builds.length === 1) { + step = 1; + + // else if only 1 step, but multiple builds + // we should only render the list of builds + } else if (!step) { + return; + } + + $scope.task = payload.data.builds[step-1]; if (['pending', 'killed'].indexOf($scope.task.state) !== -1) { // do nothing @@ -135,7 +146,7 @@ $scope.restart = function() { builds.restart(fullName, number).then(function(payload){ $scope.build = payload.data; - $scope.task = payload.data.tasks[step-1]; + $scope.task = payload.data.builds[step-1]; }).catch(function(err){ $scope.error = err; }); @@ -144,7 +155,7 @@ $scope.cancel = function() { builds.cancel(fullName, number).then(function(payload){ $scope.build = payload.data; - $scope.task = payload.data.tasks[step-1]; + $scope.task = payload.data.builds[step-1]; }).catch(function(err) { $scope.error = err; }); @@ -155,12 +166,12 @@ }; repos.subscribe(fullName, function(event) { - if (event.number !== parseInt(number)) { + if (event.sequence !== parseInt(number)) { return; // ignore } // update the build $scope.build = event; - $scope.task = event.tasks[step-1]; + $scope.task = event.builds[step-1]; $scope.$apply(); // start streaming the current build diff --git a/server/static/scripts/controllers/commits.js b/server/static/scripts/controllers/commits.js new file mode 100644 index 000000000..f6e252ad8 --- /dev/null +++ b/server/static/scripts/controllers/commits.js @@ -0,0 +1,67 @@ +(function () { + + /** + * CommitsCtrl responsible for rendering the repo's + * recent commit history. + */ + function CommitsCtrl($scope, $routeParams, builds, repos, users, logs) { + + var owner = $routeParams.owner; + var name = $routeParams.name; + var fullName = owner+'/'+name; + + // Gets the currently authenticated user + users.getCached().then(function(payload){ + $scope.user = payload.data; + }); + + // Gets a repository + repos.get(fullName).then(function(payload){ + $scope.repo = payload.data; + }).catch(function(err){ + $scope.error = err; + }); + + // Gets a list of commits + builds.list(fullName).then(function(payload){ + $scope.builds = angular.isArray(payload.data) ? payload.data : []; + }).catch(function(err){ + $scope.error = err; + }); + + $scope.watch = function(repo) { + repos.watch(repo.full_name).then(function(payload) { + $scope.repo.starred = true; + }); + } + + $scope.unwatch = function(repo) { + repos.unwatch(repo.full_name).then(function() { + $scope.repo.starred = false; + }); + } + + repos.subscribe(fullName, function(event) { + var added = false; + for (var i=0;i<$scope.builds.length;i++) { + var build = $scope.builds[i]; + if (event.sequence !== build.sequence) { + continue; // ignore + } + // update the build status + $scope.builds[i] = event; + $scope.$apply(); + added = true; + } + + if (!added) { + $scope.builds.push(event); + $scope.$apply(); + } + }); + } + + angular + .module('drone') + .controller('CommitsCtrl', CommitsCtrl) +})(); diff --git a/server/static/scripts/controllers/users.js b/server/static/scripts/controllers/users.js index 20d562090..d9b817de0 100644 --- a/server/static/scripts/controllers/users.js +++ b/server/static/scripts/controllers/users.js @@ -18,7 +18,7 @@ // Gets the user tokens tokens.list().then(function(payload){ - $scope.tokens = payload.data; + $scope.tokens = payload.data || []; }); $scope.newToken={Label: ""}; diff --git a/server/static/scripts/views/build.html b/server/static/scripts/views/build.html index 1e634a623..804a0b952 100644 --- a/server/static/scripts/views/build.html +++ b/server/static/scripts/views/build.html @@ -1,10 +1,85 @@ -

{{ repo.full_name }}/{{ build.number }}

+
+ +
+
+ {{ user.login }} + +
+
+ +
+ +
+
+
+
+
+
+

{{ build.message }}

+

authored {{ build.created_at | fromNow }} by @{{ build.author }}

+ + {{ build.sha.substr(0,8) }} + {{ build.branch }} + {{ build.duration | toDuration }} duration + + + + +
+ +
+
+
{{ logs }}
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
NumberStatusStartedFinishedDurationExit CodeMatrix
{{ task.sequence }}{{ task.state }}{{ task.started_at | fromNow }}{{ task.finished_at | fromNow }}{{ task.duration | toDuration }}{{ task.exit_code }}{{ task.environment }}
+
+ + +
+ + + + + + + + + + + diff --git a/server/static/scripts/views/builds.html b/server/static/scripts/views/builds.html index 984d126b7..1dac5ac17 100644 --- a/server/static/scripts/views/builds.html +++ b/server/static/scripts/views/builds.html @@ -3,8 +3,8 @@ Back Settings - - + + @@ -21,16 +21,16 @@ - - + + - - - - - + + + + +
{{ build.number }}
{{ build.sequence }} {{ build.state }} {{ build.started_at | fromNow }} {{ build.duration | toDuration }}{{ build.head_commit ? "push" : "pull request" }}{{ build | ref }}{{ build | sha }}{{ build | author }}{{ build | message }}{{ build.pull_request ? "pull request" : "push" }}{{ build.ref }}{{ build.sha }}{{ build.author }}{{ build.message }}
\ No newline at end of file diff --git a/server/static/scripts/views/repos_edit.html b/server/static/scripts/views/repos_edit.html index ea87a1df1..22085014f 100644 --- a/server/static/scripts/views/repos_edit.html +++ b/server/static/scripts/views/repos_edit.html @@ -4,16 +4,12 @@
- - + +
- - -
-
- - + +
diff --git a/server/static/styles/drone.css b/server/static/styles/drone.css new file mode 100644 index 000000000..0b4ae1375 --- /dev/null +++ b/server/static/styles/drone.css @@ -0,0 +1,590 @@ +html { + font-family: sans-serif; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; +} +body { + margin: 0; +} +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section, +summary { + display: block; +} +audio, +canvas, +progress, +video { + display: inline-block; + vertical-align: baseline; +} +audio:not([controls]) { + display: none; + height: 0; +} +[hidden], +template { + display: none; +} +a { + background-color: transparent; +} +a:active, +a:hover { + outline: 0; +} +abbr[title] { + border-bottom: 1px dotted; +} +b, +strong { + font-weight: bold; +} +dfn { + font-style: italic; +} +h1 { + font-size: 2em; + margin: 0.67em 0; +} +mark { + background: #ff0; + color: #000; +} +small { + font-size: 80%; +} +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} +sup { + top: -0.5em; +} +sub { + bottom: -0.25em; +} +img { + border: 0; +} +svg:not(:root) { + overflow: hidden; +} +figure { + margin: 1em 40px; +} +hr { + -moz-box-sizing: content-box; + box-sizing: content-box; + height: 0; +} +pre { + overflow: auto; +} +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} +button, +input, +optgroup, +select, +textarea { + color: inherit; + font: inherit; + margin: 0; +} +button { + overflow: visible; +} +button, +select { + text-transform: none; +} +button, +html input[type="button"], +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; + cursor: pointer; +} +button[disabled], +html input[disabled] { + cursor: default; +} +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} +input { + line-height: normal; +} +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; + /* 1 */ + padding: 0; + /* 2 */ +} +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} +input[type="search"] { + -webkit-appearance: textfield; + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; + box-sizing: content-box; +} +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} +legend { + border: 0; + padding: 0; +} +textarea { + overflow: auto; +} +optgroup { + font-weight: bold; +} +table { + border-collapse: collapse; + border-spacing: 0; +} +td, +th { + padding: 0; +} +html, +body { + width: 100%; + height: 100%; +} +body { + font-family: 'Open Sans'; + font-size: 14px; + background: #EEE; +} +h1 { + flex: 1 1 auto; + display: inline; + margin: 0px; + line-height: 70px; + font-size: 22px; + font-weight: normal; + padding-left: 40px; + color: #9E9E9E; +} +h1 span { + color: #212121; +} +h1 span:before { + content: "/"; + margin-left: 5px; + margin-right: 5px; +} +header { + height: 70px; + min-height: 70px; + max-height: 70px; + border-bottom: 1px solid #eeeeee; + display: flex; +} +header nav { + flex: 1 1 auto; + display: inline; +} +header nav li { + display: inline-block; + line-height: 70px; +} +header menu { + flex: 1 1 auto; + display: inline; + margin: 0px; + padding: 0px; + padding-right: 20px; + text-align: right; +} +header menu li { + display: inline-block; + line-height: 70px; +} +header menu li a { + border-radius: 999em; + padding: 7px 18px; + border: 1px solid #E0E0E0; + margin-left: 5px; + font-size: 14px; + color: rgba(0, 0, 0, 0.44); + text-decoration: none; +} +header menu li a.primary { + border-color: #4CAF50; + color: #4CAF50; +} +aside { + width: 20%; + max-width: 20%; + min-width: 350px; + background: #fafafa; + box-shadow: -1px 0 1px -2px rgba(0, 0, 0, 0.9); + padding: 20px; + box-sizing: border-box; +} +aside div { + border-top: 1px solid rgba(0, 0, 0, 0.05); + margin-top: 35px; +} +aside div:first-child { + margin-top: 25px; +} +aside div h3 { + border-top: 1px solid rgba(0, 0, 0, 0.44); + text-transform: uppercase; + line-height: 20px; + font-size: 12px; + color: rgba(0, 0, 0, 0.8); + display: inline-block; + margin: 0px; + padding: 10px 0px; + margin-bottom: 15px; +} +aside ul.branches { + color: rgba(0, 0, 0, 0.901961); + padding: 0px; + margin: 0px; +} +aside ul.branches li { + list-style: none; + vertical-align: middle; + height: 30px; + margin-bottom: 3px; +} +aside ul.branches li:before { + content: "\f299"; + font-family: "Material-Design-Iconic-Font"; + display: inline-block; + width: 22px; + height: 22px; + border-radius: 50%; + border: 1px solid #E0E0E0; + color: rgba(0, 0, 0, 0.44); + vertical-align: middle; + margin-right: 10px; + text-align: center; + line-height: 22px; +} +aside ul.branches li.success:before { + border-color: #4CAF50; + color: #4CAF50; +} +aside ul.branches li.failure:before { + content: "\f29a"; + border-color: #F44336; + color: #F44336; +} +aside ul.users { + color: rgba(0, 0, 0, 0.901961); + padding: 0px; + margin: 0px; +} +aside ul.users li { + list-style: none; + vertical-align: middle; + height: 30px; + margin-bottom: 3px; +} +aside ul.users li img { + width: 22px; + height: 22px; + border-radius: 50%; + vertical-align: middle; + margin-right: 10px; +} +article { + flex: 1 1 auto; + box-sizing: border-box; + padding: 40px; + padding-left: 80px; +} +article section pre { + background: #212121; + color: #FFF; + padding: 50px; + font-family: 'Droid Sans Mono'; + position: relative; + line-height: 20px; + font-size: 13px; + margin: 0px; + margin-bottom: 20px; + white-space: pre-wrap; + word-break: break-all; + overflow: hidden; + border-bottom-right-radius: 4px; +} +.logo { + height: 50px; + max-height: 50px; + line-height: 50px; + width: 50px; + margin-left: 10px; + margin-top: 10px; + margin-right: 10px; + border-radius: 50%; + opacity: 0.7; +} +.logo:before { + content: ''; + display: block; + height: 50px; + background: url("../images/drone_32.svg") no-repeat center center; + background-size: 34px; +} +header { + background: #212121; + height: 70px; + min-height: 70px; + max-height: 70px; + display: flex; +} +nav { + background: #FFF; + height: 70px; + min-height: 70px; + max-height: 70px; + display: flex; + border-bottom: 1px solid #F5F5F5; + border-bottom: 1px solid #EEEEEE; +} +.logo { + width: 50px; + margin-left: 10px; + margin-top: 10px; + margin-right: 10px; + border-radius: 50%; + opacity: 0.7; +} +.logo:before { + content: ''; + display: block; + height: 50px; + background: url("../images/logo.svg") no-repeat center center; + background-size: 34px; +} +.logo:hover { + opacity: 1; +} +.title { + flex: 1 1 auto; +} +.user { + text-align: right; + line-height: 70px; +} +.user img { + border-radius: 50%; + width: 34px; + height: 34px; + display: inline-block; + vertical-align: middle; + margin-right: 20px; + margin-left: 15px; +} +.user span { + font-size: 16px; + color: rgba(255, 255, 255, 0.8); + display: inline-block; + vertical-align: middle; +} +main { + padding: 40px 80px 80px; +} +section { + margin: 0px auto; + max-width: 1000px; + background: #FFF; + box-sizing: border-box; +} +section pre { + background: #212121; + color: #FFF; + padding: 50px; + font-family: 'Droid Sans Mono'; + position: relative; + line-height: 20px; + font-size: 13px; + margin: 0px; + margin-bottom: 20px; + white-space: pre-wrap; + word-break: break-all; + overflow: hidden; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; +} +.commit-section { + display: flex; + position: relative; + padding-top: 30px; + padding-bottom: 30px; + padding-left: 30px; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + margin-bottom: 20px; +} +.commit-section .commit-status { + width: 60px; + min-width: 60px; + max-width: 60px; +} +.commit-section .commit-meta { + flex: 1 1 auto; +} +.commit-section .commit-meta h2 { + margin: 0px; + padding: 0px; + font-weight: normal; + font-size: 24px; + margin-bottom: 5px; + margin-top: 3px; +} +.commit-section .commit-meta .octicon { + font-weight: normal; + font-size: 12px; + margin: 0px; + margin-top: 20px; + color: #757575; + display: inline-block; + text-align: right; + background: #eee; + background: #F5F5F5; + padding: 8px 18px; + padding-left: 15px; + color: #9E9E9E; +} +.commit-section .commit-meta .octicon:before { + margin-right: 10px; +} +.commit-section .commit-meta h2 small { + color: #757575; + margin-left: 10px; + font-size: 90%; +} +.commit-section .commit-meta h3 { + margin: 0px; + padding: 0px; + font-weight: normal; + font-size: 14px; + color: #757575; +} +.commit-section .commit-meta span { + color: #212121; +} +.commit-section .commit-meta p { + color: #424242; + padding: 0px; + margin: 0px; + font-size: 14px; + color: #757575; + margin-top: 3px; +} +.commit-section .commit-meta p.tag { + font-weight: normal; + font-size: 12px; + margin: 0px; + margin-top: 15px; + margin-bottom: 10px; + color: #757575; + display: block; + text-align: right; + margin-bottom: 5px; + float: right; + background: #eee; + padding: 5px 20px; + margin-left: 5px; +} +.fab { + position: fixed; + bottom: 20px; + right: 50px; + background: rgba(158, 158, 158, 0.75); + width: 40px; + height: 40px; + z-index: 2; + border-radius: 50%; + opacity: 0.6; + cursor: pointer; +} +.fab:before { + content: "\f297"; + font-family: 'Material-Design-Iconic-Font'; + transform: rotate(90%); + line-height: 40px; + text-align: center; + width: 40px; + display: block; + color: #FFF; + font-size: 22px; + transform: rotate(-90deg); +} +.status { + border-radius: 50%; + width: 50px; + height: 50px; + color: #FFF; + font-size: 20px; + font-family: "Open Sans"; + font-weight: 600; + width: 35px; + height: 35px; + font-size: 16px; +} +.status:before { + line-height: 35px; + text-align: center; + display: block; + font-family: 'Material-Design-Iconic-Font'; +} +.success { + background: #4CAF50; + background: #FFF; + border: 2px solid #4CAF50; +} +.success:before { + content: "\f023"; + color: #4CAF50; +} +.failure { + background: #F44336; + background: #FFF; + border: 2px solid #F44336; +} +.failure:before { + content: "\f29a"; + color: #F44336; +} diff --git a/server/token.go b/server/token.go index 566d606fe..0e615cd32 100644 --- a/server/token.go +++ b/server/token.go @@ -30,13 +30,14 @@ func PostToken(c *gin.Context) { token := &common.Token{} token.Label = in.Label - token.Repos = in.Repos - token.Scopes = in.Scopes + token.UserID = user.ID + // token.Repos = in.Repos + // token.Scopes = in.Scopes token.Login = user.Login token.Kind = common.TokenUser token.Issued = time.Now().UTC().Unix() - err := store.SetToken(token) + err := store.AddToken(token) if err != nil { c.Fail(400, err) } @@ -57,7 +58,7 @@ func DelToken(c *gin.Context) { user := ToUser(c) label := c.Params.ByName("label") - token, err := store.Token(user.Login, label) + token, err := store.TokenLabel(user, label) if err != nil { c.Fail(404, err) } diff --git a/server/user.go b/server/user.go index 607b8f461..faba2c7b3 100644 --- a/server/user.go +++ b/server/user.go @@ -50,7 +50,7 @@ func PutUserCurr(c *gin.Context) { func GetUserRepos(c *gin.Context) { store := ToDatastore(c) user := ToUser(c) - repos, err := store.RepoList(user.Login) + repos, err := store.RepoList(user) if err != nil { c.Fail(400, err) } else { @@ -67,7 +67,7 @@ func GetUserRepos(c *gin.Context) { func GetUserTokens(c *gin.Context) { store := ToDatastore(c) user := ToUser(c) - tokens, err := store.TokenList(user.Login) + tokens, err := store.TokenList(user) if err != nil { c.Fail(400, err) } else { diff --git a/server/users.go b/server/users.go index 72adaae1f..53f095afe 100644 --- a/server/users.go +++ b/server/users.go @@ -35,7 +35,7 @@ func PostUser(c *gin.Context) { user := &common.User{Login: name, Name: name} user.Token = c.Request.FormValue("token") user.Secret = c.Request.FormValue("secret") - if err := store.SetUserNotExists(user); err != nil { + if err := store.AddUser(user); err != nil { c.Fail(400, err) } else { c.JSON(201, user) @@ -51,7 +51,7 @@ func PostUser(c *gin.Context) { func GetUser(c *gin.Context) { store := ToDatastore(c) name := c.Params.ByName("name") - user, err := store.User(name) + user, err := store.UserLogin(name) if err != nil { c.Fail(404, err) } else { @@ -69,7 +69,7 @@ func PutUser(c *gin.Context) { store := ToDatastore(c) me := ToUser(c) name := c.Params.ByName("name") - user, err := store.User(name) + user, err := store.UserLogin(name) if err != nil { c.Fail(404, err) return @@ -106,7 +106,7 @@ func DeleteUser(c *gin.Context) { store := ToDatastore(c) me := ToUser(c) name := c.Params.ByName("name") - user, err := store.User(name) + user, err := store.UserLogin(name) if err != nil { c.Fail(404, err) return diff --git a/server/ws.go b/server/ws.go index fda0bcb63..56da691fa 100644 --- a/server/ws.go +++ b/server/ws.go @@ -88,11 +88,11 @@ func GetRepoEvents(c *gin.Context) { } func GetStream(c *gin.Context) { - // store := ToDatastore(c) + store := ToDatastore(c) repo := ToRepo(c) runner := ToRunner(c) - build, _ := strconv.Atoi(c.Params.ByName("build")) - task, _ := strconv.Atoi(c.Params.ByName("number")) + commitseq, _ := strconv.Atoi(c.Params.ByName("build")) + buildseq, _ := strconv.Atoi(c.Params.ByName("number")) // agent, err := store.BuildAgent(repo.FullName, build) // if err != nil { @@ -100,7 +100,18 @@ func GetStream(c *gin.Context) { // return // } - rc, err := runner.Logs(repo.FullName, build, task) + commit, err := store.CommitSeq(repo, commitseq) + if err != nil { + c.Fail(404, err) + return + } + build, err := store.BuildSeq(commit, buildseq) + if err != nil { + c.Fail(404, err) + return + } + + rc, err := runner.Logs(build) if err != nil { c.Fail(404, err) return diff --git a/settings/settings.go b/settings/settings.go index 235693a54..62d19f392 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -87,7 +87,8 @@ type Docker struct { // Database represents the configuration details used // to connect to the embedded Bolt database. type Database struct { - Path string `toml:"path"` + Driver string `toml:"driver"` + Datasource string `toml:"datasource"` } // Settings defines global settings for the Drone system.