storj/satellite/admin/project.go
Ivan Fraixedes fed09316b8
satellite/admin: Use helper for sending JSON
A previous commit added a helper function for sending JSON data back to
the client.

This commit makes use of it for homogenizing the current implementation.

It also renames the existing helper message to send JSON errors to
starts with "send" because the new helper starts with it and they
helpers are clearer with their name starting with it.

Change-Id: I53ee0b4ca33d677a8ccd366c9ba6d73f4f472247
2021-10-04 12:13:04 +02:00

527 lines
14 KiB
Go

// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package admin
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"time"
"github.com/gorilla/mux"
"github.com/gorilla/schema"
"storj.io/common/macaroon"
"storj.io/common/memory"
"storj.io/common/storj"
"storj.io/common/uuid"
"storj.io/storj/satellite/console"
"storj.io/storj/satellite/payments/stripecoinpayments"
)
func (server *Server) checkProjectUsage(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
projectUUIDString, ok := vars["project"]
if !ok {
sendJSONError(w, "project-uuid missing",
"", http.StatusBadRequest)
return
}
projectUUID, err := uuid.FromString(projectUUIDString)
if err != nil {
sendJSONError(w, "invalid project-uuid",
err.Error(), http.StatusBadRequest)
return
}
if !server.checkUsage(ctx, w, projectUUID) {
sendJSONData(w, http.StatusOK, []byte(`{"result":"no project usage exist"}`))
}
}
func (server *Server) getProject(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
projectUUIDString, ok := vars["project"]
if !ok {
sendJSONError(w, "project-uuid missing",
"", http.StatusBadRequest)
return
}
projectUUID, err := uuid.FromString(projectUUIDString)
if err != nil {
sendJSONError(w, "invalid project-uuid",
err.Error(), http.StatusBadRequest)
return
}
if err := r.ParseForm(); err != nil {
sendJSONError(w, "invalid form",
err.Error(), http.StatusBadRequest)
return
}
project, err := server.db.Console().Projects().Get(ctx, projectUUID)
if err != nil {
sendJSONError(w, "unable to fetch project details",
err.Error(), http.StatusInternalServerError)
return
}
data, err := json.Marshal(project)
if err != nil {
sendJSONError(w, "json encoding failed",
err.Error(), http.StatusInternalServerError)
return
}
sendJSONData(w, http.StatusOK, data)
}
func (server *Server) getProjectLimit(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
projectUUIDString, ok := vars["project"]
if !ok {
sendJSONError(w, "project-uuid missing",
"", http.StatusBadRequest)
return
}
projectUUID, err := uuid.FromString(projectUUIDString)
if err != nil {
sendJSONError(w, "invalid project-uuid",
err.Error(), http.StatusBadRequest)
return
}
project, err := server.db.Console().Projects().Get(ctx, projectUUID)
if err != nil {
sendJSONError(w, "failed to get project",
err.Error(), http.StatusInternalServerError)
return
}
var output struct {
Usage struct {
Amount memory.Size `json:"amount"`
Bytes int64 `json:"bytes"`
} `json:"usage"`
Bandwidth struct {
Amount memory.Size `json:"amount"`
Bytes int64 `json:"bytes"`
} `json:"bandwidth"`
Rate struct {
RPS int `json:"rps"`
} `json:"rate"`
Buckets int `json:"maxBuckets"`
}
if project.StorageLimit != nil {
output.Usage.Amount = *project.StorageLimit
output.Usage.Bytes = project.StorageLimit.Int64()
}
if project.BandwidthLimit != nil {
output.Bandwidth.Amount = *project.BandwidthLimit
output.Bandwidth.Bytes = project.BandwidthLimit.Int64()
}
if project.MaxBuckets != nil {
output.Buckets = *project.MaxBuckets
}
if project.RateLimit != nil {
output.Rate.RPS = *project.RateLimit
}
data, err := json.Marshal(output)
if err != nil {
sendJSONError(w, "json encoding failed",
err.Error(), http.StatusInternalServerError)
return
}
sendJSONData(w, http.StatusOK, data)
}
func (server *Server) putProjectLimit(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
projectUUIDString, ok := vars["project"]
if !ok {
sendJSONError(w, "project-uuid missing",
"", http.StatusBadRequest)
return
}
projectUUID, err := uuid.FromString(projectUUIDString)
if err != nil {
sendJSONError(w, "invalid project-uuid",
err.Error(), http.StatusBadRequest)
return
}
var arguments struct {
Usage *memory.Size `schema:"usage"`
Bandwidth *memory.Size `schema:"bandwidth"`
Rate *int `schema:"rate"`
Burst *int `schema:"burst"`
Buckets *int `schema:"buckets"`
}
if err := r.ParseForm(); err != nil {
sendJSONError(w, "invalid form",
err.Error(), http.StatusBadRequest)
return
}
decoder := schema.NewDecoder()
err = decoder.Decode(&arguments, r.Form)
if err != nil {
sendJSONError(w, "invalid arguments",
err.Error(), http.StatusBadRequest)
return
}
if arguments.Usage != nil {
if *arguments.Usage < 0 {
sendJSONError(w, "negative usage",
fmt.Sprintf("%v", arguments.Usage), http.StatusBadRequest)
return
}
err = server.db.ProjectAccounting().UpdateProjectUsageLimit(ctx, projectUUID, *arguments.Usage)
if err != nil {
sendJSONError(w, "failed to update usage",
err.Error(), http.StatusInternalServerError)
return
}
}
if arguments.Bandwidth != nil {
if *arguments.Bandwidth < 0 {
sendJSONError(w, "negative bandwidth",
fmt.Sprintf("%v", arguments.Usage), http.StatusBadRequest)
return
}
err = server.db.ProjectAccounting().UpdateProjectBandwidthLimit(ctx, projectUUID, *arguments.Bandwidth)
if err != nil {
sendJSONError(w, "failed to update bandwidth",
err.Error(), http.StatusInternalServerError)
return
}
}
if arguments.Rate != nil {
if *arguments.Rate < 0 {
sendJSONError(w, "negative rate",
fmt.Sprintf("%v", arguments.Rate), http.StatusBadRequest)
return
}
err = server.db.Console().Projects().UpdateRateLimit(ctx, projectUUID, *arguments.Rate)
if err != nil {
sendJSONError(w, "failed to update rate",
err.Error(), http.StatusInternalServerError)
return
}
}
if arguments.Burst != nil {
if *arguments.Burst < 0 {
sendJSONError(w, "negative burst rate",
fmt.Sprintf("%v", arguments.Burst), http.StatusBadRequest)
return
}
err = server.db.Console().Projects().UpdateBurstLimit(ctx, projectUUID, *arguments.Burst)
if err != nil {
sendJSONError(w, "failed to update burst",
err.Error(), http.StatusInternalServerError)
return
}
}
if arguments.Buckets != nil {
if *arguments.Buckets < 0 {
sendJSONError(w, "negative bucket coun",
fmt.Sprintf("t: %v", arguments.Buckets), http.StatusBadRequest)
return
}
err = server.db.Console().Projects().UpdateBucketLimit(ctx, projectUUID, *arguments.Buckets)
if err != nil {
sendJSONError(w, "failed to update bucket limit",
err.Error(), http.StatusInternalServerError)
return
}
}
}
func (server *Server) addProject(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
body, err := ioutil.ReadAll(r.Body)
if err != nil {
sendJSONError(w, "failed to read body",
err.Error(), http.StatusInternalServerError)
return
}
var input struct {
OwnerID uuid.UUID `json:"ownerId"`
ProjectName string `json:"projectName"`
}
var output struct {
ProjectID uuid.UUID `json:"projectId"`
}
err = json.Unmarshal(body, &input)
if err != nil {
sendJSONError(w, "failed to unmarshal request",
err.Error(), http.StatusBadRequest)
return
}
if input.OwnerID.IsZero() {
sendJSONError(w, "OwnerID is not set",
"", http.StatusBadRequest)
return
}
if input.ProjectName == "" {
sendJSONError(w, "ProjectName is not set",
"", http.StatusBadRequest)
return
}
project, err := server.db.Console().Projects().Insert(ctx, &console.Project{
Name: input.ProjectName,
OwnerID: input.OwnerID,
})
if err != nil {
sendJSONError(w, "failed to insert project",
err.Error(), http.StatusInternalServerError)
return
}
_, err = server.db.Console().ProjectMembers().Insert(ctx, project.OwnerID, project.ID)
if err != nil {
sendJSONError(w, "failed to insert project member",
err.Error(), http.StatusInternalServerError)
return
}
output.ProjectID = project.ID
data, err := json.Marshal(output)
if err != nil {
sendJSONError(w, "json encoding failed",
err.Error(), http.StatusInternalServerError)
return
}
sendJSONData(w, http.StatusOK, data)
}
func (server *Server) renameProject(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
projectUUIDString, ok := vars["project"]
if !ok {
sendJSONError(w, "project-uuid missing",
"", http.StatusBadRequest)
return
}
projectUUID, err := uuid.FromString(projectUUIDString)
if err != nil {
sendJSONError(w, "invalid project-uuid",
err.Error(), http.StatusBadRequest)
return
}
project, err := server.db.Console().Projects().Get(ctx, projectUUID)
if errors.Is(err, sql.ErrNoRows) {
sendJSONError(w, "project with specified uuid does not exist",
"", http.StatusBadRequest)
return
}
if err != nil {
sendJSONError(w, "error getting project",
err.Error(), http.StatusInternalServerError)
return
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
sendJSONError(w, "ailed to read body",
err.Error(), http.StatusInternalServerError)
return
}
var input struct {
ProjectName string `json:"projectName"`
Description string `json:"description"`
}
err = json.Unmarshal(body, &input)
if err != nil {
sendJSONError(w, "failed to unmarshal request",
err.Error(), http.StatusBadRequest)
return
}
if input.ProjectName == "" {
sendJSONError(w, "ProjectName is not set",
"", http.StatusBadRequest)
return
}
project.Name = input.ProjectName
if input.Description != "" {
project.Description = input.Description
}
err = server.db.Console().Projects().Update(ctx, project)
if err != nil {
sendJSONError(w, "error renaming project",
err.Error(), http.StatusInternalServerError)
return
}
}
func (server *Server) deleteProject(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
projectUUIDString, ok := vars["project"]
if !ok {
sendJSONError(w, "project-uuid missing",
"", http.StatusBadRequest)
return
}
projectUUID, err := uuid.FromString(projectUUIDString)
if err != nil {
sendJSONError(w, "invalid project-uuid",
err.Error(), http.StatusBadRequest)
return
}
if err := r.ParseForm(); err != nil {
sendJSONError(w, "invalid form",
err.Error(), http.StatusBadRequest)
return
}
options := storj.BucketListOptions{Limit: 1, Direction: storj.Forward}
buckets, err := server.db.Buckets().ListBuckets(ctx, projectUUID, options, macaroon.AllowedBuckets{All: true})
if err != nil {
sendJSONError(w, "unable to list buckets",
err.Error(), http.StatusInternalServerError)
return
}
if len(buckets.Items) > 0 {
sendJSONError(w, "buckets still exist",
fmt.Sprintf("%v", bucketNames(buckets.Items)), http.StatusConflict)
return
}
keys, err := server.db.Console().APIKeys().GetPagedByProjectID(ctx, projectUUID, console.APIKeyCursor{Limit: 1, Page: 1})
if err != nil {
sendJSONError(w, "unable to list api-keys",
err.Error(), http.StatusInternalServerError)
return
}
if keys.TotalCount > 0 {
sendJSONError(w, "api-keys still exist",
fmt.Sprintf("count %d", keys.TotalCount), http.StatusConflict)
return
}
// if usage exist, return error to client and exit
if server.checkUsage(ctx, w, projectUUID) {
return
}
err = server.db.Console().Projects().Delete(ctx, projectUUID)
if err != nil {
sendJSONError(w, "unable to delete project",
err.Error(), http.StatusInternalServerError)
return
}
}
func (server *Server) checkUsage(ctx context.Context, w http.ResponseWriter, projectID uuid.UUID) (hasUsage bool) {
// do not delete projects that have usage for the current month.
year, month, _ := server.nowFn().UTC().Date()
firstOfMonth := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC)
currentUsage, err := server.db.ProjectAccounting().GetProjectTotal(ctx, projectID, firstOfMonth, server.nowFn())
if err != nil {
sendJSONError(w, "unable to list project usage", err.Error(), http.StatusInternalServerError)
return true
}
if currentUsage.Storage > 0 || currentUsage.Egress > 0 || currentUsage.ObjectCount > 0 {
sendJSONError(w, "usage for current month exists", "", http.StatusConflict)
return true
}
// if usage of last month exist, make sure to look for billing records
lastMonthUsage, err := server.db.ProjectAccounting().GetProjectTotal(ctx, projectID, firstOfMonth.AddDate(0, -1, 0), firstOfMonth.AddDate(0, 0, -1))
if err != nil {
sendJSONError(w, "error getting project totals",
"", http.StatusInternalServerError)
return true
}
if lastMonthUsage.Storage > 0 || lastMonthUsage.Egress > 0 || lastMonthUsage.ObjectCount > 0 {
// time passed into the check function need to be the UTC midnight dates
// of the first day of the current month and the first day of the last
// month
err := server.db.StripeCoinPayments().ProjectRecords().Check(ctx, projectID, firstOfMonth.AddDate(0, -1, 0), firstOfMonth)
if errors.Is(err, stripecoinpayments.ErrProjectRecordExists) {
record, err := server.db.StripeCoinPayments().ProjectRecords().Get(ctx, projectID, firstOfMonth.AddDate(0, -1, 0), firstOfMonth)
if err != nil {
sendJSONError(w, "unable to get project records", err.Error(), http.StatusInternalServerError)
return true
}
// state = 0 means unapplied and not invoiced yet.
if record.State == 0 {
sendJSONError(w, "unapplied project invoice record exist", "", http.StatusConflict)
return true
}
// Record has been applied, so project can be deleted.
return false
}
if err != nil {
sendJSONError(w, "unable to get project records", err.Error(), http.StatusInternalServerError)
return true
}
sendJSONError(w, "usage for last month exist, but is not billed yet", "", http.StatusConflict)
return true
}
return false
}
func bucketNames(buckets []storj.Bucket) []string {
var xs []string
for _, b := range buckets {
xs = append(xs, b.Name)
}
return xs
}