github.com/billybanfield/evergreen@v0.0.0-20170525200750-eeee692790f7/service/ui.go (about) 1 package service 2 3 import ( 4 "fmt" 5 htmlTemplate "html/template" 6 "net/http" 7 "path/filepath" 8 "runtime/debug" 9 "strings" 10 11 "github.com/evergreen-ci/evergreen" 12 "github.com/evergreen-ci/evergreen/apiv3/route" 13 "github.com/evergreen-ci/evergreen/auth" 14 "github.com/evergreen-ci/evergreen/db" 15 "github.com/evergreen-ci/evergreen/plugin" 16 "github.com/evergreen-ci/evergreen/util" 17 "github.com/evergreen-ci/render" 18 "github.com/gorilla/mux" 19 "github.com/gorilla/sessions" 20 "github.com/mongodb/grip" 21 "github.com/pkg/errors" 22 ) 23 24 const ( 25 ProjectKey string = "projectKey" 26 ProjectCookieName string = "mci-project-cookie" 27 28 ProjectUnknown string = "Unknown Project" 29 30 // Format string for when a project is not found 31 ProjectNotFoundFormat string = "Project '%v' not found" 32 ) 33 34 // UIServer provides a web interface for Evergreen. 35 type UIServer struct { 36 *render.Render 37 38 // Home is the root path on disk from which relative urls are constructed for loading 39 // plugins or other assets. 40 Home string 41 42 // The root URL of the server, used in redirects for instance. 43 RootURL string 44 45 //authManager 46 UserManager auth.UserManager 47 Settings evergreen.Settings 48 CookieStore *sessions.CookieStore 49 PluginTemplates map[string]*htmlTemplate.Template 50 clientConfig *evergreen.ClientConfig 51 plugin.PanelManager 52 } 53 54 func NewUIServer(settings *evergreen.Settings, home string) (*UIServer, error) { 55 uis := &UIServer{} 56 if len(evergreen.Logger.Appenders) == 0 { 57 evergreen.SetLegacyLogger() 58 } 59 db.SetGlobalSessionProvider(db.SessionFactoryFromConfig(settings)) 60 61 uis.Settings = *settings 62 uis.Home = home 63 64 userManager, err := auth.LoadUserManager(settings.AuthConfig) 65 if err != nil { 66 return nil, err 67 } 68 uis.UserManager = userManager 69 70 clientConfig, err := getClientConfig(settings) 71 if err != nil { 72 return nil, err 73 } 74 uis.clientConfig = clientConfig 75 76 uis.CookieStore = sessions.NewCookieStore([]byte(settings.Ui.Secret)) 77 78 uis.PluginTemplates = map[string]*htmlTemplate.Template{} 79 80 return uis, nil 81 } 82 83 // InitPlugins registers all installed plugins with the UI Server. 84 func (uis *UIServer) InitPlugins() error { 85 uis.PanelManager = &plugin.SimplePanelManager{} 86 return uis.PanelManager.RegisterPlugins(plugin.UIPlugins) 87 } 88 89 // NewRouter sets up a request router for the UI, installing 90 // hard-coded routes as well as those belonging to plugins. 91 func (uis *UIServer) NewRouter() (*mux.Router, error) { 92 r := mux.NewRouter().StrictSlash(true) 93 94 // User login and logout 95 r.HandleFunc("/login", uis.loginPage).Methods("GET") 96 r.HandleFunc("/login", uis.login).Methods("POST") 97 98 // User login with redirect to external site and redirect back 99 if uis.UserManager.GetLoginHandler != nil { 100 r.HandleFunc("/login/redirect", uis.UserManager.GetLoginHandler(uis.RootURL)).Methods("GET") 101 } 102 if uis.UserManager.GetLoginCallbackHandler != nil { 103 r.HandleFunc("/login/redirect/callback", uis.UserManager.GetLoginCallbackHandler()).Methods("GET") 104 } 105 r.HandleFunc("/logout", uis.logout) 106 107 requireLogin := func(next http.HandlerFunc) http.HandlerFunc { 108 return requireUser(next, uis.RedirectToLogin) 109 } 110 111 // Waterfall pages 112 r.HandleFunc("/", uis.loadCtx(uis.waterfallPage)) 113 r.HandleFunc("/waterfall", uis.loadCtx(uis.waterfallPage)) 114 r.HandleFunc("/waterfall/{project_id}", uis.loadCtx(uis.waterfallPage)) 115 116 // Timeline page 117 r.HandleFunc("/timeline/{project_id}", uis.loadCtx(uis.timeline)) 118 r.HandleFunc("/timeline", uis.loadCtx(uis.timeline)) 119 r.HandleFunc("/json/timeline/{project_id}", uis.loadCtx(uis.timelineJson)) 120 r.HandleFunc("/json/patches/project/{project_id}", uis.loadCtx(uis.patchTimelineJson)) 121 r.HandleFunc("/json/patches/user/{user_id}", uis.loadCtx(uis.patchTimelineJson)) 122 123 // Grid page 124 r.HandleFunc("/grid", uis.loadCtx(uis.grid)) 125 r.HandleFunc("/grid/{project_id}", uis.loadCtx(uis.grid)) 126 r.HandleFunc("/grid/{project_id}/{version_id}", uis.loadCtx(uis.grid)) 127 r.HandleFunc("/grid/{project_id}/{version_id}/{depth}", uis.loadCtx(uis.grid)) 128 129 // Task page (and related routes) 130 r.HandleFunc("/task/{task_id}", uis.loadCtx(uis.taskPage)).Methods("GET") 131 r.HandleFunc("/task/{task_id}/{execution}", uis.loadCtx(uis.taskPage)).Methods("GET") 132 r.HandleFunc("/tasks/{task_id}", requireLogin(uis.loadCtx(uis.taskModify))).Methods("PUT") 133 r.HandleFunc("/json/task_log/{task_id}", uis.loadCtx(uis.taskLog)) 134 r.HandleFunc("/json/task_log/{task_id}/{execution}", uis.loadCtx(uis.taskLog)) 135 r.HandleFunc("/task_log_raw/{task_id}/{execution}", uis.loadCtx(uis.taskLogRaw)) 136 137 // Test Logs 138 r.HandleFunc("/test_log/{task_id}/{task_execution}/{test_name}", uis.loadCtx(uis.testLog)) 139 r.HandleFunc("/test_log/{log_id}", uis.loadCtx(uis.testLog)) 140 141 // Build page 142 r.HandleFunc("/build/{build_id}", uis.loadCtx(uis.buildPage)).Methods("GET") 143 r.HandleFunc("/builds/{build_id}", requireLogin(uis.loadCtx(uis.modifyBuild))).Methods("PUT") 144 r.HandleFunc("/json/build_history/{build_id}", uis.loadCtx(uis.buildHistory)).Methods("GET") 145 146 // Version page 147 r.HandleFunc("/version/{version_id}", uis.loadCtx(uis.versionPage)).Methods("GET") 148 r.HandleFunc("/version/{version_id}", requireLogin(uis.loadCtx(uis.modifyVersion))).Methods("PUT") 149 r.HandleFunc("/json/version_history/{version_id}", uis.loadCtx(uis.versionHistory)) 150 r.HandleFunc("/version/{project_id}/{revision}", uis.loadCtx(uis.versionFind)).Methods("GET") 151 152 // Hosts 153 r.HandleFunc("/hosts", requireLogin(uis.loadCtx(uis.hostsPage))).Methods("GET") 154 r.HandleFunc("/hosts", requireLogin(uis.loadCtx(uis.modifyHosts))).Methods("PUT") 155 r.HandleFunc("/host/{host_id}", requireLogin(uis.loadCtx(uis.hostPage))).Methods("GET") 156 r.HandleFunc("/host/{host_id}", requireLogin(uis.loadCtx(uis.modifyHost))).Methods("PUT") 157 158 // Distros 159 r.HandleFunc("/distros", requireLogin(uis.loadCtx(uis.distrosPage))).Methods("GET") 160 r.HandleFunc("/distros", uis.requireSuperUser(uis.loadCtx(uis.addDistro))).Methods("PUT") 161 r.HandleFunc("/distros/{distro_id}", requireLogin(uis.loadCtx(uis.getDistro))).Methods("GET") 162 r.HandleFunc("/distros/{distro_id}", uis.requireSuperUser(uis.loadCtx(uis.addDistro))).Methods("PUT") 163 r.HandleFunc("/distros/{distro_id}", uis.requireSuperUser(uis.loadCtx(uis.modifyDistro))).Methods("POST") 164 r.HandleFunc("/distros/{distro_id}", uis.requireSuperUser(uis.loadCtx(uis.removeDistro))).Methods("DELETE") 165 166 // Event Logs 167 r.HandleFunc("/event_log/{resource_type}/{resource_id:[\\w_\\-\\:\\.\\@]+}", uis.loadCtx(uis.fullEventLogs)) 168 169 // Task History 170 r.HandleFunc("/task_history/{task_name}", uis.loadCtx(uis.taskHistoryPage)) 171 r.HandleFunc("/task_history/{project_id}/{task_name}", uis.loadCtx(uis.taskHistoryPage)) 172 r.HandleFunc("/task_history/{project_id}/{task_name}/pickaxe", uis.loadCtx(uis.taskHistoryPickaxe)) 173 r.HandleFunc("/task_history/{project_id}/{task_name}/test_names", uis.loadCtx(uis.taskHistoryTestNames)) 174 175 // History Drawer Endpoints 176 r.HandleFunc("/history/tasks/{task_id}/{window}", uis.loadCtx(uis.taskHistoryDrawer)) 177 r.HandleFunc("/history/versions/{version_id}/{window}", uis.loadCtx(uis.versionHistoryDrawer)) 178 179 // Variant History 180 r.HandleFunc("/build_variant/{project_id}/{variant}", uis.loadCtx(uis.variantHistory)) 181 182 // Task queues 183 r.HandleFunc("/task_queue/", uis.loadCtx(uis.allTaskQueues)) 184 185 // Scheduler 186 r.HandleFunc("/scheduler/distro/{distro_id}", uis.loadCtx(uis.getSchedulerPage)) 187 r.HandleFunc("/scheduler/distro/{distro_id}/logs", uis.loadCtx(uis.getSchedulerLogs)) 188 r.HandleFunc("/scheduler/stats", uis.loadCtx(uis.schedulerStatsPage)) 189 r.HandleFunc("/scheduler/distro/{distro_id}/stats", uis.loadCtx(uis.averageSchedulerStats)) 190 r.HandleFunc("/scheduler/stats/utilization", uis.loadCtx(uis.schedulerHostUtilization)) 191 192 // Patch pages 193 r.HandleFunc("/patch/{patch_id}", requireLogin(uis.loadCtx(uis.patchPage))).Methods("GET") 194 r.HandleFunc("/patch/{patch_id}", requireLogin(uis.loadCtx(uis.schedulePatch))).Methods("POST") 195 r.HandleFunc("/diff/{patch_id}/", requireLogin(uis.loadCtx(uis.diffPage))) 196 r.HandleFunc("/filediff/{patch_id}/", requireLogin(uis.loadCtx(uis.fileDiffPage))) 197 r.HandleFunc("/rawdiff/{patch_id}/", requireLogin(uis.loadCtx(uis.rawDiffPage))) 198 r.HandleFunc("/patches", requireLogin(uis.loadCtx(uis.patchTimeline))) 199 r.HandleFunc("/patches/project/{project_id}", requireLogin(uis.loadCtx(uis.patchTimeline))) 200 r.HandleFunc("/patches/user/{user_id}", requireLogin(uis.loadCtx(uis.userPatchesTimeline))) 201 r.HandleFunc("/patches/mine", requireLogin(uis.loadCtx(uis.myPatchesTimeline))) 202 203 // Spawnhost routes 204 r.HandleFunc("/spawn", requireLogin(uis.loadCtx(uis.spawnPage))).Methods("GET") 205 r.HandleFunc("/spawn", requireLogin(uis.loadCtx(uis.requestNewHost))).Methods("PUT") 206 r.HandleFunc("/spawn", requireLogin(uis.loadCtx(uis.modifySpawnHost))).Methods("POST") 207 r.HandleFunc("/spawn/hosts", requireLogin(uis.loadCtx(uis.getSpawnedHosts))).Methods("GET") 208 r.HandleFunc("/spawn/distros", requireLogin(uis.loadCtx(uis.listSpawnableDistros))).Methods("GET") 209 r.HandleFunc("/spawn/keys", requireLogin(uis.loadCtx(uis.getUserPublicKeys))).Methods("GET") 210 211 // User settings 212 r.HandleFunc("/settings", requireLogin(uis.loadCtx(uis.userSettingsPage))).Methods("GET") 213 r.HandleFunc("/settings", requireLogin(uis.loadCtx(uis.userSettingsModify))).Methods("PUT") 214 r.HandleFunc("/settings/newkey", requireLogin(uis.loadCtx(uis.newAPIKey))).Methods("POST") 215 216 // Task stats 217 r.HandleFunc("/task_timing", requireLogin(uis.loadCtx(uis.taskTimingPage))).Methods("GET") 218 r.HandleFunc("/task_timing/{project_id}", requireLogin(uis.loadCtx(uis.taskTimingPage))).Methods("GET") 219 r.HandleFunc("/json/task_timing/{project_id}/{build_variant}/{request}/{task_name}", requireLogin(uis.loadCtx(uis.taskTimingJSON))).Methods("GET") 220 r.HandleFunc("/json/task_timing/{project_id}/{build_variant}/{request}", requireLogin(uis.loadCtx(uis.taskTimingJSON))).Methods("GET") 221 222 // Project routes 223 r.HandleFunc("/projects", requireLogin(uis.loadCtx(uis.projectsPage))).Methods("GET") 224 r.HandleFunc("/project/{project_id}", uis.loadCtx(uis.requireAdmin(uis.projectPage))).Methods("GET") 225 r.HandleFunc("/project/{project_id}", uis.loadCtx(uis.requireAdmin(uis.modifyProject))).Methods("POST") 226 r.HandleFunc("/project/{project_id}", uis.loadCtx(uis.requireAdmin(uis.addProject))).Methods("PUT") 227 r.HandleFunc("/project/{project_id}/repo_revision", uis.loadCtx(uis.requireAdmin(uis.setRevision))).Methods("PUT") 228 229 // REST API V1 230 AttachRESTHandler(r, uis) 231 232 // attaches /rest/v2 routes 233 route.AttachHandler(r, uis.Settings.SuperUsers, uis.Settings.Ui.Url, evergreen.RestRoutePrefix) 234 235 // Static Path handlers 236 r.PathPrefix("/clients").Handler(http.StripPrefix("/clients", http.FileServer(http.Dir(filepath.Join(uis.Home, evergreen.ClientDirectory))))) 237 238 // Plugin routes 239 rootPluginRouter := r.PathPrefix("/plugin/").Subrouter() 240 for _, pl := range plugin.UIPlugins { 241 242 // get the settings 243 pluginSettings := uis.Settings.Plugins[pl.Name()] 244 err := pl.Configure(pluginSettings) 245 if err != nil { 246 return nil, errors.Wrapf(err, "Failed to configure plugin %v", pl.Name()) 247 } 248 249 // check if a plugin is an app level plugin first 250 if appPlugin, ok := pl.(plugin.AppUIPlugin); ok { 251 // register the app level pa}rt of the plugin 252 appFunction := uis.GetPluginHandler(appPlugin.GetAppPluginInfo(), pl.Name()) 253 rootPluginRouter.HandleFunc(fmt.Sprintf("/%v/app", pl.Name()), uis.loadCtx(appFunction)) 254 } 255 256 // check if there are any errors getting the panel config 257 uiConf, err := pl.GetPanelConfig() 258 if err != nil { 259 panic(fmt.Sprintf("Error getting UI config for plugin %v: %v", pl.Name(), err)) 260 } 261 if uiConf == nil { 262 grip.Debugf("No UI config needed for plugin %s, skipping", pl.Name()) 263 continue 264 } 265 266 // create a root path for the plugin based on its name 267 plRouter := rootPluginRouter.PathPrefix(fmt.Sprintf("/%v/", pl.Name())).Subrouter() 268 269 // set up a fileserver in plugin's static root, if one is provided 270 pluginStaticPath := filepath.Join(uis.Home, "service", "plugins", pl.Name(), "static") 271 272 grip.Infof("Registering static path for plugin '%s' in %s", pl.Name(), pluginStaticPath) 273 plRouter.PathPrefix("/static/").Handler( 274 http.StripPrefix(fmt.Sprintf("/plugin/%v/static/", pl.Name()), 275 http.FileServer(http.Dir(pluginStaticPath))), 276 ) 277 pluginUIhandler := pl.GetUIHandler() 278 util.MountHandler(rootPluginRouter, fmt.Sprintf("/%v/", pl.Name()), withPluginUser(pluginUIhandler)) 279 } 280 281 return r, nil 282 } 283 284 // LoggedError logs the given error and writes an HTTP response with its details formatted 285 // as JSON if the request headers indicate that it's acceptable (or plaintext otherwise). 286 func (uis *UIServer) LoggedError(w http.ResponseWriter, r *http.Request, code int, err error) { 287 stack := debug.Stack() 288 grip.Errorln(r.Method, r.URL, err.Error(), string(stack)) 289 // if JSON is the preferred content type for the request, reply with a json message 290 if strings.HasPrefix(r.Header.Get("accept"), "application/json") { 291 uis.WriteJSON(w, code, struct { 292 Error string `json:"error"` 293 }{err.Error()}) 294 } else { 295 // Not a JSON request, so write plaintext. 296 http.Error(w, err.Error(), code) 297 } 298 }