// Delete accepts a request to delete a worker // from the pool. // // DELETE /sudo/api/workers/:id // func DelWorker(c web.C, w http.ResponseWriter, r *http.Request) { ctx := context.FromC(c) pool := pool.FromContext(ctx) uuid := c.URLParams["id"] server, err := datastore.GetServer(ctx, uuid) if err != nil { w.WriteHeader(http.StatusNotFound) return } err = datastore.DelServer(ctx, server) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } for _, worker := range pool.List() { if worker.(*docker.Docker).UUID == uuid { pool.Deallocate(worker) w.WriteHeader(http.StatusNoContent) return } } w.WriteHeader(http.StatusNotFound) }
// CreatePerson accepts a request to add a new person. // // POST /api/people // func CreatePerson(c web.C, w http.ResponseWriter, r *http.Request) { var ( ctx = context.FromC(c) ) // Unmarshal the person from the payload defer r.Body.Close() in := struct { Name string `json:"name"` }{} if err := json.NewDecoder(r.Body).Decode(&in); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // Validate input if len(in.Name) < 1 { http.Error(w, "no name given", http.StatusBadRequest) return } // Create our 'normal' model. person := &model.Person{Name: in.Name} err := datastore.CreatePerson(ctx, person) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(person) }
// PostWorker accepts a request to allocate a new // worker to the pool. // // POST /api/workers // func PostWorker(c web.C, w http.ResponseWriter, r *http.Request) { ctx := context.FromC(c) pool := pool.FromContext(ctx) node := r.FormValue("address") pool.Allocate(docker.NewHost(node)) w.WriteHeader(http.StatusOK) }
// GetCC accepts a request to retrieve the latest build // status for the given repository from the datastore and // in CCTray XML format. // // GET /api/badge/:host/:owner/:name/cc.xml // func GetCC(c web.C, w http.ResponseWriter, r *http.Request) { var ctx = context.FromC(c) var ( host = c.URLParams["host"] owner = c.URLParams["owner"] name = c.URLParams["name"] ) w.Header().Set("Content-Type", "application/xml") repo, err := datastore.GetRepoName(ctx, host, owner, name) if err != nil { w.WriteHeader(http.StatusNotFound) return } commits, err := datastore.GetCommitList(ctx, repo, 1, 0) if err != nil || len(commits) == 0 { w.WriteHeader(http.StatusNotFound) return } var link = httputil.GetURL(r) + "/" + repo.Host + "/" + repo.Owner + "/" + repo.Name var cc = model.NewCC(repo, commits[0], link) xml.NewEncoder(w).Encode(cc) }
// PutRegion accepts a request to retrieve information about a particular region. // // PUT /api/regions/:region // func PutRegion(c web.C, w http.ResponseWriter, r *http.Request) { var ( ctx = context.FromC(c) idStr = c.URLParams["region"] region model.Region ) id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { w.WriteHeader(http.StatusBadRequest) return } if !regionFromRequest(c, w, r, ®ion) { return } region.ID = id err = datastore.UpdateRegion(ctx, ®ion) if err != nil { log.FromContext(ctx).WithField("err", err).Error("Error updating region") w.WriteHeader(http.StatusNotFound) return } json.NewEncoder(w).Encode(®ion) }
// SetRepo is a middleware function that retrieves // the repository and stores in the context. func SetRepo(c *web.C, h http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { var ( ctx = context.FromC(*c) host = c.URLParams["host"] owner = c.URLParams["owner"] name = c.URLParams["name"] user = ToUser(c) ) repo, err := datastore.GetRepoName(ctx, host, owner, name) switch { case err != nil && user == nil: w.WriteHeader(http.StatusUnauthorized) return case err != nil && user != nil: w.WriteHeader(http.StatusNotFound) return } role, _ := datastore.GetPerm(ctx, user, repo) RepoToC(c, repo) RoleToC(c, role) h.ServeHTTP(w, r) } return http.HandlerFunc(fn) }
// GetBuildPage accepts a request to retrieve the build // output page for the package version, channel and SDK // from the datastore in JSON format. // // If the SDK is not provided, the system will lookup // the latest SDK version for this package. // // GET /:name/:number/:channel/:sdk // func GetBuildPage(c web.C, w http.ResponseWriter, r *http.Request) { ctx := context.FromC(c) name := c.URLParams["name"] number := c.URLParams["number"] channel := c.URLParams["channel"] sdk := c.URLParams["sdk"] // If no SDK is provided we should use the most recent // SDK number associated with the Package version. var build *resource.Build var err error if len(sdk) == 0 { build, err = datastore.GetBuildLatest(ctx, name, number, channel) } else { build, err = datastore.GetBuild(ctx, name, number, channel, sdk) } // If the error is not nil then we can // display some sort of NotFound page. if err != nil { http.Error(w, "Not Found", http.StatusNotFound) return } BuildTempl.Execute(w, struct { Build *resource.Build Error error }{build, err}) }
// PostUserSync accepts a request to post user sync // // POST /api/user/sync // func PostUserSync(c web.C, w http.ResponseWriter, r *http.Request) { var ctx = context.FromC(c) var user = ToUser(c) if user == nil { w.WriteHeader(http.StatusUnauthorized) return } var remote = remote.Lookup(user.Remote) if remote == nil { w.WriteHeader(http.StatusNotFound) return } if user.Syncing { w.WriteHeader(http.StatusConflict) return } user.Syncing = true if err := datastore.PutUser(ctx, user); err != nil { w.WriteHeader(http.StatusNotFound) return } go sync.SyncUser(ctx, user, remote) w.WriteHeader(http.StatusNoContent) return }
// PutUser accepts a request to update the currently // authenticated User profile. // // PUT /api/user // func PutUser(c web.C, w http.ResponseWriter, r *http.Request) { var ctx = context.FromC(c) var user = ToUser(c) if user == nil { w.WriteHeader(http.StatusUnauthorized) return } // unmarshal the repository from the payload defer r.Body.Close() in := model.User{} if err := json.NewDecoder(r.Body).Decode(&in); err != nil { w.WriteHeader(http.StatusBadRequest) return } // update the user email if len(in.Email) != 0 { user.SetEmail(in.Email) } // update the user full name if len(in.Name) != 0 { user.Name = in.Name } // update the database if err := datastore.PutUser(ctx, user); err != nil { w.WriteHeader(http.StatusInternalServerError) return } json.NewEncoder(w).Encode(user) }
// PostWorker accepts a request to allocate a new // worker to the pool. // // POST /sudo/api/workers // func PostWorker(c web.C, w http.ResponseWriter, r *http.Request) { ctx := context.FromC(c) pool := pool.FromContext(ctx) server := resource.Server{} // read the worker data from the body defer r.Body.Close() if err := json.NewDecoder(r.Body).Decode(&server); err != nil { println(err.Error()) w.WriteHeader(http.StatusBadRequest) return } // add the worker to the database err := datastore.PutServer(ctx, &server) if err != nil { println(err.Error()) w.WriteHeader(http.StatusInternalServerError) return } // create a new worker from the Docker client client, err := docker.NewCert(server.Host, []byte(server.Cert), []byte(server.Key)) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } // append user-friendly data to the host client.Host = client.Host pool.Allocate(client) w.WriteHeader(http.StatusOK) }
// PostHook accepts a post-commit hook and parses the payload // in order to trigger a build. The payload is specified to the // remote system (ie GitHub) and will therefore get parsed by // the appropriate remote plugin. // // POST /api/repos/{host}/{owner}/{name}/branches/{branch}/commits/{commit} // func PostCommit(c web.C, w http.ResponseWriter, r *http.Request) { var ctx = context.FromC(c) var ( branch = c.URLParams["branch"] hash = c.URLParams["commit"] host = c.URLParams["host"] repo = ToRepo(c) remote = remote.Lookup(host) ) commit, err := datastore.GetCommitSha(ctx, repo, branch, hash) if err != nil { w.WriteHeader(http.StatusNotFound) return } if commit.Status == model.StatusStarted || commit.Status == model.StatusEnqueue { w.WriteHeader(http.StatusConflict) return } commit.Status = model.StatusEnqueue commit.Started = 0 commit.Finished = 0 commit.Duration = 0 if err := datastore.PutCommit(ctx, commit); err != nil { w.WriteHeader(http.StatusInternalServerError) return } owner, err := datastore.GetUser(ctx, repo.UserID) if err != nil { w.WriteHeader(http.StatusBadRequest) return } // Request a new token and update user_token, err := remote.GetToken(owner) if user_token != nil { owner.Access = user_token.AccessToken owner.Secret = user_token.RefreshToken owner.TokenExpiry = user_token.Expiry datastore.PutUser(ctx, owner) } else if err != nil { w.WriteHeader(http.StatusBadRequest) return } // drop the items on the queue go worker.Do(ctx, &worker.Work{ User: owner, Repo: repo, Commit: commit, Host: httputil.GetURL(r), }) w.WriteHeader(http.StatusOK) }
// WsUser will upgrade the connection to a Websocket and will stream // all events to the browser pertinent to the authenticated user. If the user // is not authenticated, only public events are streamed. func WsUser(c web.C, w http.ResponseWriter, r *http.Request) { var ctx = context.FromC(c) var user = ToUser(c) // upgrade the websocket ws, err := upgrader.Upgrade(w, r, nil) if err != nil { w.WriteHeader(http.StatusBadRequest) return } // register a channel for global events channel := pubsub.Register(ctx, "_global") sub := channel.Subscribe() ticker := time.NewTicker(pingPeriod) defer func() { ticker.Stop() sub.Close() ws.Close() }() go func() { for { select { case msg := <-sub.Read(): work, ok := msg.(*worker.Work) if !ok { break } // user must have read access to the repository // in order to pass this message along if role, err := datastore.GetPerm(ctx, user, work.Repo); err != nil || role.Read == false { break } ws.SetWriteDeadline(time.Now().Add(writeWait)) err := ws.WriteJSON(work) if err != nil { ws.Close() return } case <-sub.CloseNotify(): ws.Close() return case <-ticker.C: ws.SetWriteDeadline(time.Now().Add(writeWait)) err := ws.WriteMessage(websocket.PingMessage, []byte{}) if err != nil { ws.Close() return } } } }() readWebsocket(ws) }
// PostUser accepts a request to create a new user in the // system. The created user account is returned in JSON // format if successful. // // POST /api/users/:host/:login // func PostUser(c web.C, w http.ResponseWriter, r *http.Request) { var ctx = context.FromC(c) var ( host = c.URLParams["host"] login = c.URLParams["login"] ) var remote = remote.Lookup(host) if remote == nil { w.WriteHeader(http.StatusNotFound) return } // not sure I love this, but POST now flexibly accepts the oauth_token for // GitHub as either application/x-www-form-urlencoded OR as applcation/json // with this format: // { "oauth_token": "...." } var oauthToken string switch cnttype := r.Header.Get("Content-Type"); cnttype { case "application/json": var out interface{} err := json.NewDecoder(r.Body).Decode(&out) if err == nil { if val, ok := out.(map[string]interface{})["oauth_token"]; ok { oauthToken = val.(string) } } case "application/x-www-form-urlencoded": oauthToken = r.PostForm.Get("oauth_token") default: // we don't recognize the content-type, but it isn't worth it // to error here log.Printf("PostUser(%s) Unknown 'Content-Type': %s)", r.URL, cnttype) } account := model.NewUser(host, login, "", oauthToken) if err := datastore.PostUser(ctx, account); err != nil { w.WriteHeader(http.StatusBadRequest) return } // borrowed this concept from login.go. upon first creation we // may trying syncing the user's repositories. account.Syncing = account.IsStale() if err := datastore.PutUser(ctx, account); err != nil { log.Println(err) w.WriteHeader(http.StatusBadRequest) return } if account.Syncing { log.Println("sync user account.", account.Login) // sync inside a goroutine go sync.SyncUser(ctx, account, remote) } json.NewEncoder(w).Encode(account) }
func main() { extdirect.Provider.RegisterAction(reflect.TypeOf(Db{})) goji.Get(extdirect.Provider.URL, extdirect.API(extdirect.Provider)) goji.Post(extdirect.Provider.URL, func(c web.C, w http.ResponseWriter, r *http.Request) { extdirect.ActionsHandlerCtx(extdirect.Provider)(gcontext.FromC(c), w, r) }) goji.Use(gojistatic.Static("public", gojistatic.StaticOptions{SkipLogging: true})) goji.Serve() }
// GetFeed accepts a request to retrieve a feed // of the latest builds in JSON format. // // GET /api/feed // func GetFeed(c web.C, w http.ResponseWriter, r *http.Request) { ctx := context.FromC(c) pkg, err := datastore.GetFeed(ctx) if err != nil { w.WriteHeader(http.StatusNotFound) return } json.NewEncoder(w).Encode(pkg) }
// GetUserList accepts a request to retrieve all users // from the datastore and return encoded in JSON format. // // GET /api/users // func GetUserList(c web.C, w http.ResponseWriter, r *http.Request) { var ctx = context.FromC(c) users, err := datastore.GetUserList(ctx) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } json.NewEncoder(w).Encode(users) }
// GetChannel accepts a request to retrieve the latest // SDK version and revision for the specified channel. // // GET /api/channel/:name // func GetChannel(c web.C, w http.ResponseWriter, r *http.Request) { ctx := context.FromC(c) name := c.URLParams["channel"] channel, err := datastore.GetChannel(ctx, name) if err != nil { w.WriteHeader(http.StatusNotFound) return } json.NewEncoder(w).Encode(channel) }
// SetUser is a middleware function that retrieves // the currently authenticated user from the request // and stores in the context. func SetUser(c *web.C, h http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { var ctx = context.FromC(*c) var user = session.GetUser(ctx, r) if user != nil && user.ID != 0 { UserToC(c, user) } h.ServeHTTP(w, r) } return http.HandlerFunc(fn) }
// GetCommitList accepts a request to retrieve a list // of recent commits by Repo, and retur in JSON format. // // GET /api/repos/:host/:owner/:name/commits // func GetCommitList(c web.C, w http.ResponseWriter, r *http.Request) { var ctx = context.FromC(c) var repo = ToRepo(c) commits, err := datastore.GetCommitList(ctx, repo) if err != nil { w.WriteHeader(http.StatusNotFound) return } json.NewEncoder(w).Encode(commits) }
// GetBuildLatest accepts a request to retrieve the build // details for the package version, channel and latest SDK // from the datastore in JSON format. // // GET /api/packages/:name/:number/channel/:channel // func GetBuildLatest(c web.C, w http.ResponseWriter, r *http.Request) { ctx := context.FromC(c) name := c.URLParams["name"] number := c.URLParams["number"] channel := c.URLParams["channel"] build, err := datastore.GetBuildLatest(ctx, name, number, channel) if err != nil { w.WriteHeader(http.StatusNotFound) return } json.NewEncoder(w).Encode(build) }
// RecoverMiddleware helps the server recover from panics. It ensures that // no request can fully bring down the horizon server, and it also logs the // panics to the logging subsystem. func RecoverMiddleware(c *web.C, h http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { ctx := gctx.FromC(*c) defer func() { if err := recover(); err != nil { stack := make([]byte, 4096) // 4k of stack should be sufficient to see the source n := runtime.Stack(stack, false) log. WithField(ctx, "stacktrace", string(stack[:n])). Errorf("panic: %+v", err) //TODO: include stack trace if in debug mode problem.Render(gctx.FromC(*c), w, problem.ServerError) } }() h.ServeHTTP(w, r) } return http.HandlerFunc(fn) }
// PutRepo accapets a request to update the named repository // in the datastore. It expects a JSON input and returns the // updated repository in JSON format if successful. // // PUT /api/repos/:host/:owner/:name // func PutRepo(c web.C, w http.ResponseWriter, r *http.Request) { var ctx = context.FromC(c) var repo = ToRepo(c) var user = ToUser(c) // unmarshal the repository from the payload defer r.Body.Close() in := struct { PostCommit *bool `json:"post_commits"` PullRequest *bool `json:"pull_requests"` Privileged *bool `json:"privileged"` Params *string `json:"params"` Timeout *int64 `json:"timeout"` PublicKey *string `json:"public_key"` PrivateKey *string `json:"private_key"` }{} if err := json.NewDecoder(r.Body).Decode(&in); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if in.Params != nil { repo.Params = *in.Params if _, err := repo.ParamMap(); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } } if in.PostCommit != nil { repo.PostCommit = *in.PostCommit } if in.PullRequest != nil { repo.PullRequest = *in.PullRequest } if in.Privileged != nil && user.Admin { repo.Privileged = *in.Privileged } if in.Timeout != nil && user.Admin { repo.Timeout = *in.Timeout } if in.PrivateKey != nil && in.PublicKey != nil { repo.PublicKey = *in.PublicKey repo.PrivateKey = *in.PrivateKey } if err := datastore.PutRepo(ctx, repo); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } json.NewEncoder(w).Encode(repo) }
// PostUser accepts a request to create a new user in the // system. The created user account is returned in JSON // format if successful. // // POST /api/users/:host/:login // func PostUser(c web.C, w http.ResponseWriter, r *http.Request) { var ctx = context.FromC(c) var ( host = c.URLParams["host"] login = c.URLParams["login"] ) account := model.NewUser(host, login, "") if err := datastore.PostUser(ctx, account); err != nil { w.WriteHeader(http.StatusBadRequest) return } json.NewEncoder(w).Encode(account) }
// GetUserFeed accepts a request to get the user's latest // build feed, across all repositories, from the datastore. // The results are encoded and returned in JSON format. // // GET /api/user/feed // func GetUserFeed(c web.C, w http.ResponseWriter, r *http.Request) { var ctx = context.FromC(c) var user = ToUser(c) if user == nil { w.WriteHeader(http.StatusUnauthorized) return } repos, err := datastore.GetCommitListUser(ctx, user) if err != nil { w.WriteHeader(http.StatusNotFound) return } json.NewEncoder(w).Encode(&repos) }
// GetBadge accepts a request to retrieve the named // repo and branhes latest build details from the datastore // and return an SVG badges representing the build results. // // GET /api/badge/:host/:owner/:name/status.svg // func GetBadge(c web.C, w http.ResponseWriter, r *http.Request) { var ctx = context.FromC(c) var ( host = c.URLParams["host"] owner = c.URLParams["owner"] name = c.URLParams["name"] branch = r.FormValue("branch") style = r.FormValue("style") ) // an SVG response is always served, even when error, so // we can go ahead and set the content type appropriately. w.Header().Set("Content-Type", "image/svg+xml") badge, ok := badgeStyles[style] if !ok { badge = defaultBadge } repo, err := datastore.GetRepoName(ctx, host, owner, name) if err != nil { w.Write(badge.none) return } if len(branch) == 0 { branch = model.DefaultBranch } commit, _ := datastore.GetCommitLast(ctx, repo, branch) // if no commit was found then display // the 'none' badge, instead of throwing // an error response if commit == nil { w.Write(badge.none) return } switch commit.Status { case model.StatusSuccess: w.Write(badge.success) case model.StatusFailure: w.Write(badge.failure) case model.StatusError: w.Write(badge.err) case model.StatusEnqueue, model.StatusStarted: w.Write(badge.started) default: w.Write(badge.none) } }
// Delete accepts a request to delete a worker // from the pool. // // DELETE /api/workers // func DelWorker(c web.C, w http.ResponseWriter, r *http.Request) { ctx := context.FromC(c) pool := pool.FromContext(ctx) uuid := r.FormValue("id") for _, worker := range pool.List() { if worker.(*docker.Docker).UUID != uuid { pool.Deallocate(worker) w.WriteHeader(http.StatusNoContent) return } } w.WriteHeader(http.StatusNotFound) }
// PostRepo accapets a request to activate the named repository // in the datastore. It returns a 201 status created if successful // // POST /api/repos/:host/:owner/:name // func PostRepo(c web.C, w http.ResponseWriter, r *http.Request) { var ctx = context.FromC(c) var repo = ToRepo(c) var user = ToUser(c) // update the repo active flag and fields repo.Active = true repo.PullRequest = true repo.PostCommit = true repo.UserID = user.ID repo.Timeout = 3600 // default to 1 hour // generate a secret key for post-commit hooks if len(repo.Token) == 0 { repo.Token = model.GenerateToken() } // generates the rsa key if len(repo.PublicKey) == 0 || len(repo.PrivateKey) == 0 { key, err := sshutil.GeneratePrivateKey() if err != nil { w.WriteHeader(http.StatusInternalServerError) return } repo.PublicKey = sshutil.MarshalPublicKey(&key.PublicKey) repo.PrivateKey = sshutil.MarshalPrivateKey(key) } var remote = remote.Lookup(repo.Host) if remote == nil { w.WriteHeader(http.StatusNotFound) return } // setup the post-commit hook with the remote system and // if necessary, register the public key var hook = fmt.Sprintf("%s/api/hook/%s/%s", httputil.GetURL(r), repo.Remote, repo.Token) if err := remote.Activate(user, repo, hook); err != nil { w.WriteHeader(http.StatusInternalServerError) return } if err := datastore.PutRepo(ctx, repo); err != nil { w.WriteHeader(http.StatusInternalServerError) return } w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(repo) }
// GetUser accepts a request to retrieve a user by hostname // and login from the datastore and return encoded in JSON // format. // // GET /api/users/:host/:login // func GetUser(c web.C, w http.ResponseWriter, r *http.Request) { var ctx = context.FromC(c) var ( user = ToUser(c) host = c.URLParams["host"] login = c.URLParams["login"] ) user, err := datastore.GetUserLogin(ctx, host, login) if err != nil { w.WriteHeader(http.StatusNotFound) return } json.NewEncoder(w).Encode(user) }
// DelRepo accepts a request to inactivate the named // repository. This will disable all builds in the system // for this repository. // // DEL /api/repos/:host/:owner/:name // func DelRepo(c web.C, w http.ResponseWriter, r *http.Request) { var ctx = context.FromC(c) var repo = ToRepo(c) // disable everything repo.Active = false repo.PullRequest = false repo.PostCommit = false if err := datastore.PutRepo(ctx, repo); err != nil { w.WriteHeader(http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) }
// GetCommit accepts a request to retrieve a commit // from the datastore for the given repository, branch and // commit hash. // // GET /api/repos/:host/:owner/:name/branches/:branch/commits/:commit // func GetCommit(c web.C, w http.ResponseWriter, r *http.Request) { var ctx = context.FromC(c) var ( branch = c.URLParams["branch"] hash = c.URLParams["commit"] repo = ToRepo(c) ) commit, err := datastore.GetCommitSha(ctx, repo, branch, hash) if err != nil { w.WriteHeader(http.StatusNotFound) return } json.NewEncoder(w).Encode(commit) }