Skip to content

Commit

Permalink
Merge pull request #1342 from tisnik/report-metadata-endpoint
Browse files Browse the repository at this point in the history
Report metadata endpoint
  • Loading branch information
tisnik authored Dec 3, 2021
2 parents 95799ac + c0a2aca commit d3c35a2
Showing 8 changed files with 470 additions and 3 deletions.
86 changes: 86 additions & 0 deletions openapi.json
Original file line number Diff line number Diff line change
@@ -298,6 +298,92 @@
]
}
},
"/organizations/{orgId}/clusters/{clusterId}/users/{userId}/report/info": {
"get": {
"summary": "Returns metainformations about the latest report for the given organization and cluster.",
"operationId": "getReportMetainfoForCluster",
"description": "The report is specified by the organization ID and the cluster ID. Metainformation about the latest report available for the given combination will be returned.",
"parameters": [
{
"name": "orgId",
"in": "path",
"required": true,
"description": "ID of the organization that owns the cluster.",
"schema": {
"type": "integer",
"format": "int64",
"minimum": 0
}
},
{
"name": "clusterId",
"in": "path",
"required": true,
"description": "ID of the cluster which must conform to UUID format.",
"example": "34c3ecc5-624a-49a5-bab8-4fdc5e51a266",
"schema": {
"type": "string",
"minLength": 36,
"maxLength": 36,
"format": "uuid"
}
},
{
"name": "userId",
"in": "path",
"required": true,
"description": "Numeric ID of the user. An example: `42`",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Metainformation about the latest available report for the given organization and cluster combination.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"metainfo": {
"type": "object",
"properties": {
"count": {
"type": "integer",
"format": "int32",
"description": "Number of rules that were hit by the cluster. -1 is returned when no rules are defined for the cluster.",
"example": "1"
},
"last_checked_at": {
"type": "string",
"format": "date-time",
"description": "Timestamp when the report has been produced.",
"example": "2020-01-23T16:15:59.478901889Z"
},
"stored_at": {
"type": "string",
"format": "date-time",
"description": "Timestamp when the report has been written into database.",
"example": "2020-01-23T16:15:59.478901889Z"
}
}
},
"status": {
"type": "string",
"example": "ok"
}
}
}
}
}
}
},
"tags": [
"prod"
]
}
},
"/organizations/{orgId}/clusters/{clusterList}/reports": {
"get": {
"summary": "Returns the latest reports for the given list of clusters.",
3 changes: 3 additions & 0 deletions server/endpoints.go
Original file line number Diff line number Diff line change
@@ -35,6 +35,8 @@ const (
OrganizationsEndpoint = "organizations"
// ReportEndpoint returns report for provided {organization}, {cluster}, and {user_id}
ReportEndpoint = "organizations/{org_id}/clusters/{cluster}/users/{user_id}/report"
// ReportMetainfoEndpoint returns (meta)information about report for provided {organization} {cluster} and {rule_id}
ReportMetainfoEndpoint = "organizations/{org_id}/clusters/{cluster}/users/{user_id}/report/info"
// RuleEndpoint returns rule report for provided {organization} {cluster} and {rule_id}
RuleEndpoint = "organizations/{org_id}/clusters/{cluster}/users/{user_id}/rules/{rule_id}"
// ReportForListOfClustersEndpoint returns rule returns reports for provided list of clusters
@@ -121,6 +123,7 @@ func (server *HTTPServer) addEndpointsToRouter(router *mux.Router) {
// common REST API endpoints
router.HandleFunc(apiPrefix+MainEndpoint, server.mainEndpoint).Methods(http.MethodGet)
router.HandleFunc(apiPrefix+ReportEndpoint, server.readReportForCluster).Methods(http.MethodGet, http.MethodOptions)
router.HandleFunc(apiPrefix+ReportMetainfoEndpoint, server.readReportMetainfoForCluster).Methods(http.MethodGet, http.MethodOptions)
router.HandleFunc(apiPrefix+RuleEndpoint, server.readSingleRule).Methods(http.MethodGet, http.MethodOptions)
router.HandleFunc(apiPrefix+LikeRuleEndpoint, server.likeRule).Methods(http.MethodPut, http.MethodOptions)
router.HandleFunc(apiPrefix+DislikeRuleEndpoint, server.dislikeRule).Methods(http.MethodPut, http.MethodOptions)
41 changes: 40 additions & 1 deletion server/server.go
Original file line number Diff line number Diff line change
@@ -69,8 +69,11 @@ import (
)

const (
// ReportResponse constant that defines the name of response field
// ReportResponse constant defines the name of response field
ReportResponse = "report"

// ReportResponseMetainfo constant defines the name of response field
ReportResponseMetainfo = "metainfo"
)

// HTTPServer in an implementation of Server interface
@@ -180,6 +183,42 @@ func (server *HTTPServer) readReportForCluster(writer http.ResponseWriter, reque
}
}

// readReportForCluster method retrieves metainformations for report stored in
// database and return the retrieved info to requester via response payload.
// The payload has type types.ReportResponseMetainfo
func (server *HTTPServer) readReportMetainfoForCluster(writer http.ResponseWriter, request *http.Request) {
clusterName, successful := readClusterName(writer, request)
if !successful {
// everything has been handled already
return
}

orgID, successful := readOrgID(writer, request)
if !successful {
return
}

reports, lastChecked, storedAt, err := server.Storage.ReadReportForCluster(orgID, clusterName)
if err != nil {
log.Error().Err(err).Msg("Unable to read report for cluster")
handleServerError(writer, err)
return
}

hitRulesCount := getHitRulesCount(reports)

response := ctypes.ReportResponseMetainfo{
Count: hitRulesCount,
LastCheckedAt: lastChecked,
StoredAt: storedAt,
}

err = responses.SendOK(writer, responses.BuildOkResponseWithData(ReportResponseMetainfo, response))
if err != nil {
log.Error().Err(err).Msg(responseDataError)
}
}

// getHitRulesCount function computes number of rule hits from given report.
//
// Special values:
149 changes: 149 additions & 0 deletions server/server_read_report_metainfo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// Copyright 2021 Red Hat, Inc
//
// 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 server_test

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

"github.com/RedHatInsights/insights-results-aggregator-data/testdata"

"github.com/RedHatInsights/insights-results-aggregator/server"
"github.com/RedHatInsights/insights-results-aggregator/tests/helpers"
)

func TestReadReportMetainfoForClusterNonIntOrgID(t *testing.T) {
helpers.AssertAPIRequest(t, nil, nil, &helpers.APIRequest{
Method: http.MethodGet,
Endpoint: server.ReportMetainfoEndpoint,
EndpointArgs: []interface{}{"non-int", testdata.ClusterName, testdata.UserID},
}, &helpers.APIResponse{
StatusCode: http.StatusBadRequest,
Body: `{
"status": "Error during parsing param 'org_id' with value 'non-int'. Error: 'unsigned integer expected'"
}`,
})
}

func TestReadReportMetainfoForClusterNegativeOrgID(t *testing.T) {
helpers.AssertAPIRequest(t, nil, nil, &helpers.APIRequest{
Method: http.MethodGet,
Endpoint: server.ReportMetainfoEndpoint,
EndpointArgs: []interface{}{-1, testdata.ClusterName, testdata.UserID},
}, &helpers.APIResponse{
StatusCode: http.StatusBadRequest,
Body: `{
"status":"Error during parsing param 'org_id' with value '-1'. Error: 'unsigned integer expected'"
}`,
})
}

func TestReadReportMetainfoForClusterBadClusterName(t *testing.T) {
helpers.AssertAPIRequest(t, nil, nil, &helpers.APIRequest{
Method: http.MethodGet,
Endpoint: server.ReportMetainfoEndpoint,
EndpointArgs: []interface{}{testdata.OrgID, testdata.BadClusterName, testdata.UserID},
}, &helpers.APIResponse{
StatusCode: http.StatusBadRequest,
Body: `{"status": "Error during parsing param 'cluster' with value 'aaaa'. Error: 'invalid UUID length: 4'"}`,
})
}

func TestReadNonExistingReportMetainfo(t *testing.T) {
helpers.AssertAPIRequest(t, nil, nil, &helpers.APIRequest{
Method: http.MethodGet,
Endpoint: server.ReportMetainfoEndpoint,
EndpointArgs: []interface{}{testdata.OrgID, testdata.ClusterName, testdata.UserID},
}, &helpers.APIResponse{
StatusCode: http.StatusNotFound,
Body: fmt.Sprintf(
`{"status":"Item with ID %v/%v was not found in the storage"}`, testdata.OrgID, testdata.ClusterName,
),
})
}

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

err := mockStorage.WriteReportForCluster(
testdata.OrgID, testdata.ClusterName, testdata.Report0Rules, testdata.ReportEmptyRulesParsed, testdata.LastCheckedAt, testdata.LastCheckedAt, testdata.KafkaOffset,
)
helpers.FailOnError(t, err)

helpers.AssertAPIRequest(t, mockStorage, nil, &helpers.APIRequest{
Method: http.MethodGet,
Endpoint: server.ReportMetainfoEndpoint,
EndpointArgs: []interface{}{testdata.OrgID, testdata.ClusterName, testdata.UserID},
}, &helpers.APIResponse{
StatusCode: http.StatusOK,
Body: `{
"status":"ok",
"metainfo": {
"count": -1,
"last_checked_at": "` + testdata.LastCheckedAt.Format(time.RFC3339) + `",
"stored_at": "` + testdata.LastCheckedAt.Format(time.RFC3339) + `"
}
}`,
})
}

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

helpers.AssertAPIRequest(t, mockStorage, nil, &helpers.APIRequest{
Method: http.MethodGet,
Endpoint: server.ReportMetainfoEndpoint,
EndpointArgs: []interface{}{testdata.OrgID, testdata.ClusterName, testdata.UserID},
}, &helpers.APIResponse{
StatusCode: http.StatusInternalServerError,
Body: `{"status":"Internal Server Error"}`,
})
}

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

