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

Bug/price list error #180

Open
wants to merge 28 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8daf59a
#SOS-15
Jackafive753 May 28, 2021
35a3f71
#SOS-22
Jackafive753 May 29, 2021
5866b6e
#SOS-15
Jackafive753 May 31, 2021
839ae2f
#SOS-23 Add getAccounts + save account information in event_status
daniebrill Jun 4, 2021
6cd0a85
#SOS-23 Use querylimit in aggregation + add error handling for type a…
daniebrill Jun 5, 2021
f8e931b
#SOS-23 Fix existing tests + code format
daniebrill Jun 8, 2021
259ccf7
#SOS-23 Modify GetAccounts for specific execution ID
daniebrill Jun 8, 2021
45db9b4
#SOS-23 Fit MockStorage to modified interface StorageDescriber
daniebrill Jun 8, 2021
2f3c15c
#SOS-23 Add tests in interpolation_test.go and server_test.go
daniebrill Jun 8, 2021
1fd641a
#SOS-20
Jackafive753 Jun 9, 2021
9c157f2
#SOS-23 Add test for elasticsearch GetAccounts
daniebrill Jun 9, 2021
0f68ad2
#SOS-16
Jackafive753 Jun 10, 2021
2b48f30
Merge remote-tracking branch 'origin/feature_multi_account_selection'…
Jackafive753 Jun 10, 2021
bf4c57d
#SOS18 Add multiaccount selection functionality to tables
daniebrill Jun 11, 2021
ecbe7ef
#SOS-16
Jackafive753 Jun 11, 2021
5a7065e
Merge remote-tracking branch 'origin/feature_multi_account_selection'…
Jackafive753 Jun 11, 2021
5ea12e5
#SOS-18
Jackafive753 Jun 14, 2021
2f99548
#SOS-18
daniebrill Jun 14, 2021
2c3e562
Merge pull request #1 from evoila/feature_multi_account_selection
Jackafive753 Jun 16, 2021
6ee0db8
bugfix: Kinesis priceList error
Jackafive753 Jun 30, 2021
c6b916e
bugfix: kinesis priceList Error
Jackafive753 Jun 30, 2021
9242b0a
bugfix: kinesis priceList error
Jackafive753 Jun 30, 2021
db579e7
bugfix: kinesis priceList error
Jackafive753 Jul 1, 2021
604e28a
bugfix: kinesis priceList Error
Jackafive753 Jul 1, 2021
4df494c
fix warnings: unused statements
Jackafive753 Jul 2, 2021
ab6405e
# SOS-57
Jackafive753 Jul 23, 2021
f8adc70
set account to filters if an account specific Chart is clicked
Jackafive753 Jul 28, 2021
225ef61
Merge pull request #3 from daniebrill/feature/multi_account_selection
daniebrill Aug 2, 2021
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
14 changes: 14 additions & 0 deletions api/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,20 @@ func (server *Server) GetExecutions(resp http.ResponseWriter, req *http.Request)
server.JSONWrite(resp, http.StatusOK, results)
}

func (server *Server) GetAccounts(resp http.ResponseWriter, req *http.Request) {
queryLimit, _ := strconv.Atoi(httpparameters.QueryParamWithDefault(req, "querylimit", storage.GetExecutionsQueryLimit))
params := mux.Vars(req)
executionID := params["executionID"]
accounts, err := server.storage.GetAccounts(executionID, queryLimit)

if err != nil {
server.JSONWrite(resp, http.StatusInternalServerError, HttpErrorResponse{Error: err.Error()})
return

}
server.JSONWrite(resp, http.StatusOK, accounts)
}

// GetResourceData return resuts details by resource type
func (server *Server) GetResourceData(resp http.ResponseWriter, req *http.Request) {
queryParams := req.URL.Query()
Expand Down
1 change: 1 addition & 0 deletions api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ func (server *Server) BindEndpoints() {

server.router.HandleFunc("/api/v1/summary/{executionID}", server.GetSummary).Methods("GET")
server.router.HandleFunc("/api/v1/executions", server.GetExecutions).Methods("GET")
server.router.HandleFunc("/api/v1/accounts/{executionID}", server.GetAccounts).Methods(("GET"))
server.router.HandleFunc("/api/v1/resources/{type}", server.GetResourceData).Methods("GET")
server.router.HandleFunc("/api/v1/trends/{type}", server.GetResourceTrends).Methods("GET")
server.router.HandleFunc("/api/v1/tags/{executionID}", server.GetExecutionTags).Methods("GET")
Expand Down
49 changes: 49 additions & 0 deletions api/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,57 @@ func TestGetExecutions(t *testing.T) {

})
}
}

