Skip to content

Commit

Permalink
implement GET /v1/projects/:id/resources/:type/operations/pending
Browse files Browse the repository at this point in the history
  • Loading branch information
majewsky committed Jun 14, 2019
1 parent 2816af8 commit 74eb5fb
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 3 deletions.
4 changes: 4 additions & 0 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ func (h *handler) BuildRouter() http.Handler {
Path(`/v1/projects/{project_id}/assets/{asset_type}/{asset_uuid}`).
HandlerFunc(h.GetAsset)

router.Methods("GET").
Path(`/v1/projects/{project_id}/resources/{asset_type}/operations/pending`).
HandlerFunc(h.GetPendingOperationsForResource)

return router
}

Expand Down
10 changes: 7 additions & 3 deletions internal/api/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ type Asset struct {
//Operation is how a db.PendingOperation or db.FinishedOperation looks like in
//the API.
type Operation struct {
AssetID string `json:"asset_id,omitempty"`
//^ AssetID is left empty when Operation appears inside type Asset.
State db.OperationState `json:"state"`
Reason db.OperationReason `json:"reason"`
OldSize uint64 `json:"old_size"`
Expand Down Expand Up @@ -109,8 +111,9 @@ func AssetFromDB(asset db.Asset) Asset {
}

//PendingOperationFromDB converts a db.PendingOperation into an api.Operation.
func PendingOperationFromDB(dbOp db.PendingOperation) *Operation {
func PendingOperationFromDB(dbOp db.PendingOperation, assetID string) Operation {
op := Operation{
AssetID: assetID,
State: dbOp.State(),
Reason: dbOp.Reason,
OldSize: dbOp.OldSize,
Expand All @@ -132,7 +135,7 @@ func PendingOperationFromDB(dbOp db.PendingOperation) *Operation {
ByUserUUID: dbOp.GreenlitByUserUUID,
}
}
return &op
return op
}

//FinishedOperationFromDB converts a db.FinishedOperation into an api.Operation.
Expand Down Expand Up @@ -225,7 +228,8 @@ func (h handler) GetAsset(w http.ResponseWriter, r *http.Request) {
} else if respondwith.ErrorText(w, err) {
return
} else {
asset.PendingOperation = PendingOperationFromDB(dbPendingOp)
op := PendingOperationFromDB(dbPendingOp, "")
asset.PendingOperation = &op
}

_, wantsFinishedOps := r.URL.Query()["history"]
Expand Down
82 changes: 82 additions & 0 deletions internal/api/operations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/******************************************************************************
*
* Copyright 2019 SAP SE
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
******************************************************************************/

package api

import (
"net/http"

"github.com/sapcc/castellum/internal/db"
"github.com/sapcc/go-bits/respondwith"
)

func (h handler) GetPendingOperationsForResource(w http.ResponseWriter, r *http.Request) {
projectUUID, token := h.CheckToken(w, r)
if token == nil {
return
}
dbResource := h.LoadResource(w, r, projectUUID, token, false)
if dbResource == nil {
return
}

var ops []db.PendingOperation
_, err := h.DB.Select(&ops, `
SELECT o.* FROM pending_operations o
JOIN assets a ON a.id = o.asset_id
WHERE a.resource_id = $1
`, dbResource.ID)
if respondwith.ErrorText(w, err) {
return
}
assetUUIDs, err := h.getAssetUUIDMap(*dbResource)
if respondwith.ErrorText(w, err) {
return
}

//render response body
var response struct {
PendingOperations []Operation `json:"pending_operations,keepempty"`
}
response.PendingOperations = make([]Operation, len(ops))
for idx, op := range ops {
response.PendingOperations[idx] = PendingOperationFromDB(op, assetUUIDs[op.AssetID])
}
respondwith.JSON(w, http.StatusOK, response)
}

func (h handler) getAssetUUIDMap(res db.Resource) (map[int64]string, error) {
rows, err := h.DB.Query(`SELECT id, uuid FROM assets WHERE resource_id = $1`, res.ID)
if err != nil {
return nil, err
}

assetUUIDs := make(map[int64]string)
for rows.Next() {
var (
id int64
uuid string
)
err := rows.Scan(&id, &uuid)
if err != nil {
return nil, err
}
assetUUIDs[id] = uuid
}
return assetUUIDs, rows.Err()
}
128 changes: 128 additions & 0 deletions internal/api/operations_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/******************************************************************************
*
* Copyright 2019 SAP SE
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
******************************************************************************/

package api

import (
"net/http"
"testing"
"time"

"github.com/sapcc/castellum/internal/db"
"github.com/sapcc/castellum/internal/test"
"github.com/sapcc/go-bits/assert"
)

func TestGetPendingOperationsForResource(baseT *testing.T) {
t := test.T{T: baseT}
h, hh, validator, _, _ := setupTest(t)

//endpoint requires a token with project access
validator.Forbid("project:access")
assert.HTTPRequest{
Method: "GET",
Path: "/v1/projects/project1/resources/foo/operations/pending",
ExpectStatus: http.StatusForbidden,
}.Check(t.T, hh)
validator.Allow("project:access")

//expect error for unknown project or resource
assert.HTTPRequest{
Method: "GET",
Path: "/v1/projects/project2/resources/foo/operations/pending",
ExpectStatus: http.StatusNotFound,
}.Check(t.T, hh)
assert.HTTPRequest{
Method: "GET",
Path: "/v1/projects/project1/resources/doesnotexist/operations/pending",
ExpectStatus: http.StatusNotFound,
}.Check(t.T, hh)

//the "unknown" resource exists, but it should be 404 regardless because we
//don't have an asset manager for it
assert.HTTPRequest{
Method: "GET",
Path: "/v1/projects/project1/resources/unknown/operations/pending",
ExpectStatus: http.StatusNotFound,
}.Check(t.T, hh)

//expect error for inaccessible resource
validator.Forbid("project:show:foo")
assert.HTTPRequest{
Method: "GET",
Path: "/v1/projects/project1/resources/foo/operations/pending",
ExpectStatus: http.StatusForbidden,
}.Check(t.T, hh)
validator.Allow("project:show:foo")

//happy path: no pending operations
validator.Forbid("project:edit:foo") //this should not be an issue
response := []assert.JSONObject{}
req := assert.HTTPRequest{
Method: "GET",
Path: "/v1/projects/project1/resources/foo/operations/pending",
ExpectStatus: http.StatusOK,
ExpectBody: assert.JSONObject{"pending_operations": response},
}
req.Check(t.T, hh)

//check rendering of a pending operation in state "created"
pendingOp := db.PendingOperation{
AssetID: 1,
Reason: db.OperationReasonHigh,
OldSize: 1024,
NewSize: 2048,
UsagePercent: 60,
CreatedAt: time.Unix(21, 0).UTC(),
}
t.Must(h.DB.Insert(&pendingOp))
pendingOpJSON := assert.JSONObject{
"asset_id": "fooasset1",
"state": "created",
"reason": "high",
"old_size": 1024,
"new_size": 2048,
"created": assert.JSONObject{
"at": 21,
"usage_percent": 60,
},
}
req.ExpectBody = assert.JSONObject{
"pending_operations": []assert.JSONObject{pendingOpJSON},
}
req.Check(t.T, hh)

//check rendering of a pending operation in state "confirmed"
pendingOp.ConfirmedAt = p2time(time.Unix(22, 0).UTC())
t.MustUpdate(h.DB, &pendingOp)
pendingOpJSON["state"] = "confirmed"
pendingOpJSON["confirmed"] = assert.JSONObject{"at": 22}
req.Check(t.T, hh)

//check rendering of a pending operation in state "greenlit"
pendingOp.GreenlitAt = p2time(time.Unix(23, 0).UTC())
t.MustUpdate(h.DB, &pendingOp)
pendingOpJSON["state"] = "greenlit"
pendingOpJSON["greenlit"] = assert.JSONObject{"at": 23}
req.Check(t.T, hh)

pendingOp.GreenlitByUserUUID = p2string("user1")
t.MustUpdate(h.DB, &pendingOp)
pendingOpJSON["greenlit"] = assert.JSONObject{"at": 23, "by_user": "user1"}
req.Check(t.T, hh)
}

0 comments on commit 74eb5fb

Please sign in to comment.