err := mockStorage.WriteReportForCluster(
testdata.OrgID,
testdata.ClusterName,
testdata.Report3Rules,
testdata.Report3RulesParsed,
testdata.LastCheckedAt,
testdata.LastCheckedAt,
testdata.KafkaOffset,
)
helpers.FailOnError(t, err)

helpers.AssertAPIRequest(t, mockStorage, nil, &helpers.APIRequest{
Method: http.MethodGet,
Endpoint: server.ReportMetainfoEndpoint,
EndpointArgs: []interface{}{testdata.OrgID, testdata.ClusterName, testdata.UserID},
}, &helpers.APIResponse{
StatusCode: http.StatusOK,
Body: `{
"status":"ok",
"metainfo": {
"count": 3,
"last_checked_at": "` + testdata.LastCheckedAt.Format(time.RFC3339) + `",
"stored_at": "` + testdata.LastCheckedAt.Format(time.RFC3339) + `"
}
}`,
})
}
2 changes: 2 additions & 0 deletions tests/rest/common.go
Original file line number Diff line number Diff line change
@@ -47,6 +47,8 @@ const (
knownCluster2ForOrganization1 = "00000000-0000-0000-ffff-000000000000"
knownCluster3ForOrganization1 = "00000000-0000-0000-0000-ffffffffffff"
unknownClusterForOrganization1 = "00000000-0000-0000-0000-000000000001"

wrongOrganizationID = "foobar"
)