func TestGetAccounts(t *testing.T) {
ms, _ := MockServer()
ms.BindEndpoints()
ms.Serve()

testCases := []struct {
endpoint string
expectedStatusCode int
Count int
}{
{"/api/v1/accounts", http.StatusNotFound, 0},
{"/api/v1/accounts/1", http.StatusOK, 2},
{"/api/v1/accounts/err", http.StatusInternalServerError, 0},
}

for _, test := range testCases {
t.Run(test.endpoint, func(t *testing.T) {

rr := httptest.NewRecorder()
req, err := http.NewRequest("GET", test.endpoint, nil)
if err != nil {
t.Fatal(err)
}
ms.Router().ServeHTTP(rr, req)
if rr.Code != test.expectedStatusCode {
t.Fatalf("handler returned wrong status code: got %v want %v", rr.Code, test.expectedStatusCode)
}

if test.expectedStatusCode == http.StatusOK {
body, err := ioutil.ReadAll(rr.Body)
if err != nil {
t.Fatal(err)
}

var accountsData []storage.Accounts

err = json.Unmarshal(body, &accountsData)
if err != nil {
t.Fatalf("Could not parse http response")
}

if len(accountsData) != test.Count {
t.Fatalf("unexpected accounts data response, got %d expected %d", len(accountsData), test.Count)
}
}
})
}
}

