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 17 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
185 changes: 185 additions & 0 deletions api/dashboard/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// 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 := 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
}
80 changes: 80 additions & 0 deletions api/dashboard/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// 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 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(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
}

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()))
}
132 changes: 132 additions & 0 deletions api/dashboard/get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// SPDX-License-Identifier: Apache-2.0

package dashboard

import (
"context"
"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 GET /api/v1/dashboards/{dashboard} dashboards GetDashboard
//
// Get a dashboard in the configured backend
//
// ---
// produces:
// - application/json
// parameters:
// - in: path
// name: dashboard
// description: Dashboard id to retrieve
// required: true
// type: string
// security:
// - ApiKeyAuth: []
// responses:
// '200':
// description: Successfully retrieved dashboard
// type: json
// schema:
// "$ref": "#/definitions/Dashboard"
// '401':
// description: Unauthorized to retrieve dashboard
// schema:
// "$ref": "#/definitions/Error"
// '500':
// description: Server error when retrieving dashboard
// schema:
// "$ref": "#/definitions/Error"

// GetDashboard represents the API handler to capture
// a dashboard for a repo from the configured backend.
func GetDashboard(c *gin.Context) {
// capture middleware values
d := dashboard.Retrieve(c)
u := user.Retrieve(c)

var err error

// 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("reading dashboard %s", d.GetID())

// initialize DashCard and set dashboard to the dashboard info pulled from database
dashboard := new(types.DashCard)
dashboard.Dashboard = d

// build RepoPartials referenced in the dashboard
dashboard.Repos, err = buildRepoPartials(c, d.Repos)
if err != nil {
util.HandleError(c, http.StatusInternalServerError, err)

return
}

c.JSON(http.StatusOK, dashboard)
}

// buildRepoPartials is a helper function which takes the dashboard repo list and builds
// a list of RepoPartials with information about the associated repository and its latest
// five builds.
func buildRepoPartials(c context.Context, repos []*types.DashboardRepo) ([]types.RepoPartial, error) {
var result []types.RepoPartial

for _, r := range repos {
repo := types.RepoPartial{}

// fetch repo from database
dbRepo, err := database.FromContext(c).GetRepo(c, r.GetID())
if err != nil {
return nil, fmt.Errorf("unable to get repo %s for dashboard: %w", r.GetName(), err)
}

// set values for RepoPartial
repo.Org = dbRepo.GetOrg()
repo.Name = dbRepo.GetName()
repo.Counter = dbRepo.GetCounter()

// list last 5 builds for repo given the branch and event filters
builds, err := database.FromContext(c).ListBuildsForDashboardRepo(c, dbRepo, r.GetBranches(), r.GetEvents())
if err != nil {
return nil, fmt.Errorf("unable to list builds for repo %s in dashboard: %w", dbRepo.GetFullName(), err)
}

bPartials := []types.BuildPartial{}

// populate BuildPartials with info from builds list
for _, build := range builds {
bPartial := types.BuildPartial{
Number: build.GetNumber(),
Status: build.GetStatus(),
Started: build.GetStarted(),
Finished: build.GetFinished(),
Sender: build.GetSender(),
Branch: build.GetBranch(),
Event: build.GetEvent(),
Link: build.GetLink(),
}

bPartials = append(bPartials, bPartial)
}

repo.Builds = bPartials

result = append(result, repo)
}

return result, nil
}
Loading
Loading