-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(dashboards)!: server-side implementation of dashboards (#1028)
* init commit * split up org and name on GET return * add sender * admin validation abstraction * add build link * feat: adding new user changes to server (#1021) Co-authored-by: Claire.Nicholas <[email protected]> * some edits and list user + uuid usage * yep * sooooo many tests to add * add list build dashboard test and remove test json file * imports order and fix integration test * update swagger * make clean * fix spec * address feedback and fix more comments * address feedback and change admin set to use names * convert admin set to nested sanitized user list * fix a comment --------- Co-authored-by: claire1618 <[email protected]> Co-authored-by: Claire.Nicholas <[email protected]>
- Loading branch information
1 parent
52c741c
commit 66b3088
Showing
66 changed files
with
3,634 additions
and
66 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package dashboard | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"net/http" | ||
"strings" | ||
"time" | ||
|
||
"github.com/gin-gonic/gin" | ||
"github.com/sirupsen/logrus" | ||
|
||
"github.com/go-vela/server/api/types" | ||
"github.com/go-vela/server/database" | ||
"github.com/go-vela/server/router/middleware/user" | ||
"github.com/go-vela/server/util" | ||
) | ||
|
||
// swagger:operation POST /api/v1/dashboards dashboards CreateDashboard | ||
// | ||
// Create a dashboard in the configured backend | ||
// | ||
// --- | ||
// produces: | ||
// - application/json | ||
// parameters: | ||
// - in: body | ||
// name: body | ||
// description: Payload containing the dashboard to create | ||
// required: true | ||
// schema: | ||
// "$ref": "#/definitions/Dashboard" | ||
// security: | ||
// - ApiKeyAuth: [] | ||
// responses: | ||
// '201': | ||
// description: Successfully created dashboard | ||
// schema: | ||
// "$ref": "#/definitions/Dashboard" | ||
// '400': | ||
// description: Bad request when creating dashboard | ||
// schema: | ||
// "$ref": "#/definitions/Error" | ||
// '401': | ||
// description: Unauthorized to create dashboard | ||
// schema: | ||
// "$ref": "#/definitions/Error" | ||
// '500': | ||
// description: Server error when creating dashboard | ||
// schema: | ||
// "$ref": "#/definitions/Error" | ||
|
||
// CreateDashboard represents the API handler to | ||
// create a dashboard in the configured backend. | ||
func CreateDashboard(c *gin.Context) { | ||
// capture middleware values | ||
u := user.Retrieve(c) | ||
|
||
// capture body from API request | ||
input := new(types.Dashboard) | ||
|
||
err := c.Bind(input) | ||
if err != nil { | ||
retErr := fmt.Errorf("unable to decode JSON for new dashboard: %w", err) | ||
|
||
util.HandleError(c, http.StatusBadRequest, retErr) | ||
|
||
return | ||
} | ||
|
||
// ensure dashboard name is defined | ||
if input.GetName() == "" { | ||
util.HandleError(c, http.StatusBadRequest, fmt.Errorf("dashboard name must be set")) | ||
|
||
return | ||
} | ||
|
||
// update engine logger with API metadata | ||
// | ||
// https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields | ||
logrus.WithFields(logrus.Fields{ | ||
"user": u.GetName(), | ||
}).Infof("creating new dashboard %s", input.GetName()) | ||
|
||
d := new(types.Dashboard) | ||
|
||
// update fields in dashboard object | ||
d.SetCreatedBy(u.GetName()) | ||
d.SetName(input.GetName()) | ||
d.SetCreatedAt(time.Now().UTC().Unix()) | ||
d.SetUpdatedAt(time.Now().UTC().Unix()) | ||
d.SetUpdatedBy(u.GetName()) | ||
|
||
// validate admins to ensure they are all active users | ||
admins, err := createAdminSet(c, u, input.GetAdmins()) | ||
if err != nil { | ||
util.HandleError(c, http.StatusBadRequest, err) | ||
|
||
return | ||
} | ||
|
||
d.SetAdmins(admins) | ||
|
||
// validate repos to ensure they are all enabled | ||
err = validateRepoSet(c, input.GetRepos()) | ||
if err != nil { | ||
util.HandleError(c, http.StatusBadRequest, err) | ||
|
||
return | ||
} | ||
|
||
d.SetRepos(input.GetRepos()) | ||
|
||
// create dashboard in database | ||
d, err = database.FromContext(c).CreateDashboard(c, d) | ||
if err != nil { | ||
retErr := fmt.Errorf("unable to create new dashboard %s: %w", d.GetName(), err) | ||
|
||
util.HandleError(c, http.StatusInternalServerError, retErr) | ||
|
||
return | ||
} | ||
|
||
// add dashboard to claims' user's dashboard set | ||
u.SetDashboards(append(u.GetDashboards(), d.GetID())) | ||
|
||
// update user in database | ||
_, err = database.FromContext(c).UpdateUser(c, u) | ||
if err != nil { | ||
retErr := fmt.Errorf("unable to update user %s: %w", u.GetName(), err) | ||
|
||
util.HandleError(c, http.StatusInternalServerError, retErr) | ||
|
||
return | ||
} | ||
|
||
c.JSON(http.StatusCreated, d) | ||
} | ||
|
||
// createAdminSet takes a slice of users, cleanses it of duplicates and throws an error | ||
// when a user is inactive or not found in the database. It returns a sanitized slice of admins. | ||
func createAdminSet(c context.Context, caller *types.User, users []*types.User) ([]*types.User, error) { | ||
// add user creating the dashboard to admin list | ||
admins := []*types.User{caller.Sanitize()} | ||
|
||
dupMap := make(map[string]bool) | ||
|
||
// validate supplied admins are actual users | ||
for _, u := range users { | ||
if u.GetName() == caller.GetName() || dupMap[u.GetName()] { | ||
continue | ||
} | ||
|
||
dbUser, err := database.FromContext(c).GetUserForName(c, u.GetName()) | ||
if err != nil || !dbUser.GetActive() { | ||
return nil, fmt.Errorf("unable to create dashboard: %s is not an active user", u.GetName()) | ||
} | ||
|
||
admins = append(admins, dbUser.Sanitize()) | ||
|
||
dupMap[dbUser.GetName()] = true | ||
} | ||
|
||
return admins, nil | ||
} | ||
|
||
// validateRepoSet is a helper function that confirms all dashboard repos exist and are enabled | ||
// in the database while also confirming the IDs match when saving. | ||
func validateRepoSet(c context.Context, repos []*types.DashboardRepo) error { | ||
for _, repo := range repos { | ||
// verify format (org/repo) | ||
parts := strings.Split(repo.GetName(), "/") | ||
if len(parts) != 2 { | ||
return fmt.Errorf("unable to create dashboard: %s is not a valid repo", repo.GetName()) | ||
} | ||
|
||
// fetch repo from database | ||
dbRepo, err := database.FromContext(c).GetRepoForOrg(c, parts[0], parts[1]) | ||
if err != nil || !dbRepo.GetActive() { | ||
return fmt.Errorf("unable to create dashboard: could not get repo %s: %w", repo.GetName(), err) | ||
} | ||
|
||
// override ID field if provided to match the database ID | ||
repo.SetID(dbRepo.GetID()) | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package dashboard | ||
|
||
import ( | ||
"fmt" | ||
"net/http" | ||
|
||
"github.com/gin-gonic/gin" | ||
"github.com/sirupsen/logrus" | ||
|
||
"github.com/go-vela/server/api/types" | ||
"github.com/go-vela/server/database" | ||
"github.com/go-vela/server/router/middleware/dashboard" | ||
"github.com/go-vela/server/router/middleware/user" | ||
"github.com/go-vela/server/util" | ||
) | ||
|
||
// swagger:operation DELETE /api/v1/dashboards/{dashboard} dashboards DeleteDashboard | ||
// | ||
// Delete a dashboard in the configured backend | ||
// | ||
// --- | ||
// produces: | ||
// - application/json | ||
// parameters: | ||
// - in: path | ||
// name: dashboard | ||
// description: id of the dashboard | ||
// required: true | ||
// type: string | ||
// security: | ||
// - ApiKeyAuth: [] | ||
// responses: | ||
// '200': | ||
// description: Successfully deleted dashboard | ||
// schema: | ||
// type: string | ||
// '401': | ||
// description: Unauthorized to delete dashboard | ||
// schema: | ||
// "$ref": "#/definitions/Error" | ||
// '500': | ||
// description: Server error when deleting dashboard | ||
// schema: | ||
// "$ref": "#/definitions/Error" | ||
|
||
// DeleteDashboard represents the API handler to remove | ||
// a dashboard from the configured backend. | ||
func DeleteDashboard(c *gin.Context) { | ||
// capture middleware values | ||
d := dashboard.Retrieve(c) | ||
u := user.Retrieve(c) | ||
|
||
// update engine logger with API metadata | ||
// | ||
// https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields | ||
logrus.WithFields(logrus.Fields{ | ||
"dashboard": d.GetID(), | ||
"user": u.GetName(), | ||
}).Infof("deleting dashboard %s", d.GetID()) | ||
|
||
if !isAdmin(d, u) { | ||
retErr := fmt.Errorf("unable to delete dashboard %s: user is not an admin", d.GetID()) | ||
|
||
util.HandleError(c, http.StatusUnauthorized, retErr) | ||
|
||
return | ||
} | ||
|
||
err := database.FromContext(c).DeleteDashboard(c, d) | ||
if err != nil { | ||
retErr := fmt.Errorf("error while deleting dashboard %s: %w", d.GetID(), err) | ||
|
||
util.HandleError(c, http.StatusInternalServerError, retErr) | ||
|
||
return | ||
} | ||
|
||
c.JSON(http.StatusOK, fmt.Sprintf("dashboard %s deleted", d.GetName())) | ||
} | ||
|
||
// isAdmin is a helper function that iterates through the dashboard admins | ||
// and confirms if the user is in the slice. | ||
func isAdmin(d *types.Dashboard, u *types.User) bool { | ||
for _, admin := range d.GetAdmins() { | ||
if admin.GetID() == u.GetID() { | ||
return true | ||
} | ||
} | ||
|
||
return false | ||
} |
Oops, something went wrong.