Skip to content

Commit

Permalink
feat(dashboards)!: server-side implementation of dashboards (#1028)
Browse files Browse the repository at this point in the history
* 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 <Z00B3R3@w6nxmk4t6t.target.com>

* 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 <55173466+claire1618@users.noreply.github.com>
Co-authored-by: Claire.Nicholas <Z00B3R3@w6nxmk4t6t.target.com>
  • Loading branch information
3 people authored Apr 17, 2024
1 parent 52c741c commit 66b3088
Show file tree
Hide file tree
Showing 66 changed files with 3,634 additions and 66 deletions.
190 changes: 190 additions & 0 deletions api/dashboard/create.go
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
}
93 changes: 93 additions & 0 deletions api/dashboard/delete.go
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
}
Loading

0 comments on commit 66b3088

Please sign in to comment.