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  }