Skip to content

Commit

Permalink
Merge pull request #1289 from Bee-lee/get-rule-recommendations
Browse files Browse the repository at this point in the history
add aggregator part for /rule/ recommendations list
  • Loading branch information
Bee-lee authored Oct 8, 2021
2 parents 9406060 + 76f9389 commit 445c3a5
Show file tree
Hide file tree
Showing 9 changed files with 465 additions and 41 deletions.
5 changes: 2 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@ require (
github.com/BurntSushi/toml v0.3.1
github.com/DATA-DOG/go-sqlmock v1.4.1
github.com/RedHatInsights/insights-content-service v0.0.0-20201009081018-083923779f00
github.com/RedHatInsights/insights-operator-utils v1.19.1
github.com/RedHatInsights/insights-results-aggregator-data v1.1.0
github.com/RedHatInsights/insights-operator-utils v1.21.0
github.com/RedHatInsights/insights-results-aggregator-data v1.3.0
github.com/Shopify/sarama v1.27.1
github.com/deckarep/golang-set v1.7.1
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/fzipp/gocyclo v0.3.1 // indirect
github.com/gchaincl/sqlhooks v1.3.0
github.com/google/uuid v1.1.2
github.com/gorilla/mux v1.8.0
Expand Down
35 changes: 5 additions & 30 deletions go.sum

Large diffs are not rendered by default.

75 changes: 75 additions & 0 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1181,6 +1181,81 @@
]
}
},
"/recommendations/organizations/{org_id}/users/{user_id}/list": {
"post": {
"summary": "Returns a list of recommendations and a number of clusters each one is hitting.",
"operationId": "getRecommendationsPost",
"description": "Recommendations will be retrieved based on the list of cluster IDs that is part of request body.",
"parameters": [
{
"name": "org_id",
"in": "path",
"description": "Organization ID represented as positive integer",
"required": true,
"schema": {
"type": "integer",
"format": "int64",
"minimum": 0
}
},
{
"name": "user_id",
"in": "path",
"required": true,
"description": "Numeric ID of the user. An example: `42`",
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "List of cluster IDs. An example: `34c3ecc5-624a-49a5-bab8-4fdc5e51a266.",
"required": true,
"content": {
"application/json": {
"schema": {
}
}
}
},
"responses": {
"200": {
"description": "List of recommendations and a number of clusters each one is hitting",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"recommendations": {
"type": "array",
"items": {
"type": "object",
"properties": {
"rule_id": {
"type": "string",
"description": "The rule ID in the | format.",
"example": "rule.module|ERROR_KEY"
},
"impacted_clusters_cnt": {
"type": "int",
"description": "The number of clusters impacted by this rule.",
"example": 42
}
}
}
},
"status": {
"type": "string",
"example": "ok"
}
}
}
}
}
}
}
}
},
"/rules/users/{userId}/disabled": {
"get": {
"summary": "Returns a list of rules disabled from current account",
Expand Down
30 changes: 24 additions & 6 deletions server/endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ const (
// ListOfDisabledRulesSystemWide returns a list of rules disabled from current account
ListOfDisabledRulesSystemWide = "rules/organizations/{org_id}/users/{user_id}/disabled_system_wide"

// RecommendationsListEndpoint receives a list of clusters in POST body and returns a list of all recommendations hitting for them
RecommendationsListEndpoint = "recommendations/organizations/{org_id}/users/{user_id}/list"

// Rating accepts a list of ratings in the request body and store them in the database for the given user
Rating = "rules/organizations/{org_id}/users/{user_id}/rating"
// MetricsEndpoint returns prometheus metrics
Expand Down Expand Up @@ -121,12 +124,11 @@ func (server *HTTPServer) addEndpointsToRouter(router *mux.Router) {
router.HandleFunc(apiPrefix+ListOfDisabledRulesFeedback, server.listOfReasons).Methods(http.MethodGet)
router.HandleFunc(apiPrefix+Rating, server.setRuleRating).Methods(http.MethodPost)

// Endpoints to handle rules to be enabled, disabled, updated, and queried system-wide
router.HandleFunc(apiPrefix+EnableRuleSystemWide, server.enableRuleSystemWide).Methods(http.MethodPut, http.MethodOptions)
router.HandleFunc(apiPrefix+DisableRuleSystemWide, server.disableRuleSystemWide).Methods(http.MethodPut, http.MethodOptions)
router.HandleFunc(apiPrefix+UpdateRuleSystemWide, server.updateRuleSystemWide).Methods(http.MethodPost, http.MethodOptions)
router.HandleFunc(apiPrefix+ReadRuleSystemWide, server.readRuleSystemWide).Methods(http.MethodGet)
router.HandleFunc(apiPrefix+ListOfDisabledRulesSystemWide, server.listOfDisabledRulesSystemWide).Methods(http.MethodGet)
// Rule Enable/Disable/etc endpoints
server.addRuleEnableDisableEndpointsToRouter(router, apiPrefix)

// Endpoints related to the Insights Advisor application
server.addInsightsAdvisorEndpointsToRouter(router, apiPrefix)

// Prometheus metrics
router.Handle(apiPrefix+MetricsEndpoint, promhttp.Handler()).Methods(http.MethodGet)
Expand All @@ -137,3 +139,19 @@ func (server *HTTPServer) addEndpointsToRouter(router *mux.Router) {
httputils.CreateOpenAPIHandler(server.Config.APISpecFile, server.Config.Debug, true),
).Methods(http.MethodGet)
}

// addRuleEnableDisableEndpointsToRouter method registers handlers for endpoints that
// allow for rules to be enabled, disabled, updated, and queried system-wide
func (server *HTTPServer) addRuleEnableDisableEndpointsToRouter(router *mux.Router, apiPrefix string) {
router.HandleFunc(apiPrefix+EnableRuleSystemWide, server.enableRuleSystemWide).Methods(http.MethodPut, http.MethodOptions)
router.HandleFunc(apiPrefix+DisableRuleSystemWide, server.disableRuleSystemWide).Methods(http.MethodPut, http.MethodOptions)
router.HandleFunc(apiPrefix+UpdateRuleSystemWide, server.updateRuleSystemWide).Methods(http.MethodPost, http.MethodOptions)
router.HandleFunc(apiPrefix+ReadRuleSystemWide, server.readRuleSystemWide).Methods(http.MethodGet)
router.HandleFunc(apiPrefix+ListOfDisabledRulesSystemWide, server.listOfDisabledRulesSystemWide).Methods(http.MethodGet)
}

// addRuleEnableDisableEndpointsToRouter method registers handlers for endpoints that
// are related to the Insights Advisor application
func (server *HTTPServer) addInsightsAdvisorEndpointsToRouter(router *mux.Router, apiPrefix string) {
router.HandleFunc(apiPrefix+RecommendationsListEndpoint, server.getRecommendations).Methods(http.MethodPost, http.MethodOptions)
}
46 changes: 44 additions & 2 deletions server/reports.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ const (

// OkStatusPayload is the text returned as body payload when an OK Status request is sent
OkStatusPayload = "ok"

// orgIDStr used in log messages
orgIDStr = "orgID"
)

// validateClusterID function checks if the cluster ID is a valid UUID.
Expand Down Expand Up @@ -200,7 +203,7 @@ func (server *HTTPServer) reportForListOfClusters(writer http.ResponseWriter, re
// wrong state has been handled already
return
}
log.Info().Int("orgID", int(orgID)).Msg("reportForListOfClusters")
log.Info().Int(orgIDStr, int(orgID)).Msg("reportForListOfClusters")

// try to read list of cluster IDs
listOfClusters, successful := readClusterListFromPath(writer, request)
Expand All @@ -224,7 +227,7 @@ func (server *HTTPServer) reportForListOfClustersPayload(writer http.ResponseWri
// wrong state has been handled already
return
}
log.Info().Int("orgID", int(orgID)).Msg("reportForListOfClustersPayload")
log.Info().Int(orgIDStr, int(orgID)).Msg("reportForListOfClustersPayload")

// try to read list of cluster IDs
listOfClusters, successful := readClusterListFromBody(writer, request)
Expand All @@ -236,3 +239,42 @@ func (server *HTTPServer) reportForListOfClustersPayload(writer http.ResponseWri
// we were able to read the cluster IDs, let's process them
processListOfClusters(server, writer, request, orgID, listOfClusters)
}

// getRecommendations retrieves all recommendations hitting for all clusters in the org
func (server *HTTPServer) getRecommendations(writer http.ResponseWriter, request *http.Request) {
// Extract user_id from URL
userID, ok := readUserID(writer, request)
if !ok {
// everything has been handled
return
}
log.Info().Str("userID", string(userID)).Msg("getRecommendations")

// extract org_id from URL
orgID, ok := readOrgID(writer, request)
if !ok {
// everything has been handled
return
}
log.Info().Int(orgIDStr, int(orgID)).Msg("getRecommendations")

var listOfClusters []types.ClusterName
err := json.NewDecoder(request.Body).Decode(&listOfClusters)
if err != nil {
handleServerError(writer, err)
return
}
log.Info().Msgf("getRecommendations list of clusters: %v", listOfClusters)

recommendations, err := server.Storage.ReadRecommendationsForClusters(listOfClusters)
if err != nil {
log.Error().Err(err).Msg("Errors retrieving recommendations")
handleServerError(writer, err)
return
}

err = responses.SendOK(writer, responses.BuildOkResponseWithData("recommendations", recommendations))
if err != nil {
log.Error().Err(err).Msg(responseDataError)
}
}
116 changes: 116 additions & 0 deletions server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"bytes"
"context"
"database/sql"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
Expand Down Expand Up @@ -1099,3 +1100,118 @@ func TestHTTPServer_ListOfDisabledRulesSystemWide(t *testing.T) {
Body: `{"disabledRules":[],"status":"ok"}`,
})
}

func TestHTTPServer_RecommendationsListEndpoint_NoRecommendations(t *testing.T) {
mockStorage, closer := helpers.MustGetMockStorage(t, true)
defer closer()

err := mockStorage.WriteRecommendationsForCluster(
testdata.OrgID, testdata.ClusterName, testdata.Report0Rules,
)
helpers.FailOnError(t, err)

clusterList := []types.ClusterName{testdata.GetRandomClusterID()}
reqBody, _ := json.Marshal(clusterList)

helpers.AssertAPIRequest(t, mockStorage, nil, &helpers.APIRequest{
Method: http.MethodPost,
Endpoint: server.RecommendationsListEndpoint,
EndpointArgs: []interface{}{testdata.OrgID, testdata.UserID},
Body: reqBody,
}, &helpers.APIResponse{
StatusCode: http.StatusOK,
Body: `{"recommendations":{},"status":"ok"}`,
})
}

func TestHTTPServer_RecommendationsListEndpoint_DifferentClusters(t *testing.T) {
mockStorage, closer := helpers.MustGetMockStorage(t, true)
defer closer()

err := mockStorage.WriteRecommendationsForCluster(
testdata.OrgID, testdata.ClusterName, testdata.Report3Rules,
)
helpers.FailOnError(t, err)

clusterList := []types.ClusterName{testdata.GetRandomClusterID(), testdata.GetRandomClusterID()}
reqBody, _ := json.Marshal(clusterList)

helpers.AssertAPIRequest(t, mockStorage, nil, &helpers.APIRequest{
Method: http.MethodPost,
Endpoint: server.RecommendationsListEndpoint,
EndpointArgs: []interface{}{testdata.OrgID, testdata.UserID},
Body: reqBody,
}, &helpers.APIResponse{
StatusCode: http.StatusOK,
Body: `{"recommendations":{},"status":"ok"}`,
})
}

func TestHTTPServer_RecommendationsListEndpoint_3Recs1Cluster(t *testing.T) {
mockStorage, closer := helpers.MustGetMockStorage(t, true)
defer closer()

err := mockStorage.WriteRecommendationsForCluster(
testdata.OrgID, testdata.ClusterName, testdata.Report3Rules,
)
helpers.FailOnError(t, err)

clusterList := []types.ClusterName{testdata.ClusterName}
reqBody, _ := json.Marshal(clusterList)

respBody := `{"recommendations":{"%v":%v,"%v":%v,"%v":%v},"status":"ok"}`
respBody = fmt.Sprintf(respBody,
testdata.Rule1CompositeID, 1,
testdata.Rule2CompositeID, 1,
testdata.Rule3CompositeID, 1,
)

helpers.AssertAPIRequest(t, mockStorage, nil, &helpers.APIRequest{
Method: http.MethodPost,
Endpoint: server.RecommendationsListEndpoint,
EndpointArgs: []interface{}{testdata.OrgID, testdata.UserID},
Body: reqBody,
}, &helpers.APIResponse{
StatusCode: http.StatusOK,
Body: respBody,
})
}

func TestHTTPServer_RecommendationsListEndpoint_3Recs2Clusters(t *testing.T) {
mockStorage, closer := helpers.MustGetMockStorage(t, true)
defer closer()

clusterList := make([]types.ClusterName, 2)
for i := range clusterList {
clusterList[i] = testdata.GetRandomClusterID()
}

err := mockStorage.WriteRecommendationsForCluster(
testdata.OrgID, clusterList[0], testdata.Report2Rules,
)
helpers.FailOnError(t, err)

err = mockStorage.WriteRecommendationsForCluster(
testdata.OrgID, clusterList[1], testdata.Report3Rules,
)
helpers.FailOnError(t, err)

reqBody, _ := json.Marshal(clusterList)

respBody := `{"recommendations":{"%v":%v,"%v":%v,"%v":%v},"status":"ok"}`
respBody = fmt.Sprintf(respBody,
testdata.Rule1CompositeID, 2,
testdata.Rule2CompositeID, 2,
testdata.Rule3CompositeID, 1,
)

helpers.AssertAPIRequest(t, mockStorage, nil, &helpers.APIRequest{
Method: http.MethodPost,
Endpoint: server.RecommendationsListEndpoint,
EndpointArgs: []interface{}{testdata.OrgID, testdata.UserID},
Body: reqBody,
}, &helpers.APIResponse{
StatusCode: http.StatusOK,
Body: respBody,
})
}
7 changes: 7 additions & 0 deletions storage/noop_storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,3 +300,10 @@ func (*NoopStorage) ListOfSystemWideDisabledRules(
orgID types.OrgID, userID types.UserID) ([]utypes.SystemWideRuleDisable, error) {
return nil, nil
}

// ReadRecommendationsForClusters reads all recommendations from recommendation table for given organization
func (*NoopStorage) ReadRecommendationsForClusters(
clusterList []types.ClusterName,
) (utypes.RecommendationImpactedClusters, error) {
return nil, nil
}
Loading

0 comments on commit 445c3a5

Please sign in to comment.