// StatusOnlyResponse represents response containing just a status
4 changes: 2 additions & 2 deletions tests/rest/reports.go
Original file line number Diff line number Diff line change
@@ -88,7 +88,7 @@ func reproducerForIssue384() {

// checkReportEndpointForImproperOrganization check if the endpoint to return report works as expected
func checkReportEndpointForImproperOrganization() {
url := constructURLForReportForOrgCluster("foobar", knownClusterForOrganization1, testdata.UserID)
url := constructURLForReportForOrgCluster(wrongOrganizationID, knownClusterForOrganization1, testdata.UserID)
f := frisby.Create("Check the endpoint to return report for improper organization").Get(url)
setAuthHeader(f)
f.Send()
@@ -170,7 +170,7 @@ func checkReportEndpointForUnknownOrganizationAndUnknownClusterUnauthorizedCase(
// checkReportEndpointForImproperOrganizationUnauthorizedCase check if the endpoint to return report works as expected
// This test variant does not sent authorization header
func checkReportEndpointForImproperOrganizationUnauthorizedCase() {
url := constructURLForReportForOrgCluster("foobar", knownClusterForOrganization1, testdata.UserID)
url := constructURLForReportForOrgCluster(wrongOrganizationID, knownClusterForOrganization1, testdata.UserID)
f := frisby.Create("Check the endpoint to return report for improper organization w/o authorization token").Get(url)
f.Send()
f.ExpectStatus(401)
Loading

0 comments on commit d3c35a2

Please sign in to comment.