func TestSave(t *testing.T) {
ms, mockStorage := MockServer()
ms.BindEndpoints()
Expand Down
110 changes: 84 additions & 26 deletions api/storage/elasticsearch/elasticsearch.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"fmt"
"reflect"
"strconv"
"strings"
"time"

elastic "github.com/olivere/elastic/v7"
Expand Down Expand Up @@ -110,14 +111,24 @@ func (sm *StorageManager) getDynamicMatchQuery(filters map[string]string, operat
dynamicMatchQuery := []elastic.Query{}
var mq *elastic.MatchQuery
for name, value := range filters {
mq = elastic.NewMatchQuery(name, value)
// Minimum number of clauses that must match for a document to be returned
mq.MinimumShouldMatch("100%")
if operator == "and" {
mq = mq.Operator("and")
if name == "Data.AccountID" {
var accountIds = strings.Split(value, ",")
var accountBoolQuery = elastic.NewBoolQuery()
for _, accountId := range accountIds {
accountBoolQuery.Should(elastic.NewMatchQuery(name, accountId))
}
accountBoolQuery.MinimumShouldMatch("1")
dynamicMatchQuery = append(dynamicMatchQuery, accountBoolQuery)
} else {
mq = elastic.NewMatchQuery(name, value)
// Minimum number of clauses that must match for a document to be returned
mq.MinimumShouldMatch("100%")
if operator == "and" {
mq = mq.Operator("and")
}
log.Info("Query ", mq)
dynamicMatchQuery = append(dynamicMatchQuery, mq)
}

dynamicMatchQuery = append(dynamicMatchQuery, mq)
}
return dynamicMatchQuery
}
Expand Down Expand Up @@ -183,60 +194,69 @@ func (sm *StorageManager) GetSummary(executionID string, filters map[string]stri
for resourceName, resourceData := range summary {
filters["ResourceName"] = resourceName
log.WithField("filters", filters).Debug("Going to get resources summary details with the following filters")
totalSpent, resourceCount, err := sm.getResourceSummaryDetails(executionID, filters)
totalSpent, resourceCount, spentAccounts, err := sm.getResourceSummaryDetails(executionID, filters)

if err != nil {
continue
}
newResourceData := resourceData
newResourceData.TotalSpent = totalSpent
newResourceData.ResourceCount = resourceCount
newResourceData.SpentAccounts = spentAccounts
summary[resourceName] = newResourceData

}

return summary, nil

}

// getResourceSummaryDetails returns total resource spent and total resources detected
func (sm *StorageManager) getResourceSummaryDetails(executionID string, filters map[string]string) (float64, int64, error) {
func (sm *StorageManager) getResourceSummaryDetails(executionID string, filters map[string]string) (float64, int64, map[string]float64, error) {

var totalSpent float64
var resourceCount int64
var spentAccounts = make(map[string]float64)

dynamicMatchQuery := sm.getDynamicMatchQuery(filters, "or")
dynamicMatchQuery = append(dynamicMatchQuery, elastic.NewTermQuery("ExecutionID", executionID))
dynamicMatchQuery = append(dynamicMatchQuery, elastic.NewTermQuery("EventType", "resource_detected"))

searchResult, err := sm.client.Search().
searchResultAccount, err := sm.client.Search().
Query(elastic.NewBoolQuery().Must(dynamicMatchQuery...)).
Aggregation("sum", elastic.NewSumAggregation().Field("Data.PricePerMonth")).
Aggregation("accounts", elastic.NewTermsAggregation().Field("Data.AccountID.keyword").
SubAggregation("accountSum", elastic.NewSumAggregation().Field("Data.PricePerMonth"))).
Size(0).Do(context.Background())

if err != nil {
log.WithError(err).WithFields(log.Fields{
"filters": filters,
}).Error("error when trying to get summary details")
log.WithError(err).Error("error when trying to get executions collectors")
return totalSpent, resourceCount, spentAccounts, ErrInvalidQuery
}

return totalSpent, resourceCount, err
respAccount, ok := searchResultAccount.Aggregations.Terms("accounts")
if !ok {
log.Error("accounts field term does not exist")
return totalSpent, resourceCount, spentAccounts, ErrAggregationTermNotFound
}

log.WithFields(log.Fields{
"filters": filters,
"milliseconds": searchResult.TookInMillis,
}).Debug("get execution details")
for _, AccountIdBucket := range respAccount.Buckets {

resp, ok := searchResult.Aggregations.Terms("sum")
if ok {
if val, ok := resp.Aggregations["value"]; ok {
spent, ok := AccountIdBucket.Aggregations.Terms("accountSum")
if ok {
if val, ok := spent.Aggregations["value"]; ok {
accountID, ok := AccountIdBucket.Key.(string)
if !ok {
log.Error("type assertion to string failed")
continue
}
spentAccounts[accountID], _ = strconv.ParseFloat(string(val), 64)

totalSpent, _ = strconv.ParseFloat(string(val), 64)
resourceCount = searchResult.Hits.TotalHits.Value
totalSpent += spentAccounts[accountID]
resourceCount += AccountIdBucket.DocCount
}
}
}

return totalSpent, resourceCount, nil
return totalSpent, resourceCount, spentAccounts, nil
}

// GetExecutions returns collector executions
Expand Down Expand Up @@ -298,6 +318,44 @@ func (sm *StorageManager) GetExecutions(queryLimit int) ([]storage.Executions, e
return executions, nil
}

func (sm *StorageManager) GetAccounts(executionID string, querylimit int) ([]storage.Accounts, error) {
accounts := []storage.Accounts{}

searchResult, err := sm.client.Search().Query(elastic.NewMatchQuery("ExecutionID", executionID)).
Aggregation("Accounts", elastic.NewTermsAggregation().
Field("Data.AccountInformation.keyword").Size(querylimit)).
Do(context.Background())

if err != nil {
log.WithError(err).Error("error when trying to get AccountIDs")
return accounts, ErrInvalidQuery
}

resp, ok := searchResult.Aggregations.Terms("Accounts")
if !ok {
log.Error("accounts field term does not exist")
return accounts, ErrAggregationTermNotFound
}

for _, accountsBucket := range resp.Buckets {
account, ok := accountsBucket.Key.(string)
if !ok {
log.Error("type assertion to string failed")
continue
}
name, id, err := interpolation.ExtractAccountInformation(account)
if err != nil {
log.WithError(err).WithField("account", account).Error("could not extract account information")
continue
}
accounts = append(accounts, storage.Accounts{
ID: id,
Name: name,
})
}
return accounts, nil
}

// GetResources return resource data
func (sm *StorageManager) GetResources(resourceType string, executionID string, filters map[string]string) ([]map[string]interface{}, error) {

Expand Down
65 changes: 64 additions & 1 deletion api/storage/elasticsearch/elasticsearch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,64 @@ func TestGetExecutions(t *testing.T) {
}
}

func TestGetAccounts(t *testing.T) {

// each different queryLimit will result in a different elasticsearch response.
// 1 - returns valid account data response
// 2 - returns invalid aggregation term query response
// 4 - returns invalid statuscode response
testCases := []struct {
name string
queryLimit int
responseCount int
ErrorMessage error
}{
{"valid response", 1, 2, nil},
{"invalid terms", 2, 0, ErrAggregationTermNotFound},
{"invalid es response", 3, 0, ErrInvalidQuery},
}

mockClient, config := testutils.NewESMock(prefixIndexName, true)

mockClient.Router.HandleFunc("/_search", func(resp http.ResponseWriter, req *http.Request) {
switch testutils.GetPostParams(req) {
case `{"aggregations":{"Accounts":{"terms":{"field":"Data.AccountInformation.keyword","size":1}}},"query":{"match":{"ExecutionID":{"query":"1"}}}}`:
testutils.JSONResponse(resp, http.StatusOK, elastic.SearchResult{Aggregations: map[string]json.RawMessage{
"Accounts": testutils.LoadResponse("accounts/aggregations/default"),
}})
case `{"aggregations":{"Accounts":{"terms":{"field":"Data.AccountInformation.keyword","size":2}}},"query":{"match":{"ExecutionID":{"query":"1"}}}}`:
testutils.JSONResponse(resp, http.StatusOK, elastic.SearchResult{Aggregations: map[string]json.RawMessage{
"invalid-key": testutils.LoadResponse("accounts/aggregations/default"),
}})
case `{"aggregations":{"Accounts":{"terms":{"field":"Data.AccountInformation.keyword","size":3}}},"query":{"match":{"ExecutionID":{"query":"1"}}}}`:
testutils.JSONResponse(resp, http.StatusBadRequest, elastic.SearchResult{Aggregations: map[string]json.RawMessage{}})
default:
t.Fatalf("unexpected request params")
}
})

es, err := NewStorageManager(config)
if err != nil {
t.Fatalf("unexpected error, got %v expected nil", err)
}

for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {

response, err := es.GetAccounts("1", test.queryLimit)

if err != test.ErrorMessage {
t.Fatalf("unexpected error, got %v expected %v", err, test.ErrorMessage)
}

if len(response) != test.responseCount {
t.Fatalf("handler query response: got %d want %d", len(response), test.responseCount)
}

})
}
}

func TestGetSummary(t *testing.T) {

mockClient, config := testutils.NewESMock(prefixIndexName, true)
Expand All @@ -225,7 +283,9 @@ func TestGetSummary(t *testing.T) {

response := elastic.SearchResult{}

switch testutils.GetPostParams(req) {
var s = testutils.GetPostParams(req)
fmt.Println(s)
switch s {
case `{"query":{"bool":{"must":[{"term":{"EventType":"service_status"}},{"term":{"ExecutionID":""}}]}},"size":0}`:
response.Hits = &elastic.SearchHits{TotalHits: &elastic.TotalHits{Value: 1}}
case `{"query":{"bool":{"must":[{"term":{"EventType":"service_status"}},{"term":{"ExecutionID":""}}]}},"size":1}`:
Expand All @@ -238,6 +298,9 @@ func TestGetSummary(t *testing.T) {
case `{"aggregations":{"sum":{"sum":{"field":"Data.PricePerMonth"}}},"query":{"bool":{"must":[{"match":{"ResourceName":{"minimum_should_match":"100%","query":"aws_resource_name"}}},{"term":{"ExecutionID":""}},{"term":{"EventType":"resource_detected"}}]}},"size":0}`:
response.Aggregations = map[string]json.RawMessage{"sum": []byte(`{"value": 36.5}`)}
response.Hits = &elastic.SearchHits{TotalHits: &elastic.TotalHits{Value: 1}}
case `{"aggregations":{"accounts":{"aggregations":{"accountSum":{"sum":{"field":"Data.PricePerMonth"}}},"terms":{"field":"Data.AccountID.keyword"}}},"query":{"bool":{"must":[{"match":{"ResourceName":{"minimum_should_match":"100%","query":"aws_resource_name"}}},{"term":{"ExecutionID":""}},{"term":{"EventType":"resource_detected"}}]}},"size":0}`:
response.Aggregations = map[string]json.RawMessage{"accounts": testutils.LoadResponse("summary/aggregations/default")}
response.Hits = &elastic.SearchHits{TotalHits: &elastic.TotalHits{Value: 1}}
default:
t.Fatalf("unexpected request params")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"buckets" : [
{
"key" : "Test_123456789",
"doc_count" : 66
},
{
"key" : "Test2_12345675",
"doc_count" : 6
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"buckets": [
{
"key": "123456789012",
"doc_count": 1,
"accountSum": {
"value": 36.5
}
}
]
}
Loading