Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dashboards)!: server-side implementation of dashboards #1028

Merged
merged 23 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 193 additions & 0 deletions api/dashboard/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// 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 the dashboard
// schema:
// "$ref": "#/definitions/Dashboard"
// '400':
// description: Unable to create the dashboard
// schema:
// "$ref": "#/definitions/Error"
// '403':
// description: Unable to create the dashboard
// schema:
// "$ref": "#/definitions/Error"
// '409':
// description: Unable to create the dashboard
// schema:
// "$ref": "#/definitions/Error"
// '500':
// description: Unable to create the dashboard
// schema:
// "$ref": "#/definitions/Error"
// '503':
// description: Unable to create the 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 := validateAdminSet(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())

// send API call to create the dashboard
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()))

_, 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)
}

// validateAdminSet takes a slice of user names and converts it into a slice of matching
// user ids in order to preserve data integrity in case of name change.
func validateAdminSet(c context.Context, caller *types.User, users []string) ([]string, error) {
// add user creating the dashboard to admin list
admins := []string{fmt.Sprintf("%d", caller.GetID())}
ecrupper marked this conversation as resolved.
Show resolved Hide resolved

// validate supplied admins are actual users
for _, admin := range users {
if admin == caller.GetName() {
continue
}

u, err := database.FromContext(c).GetUserForName(c, admin)
if err != nil || !u.GetActive() {
return nil, fmt.Errorf("unable to create dashboard: %s is not an active user", admin)
}

admins = append(admins, fmt.Sprintf("%d", u.GetID()))
ecrupper marked this conversation as resolved.
Show resolved Hide resolved
}

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
}
81 changes: 81 additions & 0 deletions api/dashboard/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// 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/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 the dashboard
// schema:
// type: string
// '500':
// description: Unable to deleted the dashboard
// schema:
// "$ref": "#/definitions/Error"
// '510':
// description: Unable to deleted the 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(u.GetID(), d.GetAdmins()) {
retErr := fmt.Errorf("unable to delete dashboard %s: user is not an admin", d.GetName())

util.HandleError(c, http.StatusUnauthorized, retErr)

return
}

// Comment out actual delete until delete mechanism is fleshed out
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()))
}
Loading
Loading