handlers_v2.go

Copyright 2020, 2021, 2022, 2024 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

handlers for API V2 endpoints


import
(
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"math"
"net/http"
"net/url"
"time"
"github.com/rs/zerolog/log"
"github.com/RedHatInsights/insights-content-service/groups"
httputils
"github.com/RedHatInsights/insights-operator-utils/http"
"github.com/RedHatInsights/insights-operator-utils/responses"
utypes
"github.com/RedHatInsights/insights-operator-utils/types"
ctypes
"github.com/RedHatInsights/insights-results-types"
ira_server
"github.com/RedHatInsights/insights-results-aggregator/server"
"github.com/RedHatInsights/insights-results-smart-proxy/content"
"github.com/RedHatInsights/insights-results-smart-proxy/services"
"github.com/RedHatInsights/insights-results-smart-proxy/types"
)
const
(

OnlyImpacting flag to only return impacting recommendations on GET /rule/

	
OnlyImpacting
=
iota

IncludingImpacting flag to return all recommendations including impacting ones on GET /rule/

	
IncludingImpacting

ExcludingImpacting flag to return all recommendations excluding impacting ones on GET /rule/

	
ExcludingImpacting

OkMsg is in status field with HTTP 200 response

	
OkMsg
=
"ok"
selectorStr
=
"selector"

StatusProcessed is a message returned for already processed reports stored in Redis

	
StatusProcessed
=
"processed"

RequestsForClusterNotFound is a message returned when no request IDs were found for a given clusterID

	
RequestsForClusterNotFound
=
"Requests for cluster not found"

RequestIDNotFound is returned when the requested request ID was not found in the list of request IDs for given cluster

	
RequestIDNotFound
=
"Request ID not found for given org_id and cluster_id"

RedisNotInitializedErrorMessage is an error message written into log when Redis client is not initialized properly

	
RedisNotInitializedErrorMessage
=
"Redis is not initialized, request can not be finished correctly"

AMSApiNotInitializedErrorMessage is an error message written into log when AMS API client is not initialized properly

	
AMSApiNotInitializedErrorMessage
=
"AMS API connection is not initialized"
)
func
safeUint8
(
value
int
)
(
uint8
,
error
)
{
if
value
<
0
{
return
0
,
fmt
.
Errorf
(
"cannot convert negative number to uint8: %d"
,
value
)
}
if
value
>
math
.
MaxUint8
{
return
0
,
fmt
.
Errorf
(
"value %d is greater than the maximum uint8 value"
,
value
)
}
return
uint8
(
value
)
,
nil
}
func
safeUint32
(
value
int
)
(
uint32
,
error
)
{
if
value
<
0
{
return
0
,
fmt
.
Errorf
(
"cannot convert negative number to uint32: %d"
,
value
)
}
if
value
>
math
.
MaxUint32
{
return
0
,
fmt
.
Errorf
(
"value %d is greater than the maximum uint32 value"
,
value
)
}
return
uint32
(
value
)
,
nil
}

getContentCheckInternal retrieves static content for the given ruleID and if the rule is internal, checks if user has permissions to access it.

func
(
server
HTTPServer
)
getContentCheckInternal
(
ruleID
ctypes
.
RuleID
,
request
*
http
.
Request
)
(
ruleContent
*
types
.
RuleWithContent
,
err
error
,
)
{
ruleContent
,
err
=
content
.
GetContentForRecommendation
(
ruleID
)
if
err
!=
nil
{
return
}

check for internal rule permissions

	
if
internal
:=
content
.
IsRuleInternal
(
ruleID
)
;
internal
{
err
=
server
.
checkInternalRulePermissions
(
request
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Send
(
)
return
}
}
return
}

getRuleWithGroups retrieves static content for the given ruleID along with rule groups

func
(
server
HTTPServer
)
getRuleWithGroups
(
request
*
http
.
Request
,
ruleID
ctypes
.
RuleID
,
)
(
ruleContent
*
types
.
RuleWithContent
,
ruleGroups
[
]
groups
.
Group
,
err
error
,
)
{
ruleContent
,
err
=
server
.
getContentCheckInternal
(
ruleID
,
request
)
if
err
!=
nil
{
log
.
Error
(
)
.
Interface
(
ruleIDStr
,
ruleID
)
.
Msg
(
"error retrieving rule content for rule"
)
return
}

retrieve the latest groups configuration

	
ruleGroups
,
err
=
server
.
getGroupsConfig
(
)
if
err
!=
nil
{
log
.
Error
(
)
.
Msg
(
"error retrieving rule groups"
)
return
}
return
}

getRecommendationContent retrieves the static content for the given ruleID tied with groups info. rule ID is expected to be the composite rule ID (rule.module|ERROR_KEY)

func
(
server
HTTPServer
)
getRecommendationContent
(
writer
http
.
ResponseWriter
,
request
*
http
.
Request
)
{
ruleID
,
err
:=
readCompositeRuleID
(
request
)
if
err
!=
nil
{
log
.
Warn
(
)
.
Err
(
err
)
.
Msgf
(
"error retrieving rule ID from request"
)
handleServerError
(
writer
,
err
)
return
}
ruleContent
,
ruleGroups
,
err
:=
server
.
getRuleWithGroups
(
request
,
ruleID
)
if
err
!=
nil
{
log
.
Warn
(
)
.
Err
(
err
)
.
Msgf
(
"error retrieving rule content and groups for rule ID %v"
,
ruleID
)
handleServerError
(
writer
,
err
)
return
}
totalRisk
,
err
:=
safeUint8
(
ruleContent
.
TotalRisk
)
if
err
!=
nil
{
handleServerError
(
writer
,
err
)
return
}
impact
,
err
:=
safeUint8
(
ruleContent
.
Impact
)
if
err
!=
nil
{
handleServerError
(
writer
,
err
)
return
}
likelihood
,
err
:=
safeUint8
(
ruleContent
.
Likelihood
)
if
err
!=
nil
{
handleServerError
(
writer
,
err
)
return
}
contentResponse
:=
types
.
RecommendationContent
{

RuleID in rule.module|ERROR_KEY format

		
RuleSelector
:
ctypes
.
RuleSelector
(
ruleID
)
,
Description
:
ruleContent
.
Description
,
Generic
:
ruleContent
.
Generic
,
Reason
:
ruleContent
.
Reason
,
Resolution
:
ruleContent
.
Resolution
,
MoreInfo
:
ruleContent
.
MoreInfo
,
TotalRisk
:
totalRisk
,
Impact
:
impact
,
Likelihood
:
likelihood
,
PublishDate
:
ruleContent
.
PublishDate
,
Tags
:
ruleContent
.
Tags
,
}

prepare data structure for building response

	
responseContent
:=
make
(
map
[
string
]
interface
{
}
)
responseContent
[
"status"
]
=
OkMsg
responseContent
[
"groups"
]
=
ruleGroups
responseContent
[
"content"
]
=
contentResponse

send response to client

	
err
=
responses
.
SendOK
(
writer
,
responseContent
)
if
err
!=
nil
{
handleServerError
(
writer
,
err
)
return
}
}

getRecommendationContent retrieves the static content for the given ruleID tied with groups info. rule ID is expected to be the composite rule ID (rule.module|ERROR_KEY)

func
(
server
HTTPServer
)
getRecommendationContentWithUserData
(
writer
http
.
ResponseWriter
,
request
*
http
.
Request
)
{
orgID
,
err
:=
server
.
GetCurrentOrgID
(
request
)
if
err
!=
nil
{
log
.
Err
(
err
)
.
Msg
(
orgIDTokenError
)
handleServerError
(
writer
,
err
)
return
}
ruleID
,
err
:=
readCompositeRuleID
(
request
)
if
err
!=
nil
{
log
.
Warn
(
)
.
Err
(
err
)
.
Msg
(
"error retrieving rule ID from request"
)
handleServerError
(
writer
,
err
)
return
}
ruleContent
,
ruleGroups
,
err
:=
server
.
getRuleWithGroups
(
request
,
ruleID
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Interface
(
ruleIDStr
,
ruleID
)
.
Msg
(
"error retrieving rule content and groups for rule"
)
handleServerError
(
writer
,
err
)
return
}
rating
,
err
:=
server
.
getRatingForRecommendation
(
orgID
,
ruleID
)
if
err
!=
nil
{
switch
err
.
(
type
)
{
case
*
utypes
.
ItemNotFoundError
:
break
case
*
url
.
Error
:
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
"aggregator is not responding"
)
handleServerError
(
writer
,
&
AggregatorServiceUnavailableError
{
}
)
return
default
:
handleServerError
(
writer
,
err
)
return
}
}
ruleModule
,
errorKey
,
err
:=
types
.
RuleIDWithErrorKeyFromCompositeRuleID
(
ruleID
)
if
err
!=
nil
{
handleServerError
(
writer
,
err
)
return
}

ignoring the response and the possible error. We are just interested on know if the rule is system disabled or not

	
_
,
ackFound
,
_
:=
server
.
readRuleDisableStatus
(
ctypes
.
Component
(
ruleModule
)
,
errorKey
,
orgID
,
)
totalRisk
,
err
:=
safeUint8
(
ruleContent
.
TotalRisk
)
if
err
!=
nil
{
handleServerError
(
writer
,
err
)
return
}
resolutionRisk
,
err
:=
safeUint8
(
ruleContent
.
ResolutionRisk
)
if
err
!=
nil
{
handleServerError
(
writer
,
err
)
return
}
impact
,
err
:=
safeUint8
(
ruleContent
.
Impact
)
if
err
!=
nil
{
handleServerError
(
writer
,
err
)
return
}
likelihood
,
err
:=
safeUint8
(
ruleContent
.
Likelihood
)
if
err
!=
nil
{
handleServerError
(
writer
,
err
)
return
}

fill in user rating and other DB stuff from aggregator

	
contentResponse
:=
types
.
RecommendationContentUserData
{

RuleID in rule.module|ERROR_KEY format

		
RuleSelector
:
ctypes
.
RuleSelector
(
ruleID
)
,
Description
:
ruleContent
.
Description
,
Generic
:
ruleContent
.
Generic
,
Reason
:
ruleContent
.
Reason
,
Resolution
:
ruleContent
.
Resolution
,
MoreInfo
:
ruleContent
.
MoreInfo
,
TotalRisk
:
totalRisk
,
ResolutionRisk
:
resolutionRisk
,
Impact
:
impact
,
Likelihood
:
likelihood
,
PublishDate
:
ruleContent
.
PublishDate
,
Rating
:
rating
.
Rating
,
AckedCount
:
0
,
Tags
:
ruleContent
.
Tags
,
Disabled
:
ackFound
,
}

prepare data structure for building response

	
responseContent
:=
make
(
map
[
string
]
interface
{
}
)
responseContent
[
"status"
]
=
OkMsg
responseContent
[
"groups"
]
=
ruleGroups
responseContent
[
"content"
]
=
contentResponse

send response to client

	
err
=
responses
.
SendOK
(
writer
,
responseContent
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
problemSendingResponseError
)
handleServerError
(
writer
,
err
)
return
}
}

getRecommendations retrieves all recommendations with a count of impacted clusters By default returns only those recommendations that currently hit at least one cluster, but it's possible to show all recommendations by passing a URL parameter impacting

func
(
server
HTTPServer
)
getRecommendations
(
writer
http
.
ResponseWriter
,
request
*
http
.
Request
)
{
var
recommendationList
[
]
types
.
RecommendationListView
tStart
:=
time
.
Now
(
)
userID
,
orgID
,
impactingFlag
,
err
:=
server
.
readParamsGetRecommendations
(
writer
,
request
)
if
err
!=
nil
{

everything handled

		
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
"problem reading necessary params from request"
)
return
}
activeClustersInfo
,
err
:=
server
.
readClusterInfoForOrgID
(
orgID
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Int
(
orgIDTag
,
int
(
orgID
)
)
.
Msg
(
clusterListError
)
handleServerError
(
writer
,
err
)
return
}
clusterIDList
:=
types
.
GetClusterNames
(
activeClustersInfo
)
tStartImpacting
:=
time
.
Now
(
)
impactingRecommendations
,
err
:=
server
.
getImpactingRecommendations
(
writer
,
orgID
,
userID
,
clusterIDList
,
)
if
err
!=
nil
{

log cluster list in case of error even though message might be too large for Kibana/zerolog

		
log
.
Error
(
)
.
Err
(
err
)
.
Int
(
orgIDTag
,
int
(
orgID
)
)
.
Msgf
(
"problem getting impacting recommendations from aggregator for cluster list (# of clusters: %v)"
,
len
(
clusterIDList
)
)
return
}
log
.
Debug
(
)
.
Uint32
(
orgIDTag
,
uint32
(
orgID
)
)
.
Msgf
(
"getRecommendations get impacting recommendations from aggregator took %s"
,
time
.
Since
(
tStartImpacting
)
,
)

get a map of acknowledged rules

	
ackedRulesMap
,
err
:=
server
.
getRuleAcksMap
(
orgID
)
if
err
!=
nil
{
handleServerError
(
writer
,
err
)
return
}

retrieve user disabled rules for given list of active clusters

	
disabledClustersForRules
:=
server
.
getRuleDisabledClusters
(
writer
,
orgID
,
clusterIDList
)
recommendationList
,
err
=
getFilteredRecommendationsList
(
activeClustersInfo
,
impactingRecommendations
,
impactingFlag
,
ackedRulesMap
,
disabledClustersForRules
,
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
"problem getting recommendation content"
)
handleServerError
(
writer
,
err
)
return
}
log
.
Debug
(
)
.
Int
(
orgIDTag
,
int
(
orgID
)
)
.
Str
(
userIDTag
,
string
(
userID
)
)
.
Msgf
(
"number of final recommendations: %d"
,
len
(
recommendationList
)
)
resp
:=
make
(
map
[
string
]
interface
{
}
)
resp
[
"status"
]
=
OkMsg
resp
[
"recommendations"
]
=
recommendationList
log
.
Info
(
)
.
Uint32
(
orgIDTag
,
uint32
(
orgID
)
)
.
Msgf
(
"getRecommendations took %s"
,
time
.
Since
(
tStart
)
,
)
err
=
responses
.
SendOK
(
writer
,
resp
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
problemSendingResponseError
)
handleServerError
(
writer
,
err
)
return
}
}
func
(
server
HTTPServer
)
getRuleAcksMap
(
orgID
types
.
OrgID
)
(
ackedRulesMap
map
[
ctypes
.
RuleID
]
bool
,
err
error
,
)
{
ackedRulesMap
=
make
(
map
[
ctypes
.
RuleID
]
bool
)

retrieve rule acknowledgements (disable/enable for all clusters)

	
ackedRules
,
err
:=
server
.
readListOfAckedRules
(
orgID
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
ackedRulesError
)
return
}

put rule acks in a map so we only iterate over them once

	
ackedRulesMap
=
generateRuleAckMap
(
ackedRules
)
return
}
func
(
server
HTTPServer
)
getRuleDisabledClusters
(
writer
http
.
ResponseWriter
,
orgID
types
.
OrgID
,
clusterList
[
]
ctypes
.
ClusterName
,
)
(
ruleDisabledClusters
map
[
types
.
RuleID
]
[
]
types
.
ClusterName
,
)
{
ruleDisabledClusters
=
make
(
map
[
types
.
RuleID
]
[
]
types
.
ClusterName
)
listOfDisabledRules
,
err
:=
server
.
readListOfDisabledRulesForClusters
(
writer
,
orgID
,
clusterList
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
"error reading disabled rules from aggregator"
)

server error has been handled already

		
return
}
for
_
,
disabledRule
:=
range
listOfDisabledRules
{
compositeRuleID
,
err
:=
generateCompositeRuleIDFromDisabled
(
disabledRule
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
"error generating composite rule ID"
)
continue
}
ruleDisabledClusters
[
compositeRuleID
]
=
append
(
ruleDisabledClusters
[
compositeRuleID
]
,
disabledRule
.
ClusterID
)
}
return
}

getClustersView retrieves all clusters for given organization, retrieves the impacting rules for each cluster from aggregator and returns a list of clusters, total number of hitting rules and a count of impacting rules by severity = total risk = critical, high, moderate, low

func
(
server
HTTPServer
)
getClustersView
(
writer
http
.
ResponseWriter
,
request
*
http
.
Request
)
{
tStart
:=
time
.
Now
(
)
orgID
,
userID
,
err
:=
server
.
GetCurrentOrgIDUserIDFromToken
(
request
)
if
err
!=
nil
{
log
.
Err
(
err
)
.
Msg
(
orgIDTokenError
)
handleServerError
(
writer
,
err
)
return
}
clusterList
,
clusterRuleHits
,
ackedRulesMap
,
disabledRules
:=
server
.
getClusterListAndUserData
(
writer
,
orgID
,
userID
,
)
clusterViewResponse
,
err
:=
matchClusterInfoAndUserData
(
clusterList
,
clusterRuleHits
,
ackedRulesMap
,
disabledRules
,
)
if
err
!=
nil
{
log
.
Error
(
)
.
Uint32
(
orgIDTag
,
uint32
(
orgID
)
)
.
Err
(
err
)
.
Msg
(
"getClustersView error generating cluster list response"
)
handleServerError
(
writer
,
err
)
}
log
.
Debug
(
)
.
Uint32
(
orgIDTag
,
uint32
(
orgID
)
)
.
Msgf
(
"getClustersView final number %v"
,
len
(
clusterViewResponse
)
)
resp
:=
make
(
map
[
string
]
interface
{
}
)
metaCount
:=
map
[
string
]
int
{
"count"
:
len
(
clusterViewResponse
)
,
}
resp
[
"status"
]
=
OkMsg
resp
[
"meta"
]
=
metaCount
resp
[
"data"
]
=
clusterViewResponse
log
.
Debug
(
)
.
Uint32
(
orgIDTag
,
uint32
(
orgID
)
)
.
Msgf
(
"getClustersView took %s"
,
time
.
Since
(
tStart
)
)
err
=
responses
.
SendOK
(
writer
,
resp
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
problemSendingResponseError
)
handleServerError
(
writer
,
err
)
return
}
}

getSingleClusterInfo retrieves information about given cluster from AMS API, such as the user defined display name

func
(
server
HTTPServer
)
getSingleClusterInfo
(
writer
http
.
ResponseWriter
,
request
*
http
.
Request
)
{
if
server
.
amsClient
==
nil
{
log
.
Error
(
)
.
Msg
(
AMSApiNotInitializedErrorMessage
)
handleServerError
(
writer
,
&
AMSAPIUnavailableError
{
}
)
return
}
orgID
,
err
:=
server
.
GetCurrentOrgID
(
request
)
if
err
!=
nil
{
handleServerError
(
writer
,
err
)
return
}
clusterID
,
successful
:=
httputils
.
ReadClusterName
(
writer
,
request
)

error handled by function

	
if
!
successful
{
return
}
clusterInfo
,
err
:=
server
.
amsClient
.
GetSingleClusterInfoForOrganization
(
orgID
,
clusterID
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
"problem retrieving cluster info from AMS API"
)
handleServerError
(
writer
,
err
)
return
}

retrieval failed, but error is nil

	
if
clusterInfo
.
ID
==
""
{
err
:=
&
utypes
.
ItemNotFoundError
{
ItemID
:
clusterID
}
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
"unexpected problem retrieving cluster info from AMS API"
)
handleServerError
(
writer
,
err
)
return
}
if
err
=
responses
.
SendOK
(
writer
,
responses
.
BuildOkResponseWithData
(
"cluster"
,
clusterInfo
)
)
;
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
problemSendingResponseError
)
handleServerError
(
writer
,
err
)
return
}
}

matchClusterInfoAndUserData matches data from AMS API, rule hits from aggregator + user data from aggregator regarding disabled rules and calculates the numbers of hitting rules based on their severity (total risk)

func
matchClusterInfoAndUserData
(
clusterInfoList
[
]
types
.
ClusterInfo
,
clusterRecommendationsMap
ctypes
.
ClusterRecommendationMap
,
systemWideDisabledRules
map
[
ctypes
.
RuleID
]
bool
,
disabledRulesPerCluster
map
[
ctypes
.
ClusterName
]
[
]
ctypes
.
RuleID
,
)
(
[
]
types
.
ClusterListView
,
error
,
)
{
clusterListView
:=
make
(
[
]
types
.
ClusterListView
,
0
)
recommendationSeverities
,
uniqueSeverities
,
err
:=
content
.
GetExternalRuleSeverities
(
)
if
err
!=
nil
{
return
clusterListView
,
err
}
rulesManagedInfo
,
err
:=
content
.
GetExternalRulesManagedInfo
(
)
if
err
!=
nil
{
return
clusterListView
,
err
}

iterates over clusters and their hitting recommendations, accesses map to the get rule severity

	
for
i
:=
range
clusterInfoList
{
clusterViewItem
:=
types
.
ClusterListView
{
ClusterID
:
clusterInfoList
[
i
]
.
ID
,
ClusterName
:
clusterInfoList
[
i
]
.
DisplayName
,
Managed
:
clusterInfoList
[
i
]
.
Managed
,
HitsByTotalRisk
:
make
(
map
[
int
]
int
)
,
}

zero in unique severities to have constitent response

		
for
_
,
severity
:=
range
uniqueSeverities
{
clusterViewItem
.
HitsByTotalRisk
[
severity
]
=
0
}

check if there are any hitting recommendations

		
if
hittingRecommendations
,
exist
:=
clusterRecommendationsMap
[
clusterViewItem
.
ClusterID
]
;
exist
{
clusterViewItem
.
LastCheckedAt
=
types
.
Timestamp
(
hittingRecommendations
.
CreatedAt
.
UTC
(
)
.
Format
(
time
.
RFC3339
)
,
)
clusterViewItem
.
Version
=
hittingRecommendations
.
Meta
.
Version

filter out acked and disabled rules

			
enabledOnlyRecommendations
:=
filterOutDisabledRules
(
hittingRecommendations
.
Recommendations
,
clusterViewItem
.
ClusterID
,
systemWideDisabledRules
,
disabledRulesPerCluster
,
)
for
_
,
ruleID
:=
range
enabledOnlyRecommendations
{
if
clusterViewItem
.
Managed
&&
!
rulesManagedInfo
[
ruleID
]
{

cluster is managed, therefore must show only managed rules

					
continue
}
if
ruleSeverity
,
found
:=
recommendationSeverities
[
ruleID
]
;
found
{
clusterViewItem
.
HitsByTotalRisk
[
ruleSeverity
]
++
clusterViewItem
.
TotalHitCount
++
}
else
{

rule content is missing for this rule; mimicking behaviour of other apps such as OCM = skip rule

					
log
.
Error
(
)
.
Interface
(
ruleIDStr
,
ruleID
)
.
Msg
(
"rule content was not found for following rule ID. Skipping it"
)
}
}
}
clusterListView
=
append
(
clusterListView
,
clusterViewItem
)
}
return
clusterListView
,
nil
}

filterOutDisabledRules filters out system-wide disabled rules (rule acknowledgement) and rules which had been disabled on a single cluster basis.

func
filterOutDisabledRules
(
hittingRecommendations
[
]
ctypes
.
RuleID
,
clusterID
ctypes
.
ClusterName
,
systemWideDisabledRules
map
[
ctypes
.
RuleID
]
bool
,
disabledRulesPerCluster
map
[
ctypes
.
ClusterName
]
[
]
ctypes
.
RuleID
,
)
(
enabledOnlyRecommendations
[
]
ctypes
.
RuleID
,
)
{
for
_
,
hittingRuleID
:=
range
hittingRecommendations
{

no need to continue, rule has been acked

		
if
systemWideDisabledRules
[
hittingRuleID
]
{
continue
}

try to find rule ID in list of disabled rules, if any

		
ruleDisabled
:=
false
if
disabledRulesList
,
exists
:=
disabledRulesPerCluster
[
clusterID
]
;
exists
{
for
_
,
disabledRuleID
:=
range
disabledRulesList
{
if
disabledRuleID
==
hittingRuleID
{
ruleDisabled
=
true
}
}
}
if
!
ruleDisabled
{
enabledOnlyRecommendations
=
append
(
enabledOnlyRecommendations
,
hittingRuleID
)
}
}
return
}

Method getUserDisabledRulesPerCluster returns a map of cluster IDs with a list of disabled rules for each cluster

func
(
server
*
HTTPServer
)
getUserDisabledRulesPerCluster
(
orgID
types
.
OrgID
)
(
disabledRulesPerCluster
map
[
ctypes
.
ClusterName
]
[
]
ctypes
.
RuleID
,
)
{
listOfDisabledRules
,
err
:=
server
.
readListOfClusterDisabledRules
(
orgID
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
"error retrieving list of disabled rules"
)
return
}
disabledRulesPerCluster
=
make
(
map
[
ctypes
.
ClusterName
]
[
]
ctypes
.
RuleID
)
for
i
:=
range
listOfDisabledRules
{
disabledRule
:=
&
listOfDisabledRules
[
i
]
compositeRuleID
,
err
:=
generateCompositeRuleIDFromDisabled
(
*
disabledRule
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Interface
(
ruleIDStr
,
disabledRule
.
RuleID
)
.
Interface
(
errorKeyStr
,
disabledRule
.
ErrorKey
)
.
Msg
(
compositeRuleIDError
)
continue
}
if
ruleList
,
found
:=
disabledRulesPerCluster
[
disabledRule
.
ClusterID
]
;
found
{
disabledRulesPerCluster
[
disabledRule
.
ClusterID
]
=
append
(
ruleList
,
compositeRuleID
)
}
else
{
disabledRulesPerCluster
[
disabledRule
.
ClusterID
]
=
[
]
ctypes
.
RuleID
{
compositeRuleID
}
}
}
return
}
func
generateImpactingRuleIDList
(
impactingRecommendations
ctypes
.
RecommendationImpactedClusters
)
(
ruleIDList
[
]
ctypes
.
RuleID
)
{
ruleIDList
=
make
(
[
]
ctypes
.
RuleID
,
len
(
impactingRecommendations
)
)
i
:=
0
for
ruleID
:=
range
impactingRecommendations
{
ruleIDList
[
i
]
=
ruleID
i
++
}
return
}
func
excludeDisabledClusters
(
impactingClusters
[
]
types
.
ClusterName
,
disabledClusters
[
]
types
.
ClusterName
,
)
(
filteredClusters
[
]
types
.
ClusterName
)
{
for
_
,
impactingID
:=
range
impactingClusters
{
disabled
:=
false
for
_
,
disabledID
:=
range
disabledClusters
{
if
impactingID
==
disabledID
{
disabled
=
true
break
}
}
if
!
disabled
{
filteredClusters
=
append
(
filteredClusters
,
impactingID
)
}
}
return
}
func
getFilteredRecommendationsList
(
activeClustersInfo
[
]
types
.
ClusterInfo
,
impactingRecommendations
ctypes
.
RecommendationImpactedClusters
,
impactingFlag
types
.
ImpactingFlag
,
ruleAcksMap
map
[
types
.
RuleID
]
bool
,
disabledClustersForRules
map
[
types
.
RuleID
]
[
]
types
.
ClusterName
,
)
(
recommendationList
[
]
types
.
RecommendationListView
,
err
error
,
)
{
clusterInfoMap
:=
types
.
ClusterInfoArrayToMap
(
activeClustersInfo
)
recommendationList
=
make
(
[
]
types
.
RecommendationListView
,
0
)
var
ruleIDList
[
]
ctypes
.
RuleID
if
impactingFlag
==
OnlyImpacting
{

retrieve content only for impacting rules

		
ruleIDList
=
generateImpactingRuleIDList
(
impactingRecommendations
)
}
else
{

retrieve content for all external rules and decide whether exclude impacting in loop

		
ruleIDList
,
err
=
content
.
GetExternalRuleIDs
(
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
"unable to retrieve external rule ids from content directory"
)
return
}
}

iterate over rules and count impacted clusters, exluding user disabled ones

	
for
_
,
ruleID
:=
range
ruleIDList
{
var
impactedClustersCnt
uint32
var
ruleContent
*
types
.
RuleWithContent

rule has system-wide disabled status if found in the ack map, but the user must be able to see the number of impacted clusters in the UI, so we need to go on

		
_
,
ruleDisabled
:=
ruleAcksMap
[
ruleID
]

get list of impacting clusters

		
impactingClustersList
,
found
:=
impactingRecommendations
[
ruleID
]
if
found
&&
impactingFlag
==
ExcludingImpacting
{

rule is impacting, but requester doesn't want them

			
continue
}

remove any disabled clusters from the total count, if they're impacting

		
if
disabledClusters
,
exist
:=
disabledClustersForRules
[
ruleID
]
;
exist
{
impactingClustersList
=
excludeDisabledClusters
(
impactingClustersList
,
disabledClusters
)
}
ruleContent
,
err
=
content
.
GetContentForRecommendation
(
ruleID
)
if
err
!=
nil
{
if
err
,
ok
:=
err
.
(
*
content
.
RuleContentDirectoryTimeoutError
)
;
ok
{
return
recommendationList
,
err
}

missing rule content, simply omit the rule as we can't display anything

			
log
.
Error
(
)
.
Err
(
err
)
.
Interface
(
ruleIDStr
,
ruleID
)
.
Msg
(
ruleContentError
)
continue
}
if
!
ruleContent
.
OSDCustomer
{

rule doesn't have osd_customer tag, so it doesn't apply to managed clusters

			
for
_
,
clusterID
:=
range
impactingClustersList
{

exclude non-managed clusters from the count

				
if
!
clusterInfoMap
[
clusterID
]
.
Managed
{
impactedClustersCnt
++
}
}
}
else
{

rule has osd_customer tag and can be shown for all clusters

			
impactedClustersCnt
,
err
=
safeUint32
(
len
(
impactingClustersList
)
)
if
err
!=
nil
{
return
}
}
recommendationListView
,
err
:=
parseRecommendationListView
(
ruleID
,
ruleContent
,
ruleDisabled
,
impactedClustersCnt
)
if
err
!=
nil
{
return
recommendationList
,
err
}
recommendationList
=
append
(
recommendationList
,
recommendationListView
)
}
return
}
func
parseRecommendationListView
(
ruleID
types
.
RuleID
,
ruleContent
*
types
.
RuleWithContent
,
ruleDisabled
bool
,
impactedClustersCnt
uint32
)
(
types
.
RecommendationListView
,
error
)
{
recommendationListView
:=
types
.
RecommendationListView
{
}
totalRisk
,
err
:=
safeUint8
(
ruleContent
.
TotalRisk
)
if
err
!=
nil
{
return
recommendationListView
,
err
}
resolutionRisk
,
err
:=
safeUint8
(
ruleContent
.
ResolutionRisk
)
if
err
!=
nil
{
return
recommendationListView
,
err
}
impact
,
err
:=
safeUint8
(
ruleContent
.
Impact
)
if
err
!=
nil
{
return
recommendationListView
,
err
}
likelihood
,
err
:=
safeUint8
(
ruleContent
.
Likelihood
)
if
err
!=
nil
{
return
recommendationListView
,
err
}
recommendationListView
=
types
.
RecommendationListView
{
RuleID
:
ruleID
,
Description
:
ruleContent
.
Description
,
Generic
:
ruleContent
.
Generic
,
PublishDate
:
ruleContent
.
PublishDate
,
TotalRisk
:
totalRisk
,
ResolutionRisk
:
resolutionRisk
,
Impact
:
impact
,
Likelihood
:
likelihood
,
Tags
:
ruleContent
.
Tags
,
Disabled
:
ruleDisabled
,
ImpactedClustersCnt
:
impactedClustersCnt
,
}
return
recommendationListView
,
nil
}

getImpactingRecommendations retrieves a list of recommendations from aggregator based on the list of clusters

func
(
server
HTTPServer
)
getImpactingRecommendations
(
writer
http
.
ResponseWriter
,
orgID
ctypes
.
OrgID
,
userID
ctypes
.
UserID
,
clusterList
[
]
ctypes
.
ClusterName
,
)
(
ctypes
.
RecommendationImpactedClusters
,
error
,
)
{
var
aggregatorResponse
struct
{
Recommendations
ctypes
.
RecommendationImpactedClusters
`json:"recommendations"`
Status
string
`json:"status"`
}
aggregatorURL
:=
httputils
.
MakeURLToEndpoint
(
server
.
ServicesConfig
.
AggregatorBaseEndpoint
,
ira_server
.
RecommendationsListEndpoint
,
orgID
,
userID
,
)
jsonMarshalled
,
err
:=
json
.
Marshal
(
clusterList
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
"getImpactingRecommendations problem unmarshalling cluster list"
)
handleServerError
(
writer
,
err
)
return
nil
,
err
}

nosec G107

nolint:bodyclose // TODO: remove once the bodyclose library fixes this bug

	
aggregatorResp
,
err
:=
http
.
Post
(
aggregatorURL
,
JSONContentType
,
bytes
.
NewBuffer
(
jsonMarshalled
)
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
"getImpactingRecommendations problem getting response from aggregator"
)
handleServerError
(
writer
,
err
)
return
nil
,
err
}
defer
services
.
CloseResponseBody
(
aggregatorResp
)
responseBytes
,
err
:=
io
.
ReadAll
(
aggregatorResp
.
Body
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
"getImpactingRecommendations problem reading response body"
)
handleServerError
(
writer
,
err
)
return
nil
,
err
}
if
aggregatorResp
.
StatusCode
!=
http
.
StatusOK
{
err
:=
responses
.
Send
(
aggregatorResp
.
StatusCode
,
writer
,
responseBytes
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
problemSendingResponseError
)
handleServerError
(
writer
,
err
)
}
return
nil
,
err
}
err
=
json
.
Unmarshal
(
responseBytes
,
&
aggregatorResponse
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
"getImpactingRecommendations problem unmarshalling JSON response"
)
handleServerError
(
writer
,
err
)
return
nil
,
err
}
return
aggregatorResponse
.
Recommendations
,
nil
}

getClustersAndRecommendations retrieves a list of recommendations from aggregator based on the list of clusters

func
(
server
HTTPServer
)
getClustersAndRecommendations
(
writer
http
.
ResponseWriter
,
orgID
ctypes
.
OrgID
,
userID
ctypes
.
UserID
,
clusterList
[
]
ctypes
.
ClusterName
,
)
(
ctypes
.
ClusterRecommendationMap
,
error
)
{
var
aggregatorResponse
struct
{
Clusters
ctypes
.
ClusterRecommendationMap
`json:"clusters"`
Status
string
`json:"status"`
}
aggregatorURL
:=
httputils
.
MakeURLToEndpoint
(
server
.
ServicesConfig
.
AggregatorBaseEndpoint
,
ira_server
.
ClustersRecommendationsListEndpoint
,
orgID
,
userID
,
)
jsonMarshalled
,
err
:=
json
.
Marshal
(
clusterList
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
"getClustersAndRecommendations problem unmarshalling cluster list"
)
handleServerError
(
writer
,
err
)
return
nil
,
err
}

nosec G107

nolint:bodyclose // TODO: remove once the bodyclose library fixes this bug

	
aggregatorResp
,
err
:=
http
.
Post
(
aggregatorURL
,
JSONContentType
,
bytes
.
NewBuffer
(
jsonMarshalled
)
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
"getClustersAndRecommendations problem getting response from aggregator"
)
if
_
,
ok
:=
err
.
(
*
url
.
Error
)
;
ok
{
handleServerError
(
writer
,
&
AggregatorServiceUnavailableError
{
}
)
}
else
{
handleServerError
(
writer
,
err
)
}
return
nil
,
err
}
defer
services
.
CloseResponseBody
(
aggregatorResp
)
responseBytes
,
err
:=
io
.
ReadAll
(
aggregatorResp
.
Body
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
"getClustersAndRecommendations problem reading response body"
)
handleServerError
(
writer
,
err
)
return
nil
,
err
}
if
aggregatorResp
.
StatusCode
!=
http
.
StatusOK
{
err
:=
responses
.
Send
(
aggregatorResp
.
StatusCode
,
writer
,
responseBytes
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
problemSendingResponseError
)
handleServerError
(
writer
,
err
)
}
return
nil
,
err
}
err
=
json
.
Unmarshal
(
responseBytes
,
&
aggregatorResponse
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
"getClustersAndRecommendations problem unmarshalling JSON response"
)
handleServerError
(
writer
,
err
)
return
nil
,
err
}
return
aggregatorResponse
.
Clusters
,
nil
}

getContent retrieves all the static content tied with groups info

func
(
server
HTTPServer
)
getContentWithGroups
(
writer
http
.
ResponseWriter
,
request
*
http
.
Request
)
{

Generate an array of RuleContent

	
allRules
,
err
:=
content
.
GetAllContentV2
(
)
if
err
!=
nil
{
handleServerError
(
writer
,
err
)
return
}
var
rules
[
]
types
.
RuleContentV2
if
err
:=
server
.
checkInternalRulePermissions
(
request
)
;
err
!=
nil
{
for
_
,
rule
:=
range
allRules
{
if
!
content
.
IsRuleInternal
(
ctypes
.
RuleID
(
rule
.
Plugin
.
PythonModule
)
)
{
rules
=
append
(
rules
,
rule
)
}
}
}
else
{
rules
=
allRules
}

retrieve the latest groups configuration

	
ruleGroups
,
err
:=
server
.
getGroupsConfig
(
)
if
err
!=
nil
{
handleServerError
(
writer
,
err
)
return
}

prepare data structure for building response

	
responseContent
:=
make
(
map
[
string
]
interface
{
}
)
responseContent
[
"status"
]
=
OkMsg
responseContent
[
"groups"
]
=
ruleGroups
responseContent
[
"content"
]
=
rules

send response to client

	
err
=
responses
.
SendOK
(
writer
,
responseContent
)
if
err
!=
nil
{
handleServerError
(
writer
,
err
)
return
}
}

getImpactedClustersFromAggregator sends GET to aggregator with or without content depending on the list of active clusters provided by the AMS client.

func
getImpactedClustersFromAggregator
(
url
string
,
activeClusters
[
]
ctypes
.
ClusterName
,
)
(
resp
*
http
.
Response
,
err
error
)
{
if
len
(
activeClusters
)
<
1
{

nosec G107

		
resp
,
err
=
http
.
Get
(
url
)
return
}

generate JSON payload of the format "clusters": []clusters

	
var
jsonBody
[
]
byte
jsonBody
,
err
=
json
.
Marshal
(
map
[
string
]
[
]
ctypes
.
ClusterName
{
"clusters"
:
activeClusters
}
)
if
err
!=
nil
{
log
.
Err
(
err
)
.
Msg
(
"Couldn't encode list of active clusters to valid JSON, aborting"
)
return
}

GET method with list of active clusters in payload to avoid possible URL length problems

	
var
req
*
http
.
Request
req
,
err
=
http
.
NewRequest
(
http
.
MethodGet
,
url
,
bytes
.
NewBuffer
(
jsonBody
)
)
if
err
!=
nil
{
return
}
req
.
Header
.
Set
(
contentTypeHeader
,
JSONContentType
)
client
:=
&
http
.
Client
{
}
resp
,
err
=
client
.
Do
(
req
)
return
}

getImpactedClusters retrieves a list of clusters affected by the given recommendation from aggregator

func
(
server
HTTPServer
)
getImpactedClusters
(
writer
http
.
ResponseWriter
,
orgID
ctypes
.
OrgID
,
userID
ctypes
.
UserID
,
selector
ctypes
.
RuleSelector
,
activeClustersInfo
[
]
types
.
ClusterInfo
,
useAggregatorFallback
bool
,
)
(
[
]
ctypes
.
HittingClustersData
,
error
,
)
{
activeClusters
:=
types
.
GetClusterNames
(
activeClustersInfo
)
if
len
(
activeClusters
)
==
0
&&
!
useAggregatorFallback
{

empty list from AMS is valid

		
return
[
]
ctypes
.
HittingClustersData
{
}
,
nil
}
aggregatorURL
:=
httputils
.
MakeURLToEndpoint
(
server
.
ServicesConfig
.
AggregatorBaseEndpoint
,
ira_server
.
RuleClusterDetailEndpoint
,
selector
,
orgID
,
userID
,
)

nolint:bodyclose // TODO: remove once the bodyclose library fixes this bug

	
aggregatorResp
,
err
:=
getImpactedClustersFromAggregator
(
aggregatorURL
,
activeClusters
)

if http.Get fails for whatever reason

	
if
err
!=
nil
{
handleServerError
(
writer
,
err
)
return
[
]
ctypes
.
HittingClustersData
{
}
,
err
}
defer
services
.
CloseResponseBody
(
aggregatorResp
)
if
aggregatorResp
.
StatusCode
==
http
.
StatusOK
{
var
response
struct
{
Clusters
[
]
ctypes
.
HittingClustersData
`json:"clusters"`
Status
string
`json:"status"`
}
err
:=
json
.
NewDecoder
(
aggregatorResp
.
Body
)
.
Decode
(
&
response
)
if
err
!=
nil
{
return
[
]
ctypes
.
HittingClustersData
{
}
,
err
}
return
response
.
Clusters
,
nil
}
return
[
]
ctypes
.
HittingClustersData
{
}
,
nil
}

getClustersDetailForRule retrieves all the clusters affected by the recommendation By default returns only those recommendations that currently hit at least one cluster, but it's possible to show all recommendations by passing a URL parameter impacting

func
(
server
HTTPServer
)
getClustersDetailForRule
(
writer
http
.
ResponseWriter
,
request
*
http
.
Request
)
{
var
useAggregatorFallback
bool
selector
,
successful
:=
httputils
.
ReadRuleSelector
(
writer
,
request
)
if
!
successful
{
return
}
orgID
,
userID
,
err
:=
server
.
GetCurrentOrgIDUserIDFromToken
(
request
)
if
err
!=
nil
{
log
.
Err
(
err
)
.
Msg
(
orgIDTokenError
)
handleServerError
(
writer
,
err
)
return
}
recommendation
,
err
:=
content
.
GetContentForRecommendation
(
ctypes
.
RuleID
(
selector
)
)
if
err
!=
nil
{

The given rule selector does not exit

		
handleServerError
(
writer
,
err
)
return
}

Get list of clusters for given organization

	
activeClustersInfo
,
err
:=
server
.
readClusterInfoForOrgID
(
orgID
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Int
(
orgIDTag
,
int
(
orgID
)
)
.
Msg
(
"Error retrieving cluster IDs from AMS API. Will retrieve cluster list from aggregator."
)
useAggregatorFallback
=
true
}

if the recommendation is not intended to be used with OpenShift Dedicated ("managed") clusters, we must exclude them

	
if
!
recommendation
.
OSDCustomer
{
filteredClusters
:=
make
(
[
]
types
.
ClusterInfo
,
0
)
for
_
,
cluster
:=
range
activeClustersInfo
{

skipping managed clusters, because recommendation isn't managed

			
if
!
cluster
.
Managed
{
filteredClusters
=
append
(
filteredClusters
,
cluster
)
}
}
activeClustersInfo
=
filteredClusters
}

get the list of clusters affected by given rule from aggregator and

	
impactedClusters
,
err
:=
server
.
getImpactedClusters
(
writer
,
orgID
,
userID
,
selector
,
activeClustersInfo
,
useAggregatorFallback
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Int
(
orgIDTag
,
int
(
orgID
)
)
.
Str
(
userIDTag
,
string
(
userID
)
)
.
Str
(
selectorStr
,
string
(
selector
)
)
.
Msg
(
"Couldn't get impacted clusters for given rule selector"
)
handleServerError
(
writer
,
err
)
return
}
disabledClusters
,
acknowledge
,
ackFound
,
err
:=
server
.
getListOfDisabledClustersAndAck
(
orgID
,
selector
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Int
(
orgIDTag
,
int
(
orgID
)
)
.
Str
(
userIDTag
,
string
(
userID
)
)
.
Str
(
selectorStr
,
string
(
selector
)
)
.
Msg
(
"Couldn't retrieve disabled clusters or ack for given rule selector"
)
handleServerError
(
writer
,
err
)
return
}
err
=
server
.
processClustersDetailResponse
(
impactedClusters
,
disabledClusters
,
activeClustersInfo
,
acknowledge
,
ackFound
,
writer
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Int
(
orgIDTag
,
int
(
orgID
)
)
.
Str
(
userIDTag
,
string
(
userID
)
)
.
Str
(
selectorStr
,
string
(
selector
)
)
.
Msg
(
"Couldn't process response for clusters detail"
)
handleServerError
(
writer
,
err
)
return
}
}

getListOfDisabledClusters reads list of disabled clusters from aggregator

func
(
server
*
HTTPServer
)
getListOfDisabledClusters
(
orgID
types
.
OrgID
,
ruleSelector
ctypes
.
RuleSelector
,
)
(
[
]
ctypes
.
DisabledClusterInfo
,
error
)
{
var
response
struct
{
Status
string
`json:"status"`
DisabledClusters
[
]
ctypes
.
DisabledClusterInfo
`json:"clusters"`
}
ruleID
,
errorKey
,
err
:=
types
.
RuleIDWithErrorKeyFromCompositeRuleID
(
ctypes
.
RuleID
(
ruleSelector
)
)
if
err
!=
nil
{
return
nil
,
err
}

rules disabled using v1 enable/disable endpoints include '.report' in the module

	
aggregatorURL
:=
httputils
.
MakeURLToEndpoint
(
server
.
ServicesConfig
.
AggregatorBaseEndpoint
,
ira_server
.
ListOfDisabledClusters
,
ruleID
+
dotReport
,
errorKey
,
orgID
,
)

nosec G107

nolint:bodyclose // TODO: remove once the bodyclose library fixes this bug

	
resp
,
err
:=
http
.
Get
(
aggregatorURL
)
if
err
!=
nil
{
return
nil
,
err
}
defer
services
.
CloseResponseBody
(
resp
)
if
resp
.
StatusCode
!=
http
.
StatusOK
&&
resp
.
StatusCode
!=
http
.
StatusNotFound
{
err
:=
fmt
.
Errorf
(
"error reading disabled clusters from aggregator: %v"
,
resp
.
StatusCode
)
return
nil
,
err
}
err
=
json
.
NewDecoder
(
resp
.
Body
)
.
Decode
(
&
response
)
if
err
!=
nil
{
return
nil
,
err
}
return
response
.
DisabledClusters
,
nil
}

getListOfDisabledClustersAndAck reads list of disabled clusters from aggregator and gets information about rule ack

func
(
server
*
HTTPServer
)
getListOfDisabledClustersAndAck
(
orgID
types
.
OrgID
,
ruleSelector
ctypes
.
RuleSelector
,
)
(
disabledClusters
[
]
ctypes
.
DisabledClusterInfo
,
acknowledge
ctypes
.
Acknowledgement
,
ackFound
bool
,
err
error
,
)
{
disabledClusters
,
err
=
server
.
getListOfDisabledClusters
(
orgID
,
ruleSelector
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Int
(
orgIDTag
,
int
(
orgID
)
)
.
Str
(
selectorStr
,
string
(
ruleSelector
)
)
.
Msg
(
"Couldn't retrieve disabled clusters for given rule selector"
)
return
}
ruleID
,
errorKey
,
err
:=
types
.
RuleIDWithErrorKeyFromCompositeRuleID
(
ctypes
.
RuleID
(
ruleSelector
)
)
if
err
!=
nil
{
return
}
acknowledge
,
ackFound
,
err
=
server
.
readRuleDisableStatus
(
ctypes
.
Component
(
ruleID
)
,
errorKey
,
orgID
,
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Int
(
orgIDTag
,
int
(
orgID
)
)
.
Str
(
selectorStr
,
string
(
ruleSelector
)
)
.
Msg
(
"Couldn't retrieve rule acknowledge status for given rule selector"
)
return
}
return
}

processClustersDetailResponse processes responses from aggregator and AMS API and sends a response

func
(
server
*
HTTPServer
)
processClustersDetailResponse
(
impactedClusters
[
]
ctypes
.
HittingClustersData
,
disabledClusters
[
]
ctypes
.
DisabledClusterInfo
,
clusterInfo
[
]
types
.
ClusterInfo
,
acknowledge
ctypes
.
Acknowledgement
,
ruleAcked
bool
,
writer
http
.
ResponseWriter
,
)
error
{
data
:=
types
.
ClustersDetailData
{
EnabledClusters
:
make
(
[
]
ctypes
.
HittingClustersData
,
0
)
,
DisabledClusters
:
make
(
[
]
ctypes
.
DisabledClusterInfo
,
0
)
,
}

disabledMap is used to filter out the impacted clusters

	
disabledMap
:=
make
(
map
[
types
.
ClusterName
]
ctypes
.
DisabledClusterInfo
)
clusterInfoMap
:=
types
.
ClusterInfoArrayToMap
(
clusterInfo
)

filter out inactive clusters from disabled; fill in display names

	
for
_
,
disabledC
:=
range
disabledClusters
{

omit clusters that weren't retrieved from AMS API

		
if
cluster
,
found
:=
clusterInfoMap
[
disabledC
.
ClusterID
]
;
found
{
disabledC
.
ClusterName
=
cluster
.
DisplayName
disabledMap
[
disabledC
.
ClusterID
]
=
disabledC
data
.
DisabledClusters
=
append
(
data
.
DisabledClusters
,
disabledC
)
}
}
for
_
,
impactedC
:=
range
impactedClusters
{

omit disabled clusters

		
if
_
,
disabled
:=
disabledMap
[
impactedC
.
Cluster
]
;
disabled
{
continue
}
impactedC
.
Name
=
clusterInfoMap
[
impactedC
.
Cluster
]
.
DisplayName
if
ruleAcked
{
disabledAt
,
err
:=
time
.
Parse
(
time
.
RFC3339
,
acknowledge
.
CreatedAt
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Str
(
"createdAt"
,
acknowledge
.
CreatedAt
)
.
Msg
(
"error parsing time as RFC3339"
)
disabledAt
=
time
.
Time
{
}
}
disabledCluster
:=
ctypes
.
DisabledClusterInfo
{
ClusterID
:
impactedC
.
Cluster
,
ClusterName
:
impactedC
.
Name
,
DisabledAt
:
disabledAt
,
Justification
:
acknowledge
.
Justification
,
}
data
.
DisabledClusters
=
append
(
data
.
DisabledClusters
,
disabledCluster
)
}
else
{
data
.
EnabledClusters
=
append
(
data
.
EnabledClusters
,
impactedC
)
}
}
response
:=
types
.
ClustersDetailResponse
{
Status
:
OkMsg
,
Data
:
data
,
}
return
responses
.
Send
(
http
.
StatusOK
,
writer
,
response
)
}

getRequestStatusForCluster method implements endpoint that should return a status for given request ID.

func
(
server
*
HTTPServer
)
getRequestStatusForCluster
(
writer
http
.
ResponseWriter
,
request
*
http
.
Request
)
{
orgID
,
err
:=
server
.
GetCurrentOrgID
(
request
)
if
err
!=
nil
{
log
.
Error
(
)
.
Msg
(
authTokenFormatError
)
handleServerError
(
writer
,
err
)
return
}
clusterID
,
successful
:=
httputils
.
ReadClusterName
(
writer
,
request
)
if
!
successful
{

error handled by function

		
return
}
requestID
,
err
:=
readRequestID
(
writer
,
request
)
if
err
!=
nil
{

error handled by function

		
return
}

make sure we don't access server.redis when it's nil

	
if
!
server
.
checkRedisClientReadiness
(
writer
)
{

error has been handled already

		
return
}

get request ID list from Redis using SCAN command

	
requestIDsForCluster
,
err
:=
server
.
redis
.
GetRequestIDsForClusterID
(
orgID
,
clusterID
)
if
err
!=
nil
{
handleServerError
(
writer
,
err
)
return
}
if
len
(
requestIDsForCluster
)
==
0
{
err
:=
responses
.
SendNotFound
(
writer
,
RequestsForClusterNotFound
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
responseDataError
)
}
return
}

try to find the required request ID in list of requests IDs from Redis

	
var
found
bool
for
_
,
storedRequestID
:=
range
requestIDsForCluster
{
if
storedRequestID
==
requestID
{
found
=
true
break
}
}
if
!
found
{
err
:=
responses
.
SendNotFound
(
writer
,
RequestIDNotFound
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
responseDataError
)
}
return
}

prepare data structure

	
responseData
:=
map
[
string
]
interface
{
}
{
}
responseData
[
"cluster"
]
=
string
(
clusterID
)
responseData
[
"requestID"
]
=
requestID
responseData
[
"status"
]
=
StatusProcessed

send response to client

	
err
=
responses
.
SendOK
(
writer
,
responseData
)
if
err
!=
nil
{
handleServerError
(
writer
,
err
)
return
}
}

getRequestsForCluster method implements endpoint that should return a list of all request IDs and their details for given cluster

func
(
server
*
HTTPServer
)
getRequestsForCluster
(
writer
http
.
ResponseWriter
,
request
*
http
.
Request
)
{
orgID
,
err
:=
server
.
GetCurrentOrgID
(
request
)
if
err
!=
nil
{
log
.
Error
(
)
.
Msg
(
authTokenFormatError
)
handleServerError
(
writer
,
err
)
return
}
clusterID
,
successful
:=
httputils
.
ReadClusterName
(
writer
,
request
)
if
!
successful
{

error handled by function

		
return
}

make sure we don't access server.redis when it's nil

	
if
!
server
.
checkRedisClientReadiness
(
writer
)
{

error has been handled already

		
return
}

get request ID list from Redis using SCAN command

	
requestIDsForCluster
,
err
:=
server
.
redis
.
GetRequestIDsForClusterID
(
orgID
,
clusterID
)
if
err
!=
nil
{
handleServerError
(
writer
,
err
)
return
}
if
len
(
requestIDsForCluster
)
==
0
{
err
:=
responses
.
SendNotFound
(
writer
,
RequestsForClusterNotFound
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
responseDataError
)
}
return
}

get data for each request ID. Omit missing keys in case the data expired in the meantime

	
requestIDsData
,
err
:=
server
.
redis
.
GetTimestampsForRequestIDs
(
orgID
,
clusterID
,
requestIDsForCluster
,
true
)
if
err
!=
nil
{
handleServerError
(
writer
,
err
)
return
}

prepare data structure

	
responseData
:=
map
[
string
]
interface
{
}
{
}
responseData
[
"cluster"
]
=
string
(
clusterID
)
responseData
[
"requests"
]
=
requestIDsData
responseData
[
"status"
]
=
OkMsg

send response to client

	
err
=
responses
.
SendOK
(
writer
,
responseData
)
if
err
!=
nil
{
handleServerError
(
writer
,
err
)
return
}
}

getRequestsForCluster method implements endpoint that should return a list of request IDs and their details for given cluster and given list of request IDs provided in request body

func
(
server
*
HTTPServer
)
getRequestsForClusterPostVariant
(
writer
http
.
ResponseWriter
,
request
*
http
.
Request
)
{
const
logMsg
=
"getRequestsForClusterPostVariant"
orgID
,
err
:=
server
.
GetCurrentOrgID
(
request
)
if
err
!=
nil
{
log
.
Error
(
)
.
Msg
(
authTokenFormatError
)
handleServerError
(
writer
,
err
)
return
}
log
.
Debug
(
)
.
Uint32
(
orgIDTag
,
uint32
(
orgID
)
)
.
Msg
(
logMsg
)
clusterID
,
successful
:=
httputils
.
ReadClusterName
(
writer
,
request
)
if
!
successful
{

error handled by function

		
return
}
log
.
Debug
(
)
.
Str
(
"selected cluster"
,
string
(
clusterID
)
)
.
Msg
(
logMsg
)

get request ID list from request body

	
requestIDsForCluster
,
err
:=
readRequestIDList
(
writer
,
request
)
if
err
!=
nil
{

error handled by function

		
return
}
log
.
Debug
(
)
.
Uint32
(
orgIDTag
,
uint32
(
orgID
)
)
.
Str
(
"selected cluster"
,
string
(
clusterID
)
)
.
Int
(
"IDS count"
,
len
(
requestIDsForCluster
)
)
.
Msg
(
"requestIDs"
)

make sure we don't access server.redis when it's nil

	
if
!
server
.
checkRedisClientReadiness
(
writer
)
{

error has been handled already

		
return
}

get data for each request ID. Don't omit missing keys, because requester wants to know which are valid

	
requestIDsData
,
err
:=
server
.
redis
.
GetTimestampsForRequestIDs
(
orgID
,
clusterID
,
requestIDsForCluster
,
false
)
if
err
!=
nil
{
handleServerError
(
writer
,
err
)
return
}

prepare data structure

	
responseData
:=
map
[
string
]
interface
{
}
{
}
responseData
[
"cluster"
]
=
string
(
clusterID
)
responseData
[
"requests"
]
=
requestIDsData
responseData
[
"status"
]
=
OkMsg

send response to client

	
err
=
responses
.
SendOK
(
writer
,
responseData
)
if
err
!=
nil
{
handleServerError
(
writer
,
err
)
return
}
}

getReportForRequest method implements endpoint that should return simplified result for given request ID

func
(
server
*
HTTPServer
)
getReportForRequest
(
writer
http
.
ResponseWriter
,
request
*
http
.
Request
)
{
orgID
,
err
:=
server
.
GetCurrentOrgID
(
request
)
if
err
!=
nil
{
log
.
Error
(
)
.
Msg
(
authTokenFormatError
)
handleServerError
(
writer
,
err
)
return
}
clusterID
,
successful
:=
httputils
.
ReadClusterName
(
writer
,
request
)
if
!
successful
{

error handled by function

		
return
}
requestID
,
err
:=
readRequestID
(
writer
,
request
)
if
err
!=
nil
{

error handled by function

		
return
}

make sure we don't access server.redis when it's nil

	
if
!
server
.
checkRedisClientReadiness
(
writer
)
{

error has been handled already

		
return
}

get rule hits from Redis

	
ruleHits
,
err
:=
server
.
redis
.
GetRuleHitsForRequest
(
orgID
,
clusterID
,
requestID
)
if
err
!=
nil
{
handleServerError
(
writer
,
err
)
return
}

get a map of acknowledged rules

	
ackedRulesMap
,
err
:=
server
.
getRuleAcksMap
(
orgID
)
if
err
!=
nil
{
handleServerError
(
writer
,
err
)
return
}

retrieve user disabled rules for given cluster

	
disabledRulesForCluster
,
err
:=
server
.
getDisabledRulesForClusterMap
(
writer
,
orgID
,
clusterID
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
"problem getting user disabled rules for cluster"
)

server error has been handled already

		
return
}
filteredRuleHits
:=
filterRulesGetContent
(
ruleHits
,
ackedRulesMap
,
disabledRulesForCluster
)

prepare response

	
responseData
:=
map
[
string
]
interface
{
}
{
}
responseData
[
"cluster"
]
=
string
(
clusterID
)
responseData
[
"requestID"
]
=
requestID
responseData
[
"status"
]
=
StatusProcessed
responseData
[
"report"
]
=
filteredRuleHits

send response to client

	
err
=
responses
.
SendOK
(
writer
,
responseData
)
if
err
!=
nil
{
handleServerError
(
writer
,
err
)
return
}
}
func
filterRulesGetContent
(
ruleHits
[
]
types
.
RuleID
,
ackedRules
map
[
ctypes
.
RuleID
]
bool
,
disabledRulesForCluster
map
[
ctypes
.
RuleID
]
bool
,
)
[
]
types
.
SimplifiedRuleHit
{

initialize the return value so that it's not nil (and in API response null)

	
filteredRuleHits
:=
[
]
types
.
SimplifiedRuleHit
{
}
for
_
,
ruleID
:=
range
ruleHits
{

skip acked rule

		
if
_
,
found
:=
ackedRules
[
ruleID
]
;
found
{
continue
}

skip single disabled rules for given cluster

		
if
_
,
found
:=
disabledRulesForCluster
[
ruleID
]
;
found
{
continue
}
ruleContent
,
err
:=
content
.
GetContentForRecommendation
(
ruleID
)
if
err
!=
nil
{

rule content not found, log and skip as in other endpoints

			
log
.
Error
(
)
.
Err
(
err
)
.
Interface
(
ruleIDStr
,
ruleID
)
.
Msg
(
"error retrieving rule content for rule"
)
continue
}
ruleID
,
errorKey
,
err
:=
types
.
RuleIDWithErrorKeyFromCompositeRuleID
(
ruleID
)
if
err
!=
nil
{
log
.
Error
(
)
.
Msg
(
"error getting rule module and error key from composite rule ID."
)
}

fill in data from rule content

		
simplifiedRuleHit
:=
types
.
SimplifiedRuleHit
{
RuleFQDN
:
string
(
ruleID
)
,
ErrorKey
:
string
(
errorKey
)
,
Description
:
ruleContent
.
Generic
,
TotalRisk
:
ruleContent
.
TotalRisk
,
}
filteredRuleHits
=
append
(
filteredRuleHits
,
simplifiedRuleHit
)
}
return
filteredRuleHits
}
func
(
server
HTTPServer
)
getDisabledRulesForClusterMap
(
writer
http
.
ResponseWriter
,
orgID
types
.
OrgID
,
clusterID
types
.
ClusterName
,
)
(
disabledRules
map
[
types
.
RuleID
]
bool
,
err
error
,
)
{
disabledRules
=
make
(
map
[
types
.
RuleID
]
bool
)

use existing endpoint accepting list of clusters

	
listOfDisabledRules
,
err
:=
server
.
readListOfDisabledRulesForClusters
(
writer
,
orgID
,
[
]
types
.
ClusterName
{
clusterID
}
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
"error reading disabled rules from aggregator"
)
handleServerError
(
writer
,
err
)
return
}
for
_
,
disabledRule
:=
range
listOfDisabledRules
{
compositeRuleID
,
err
:=
generateCompositeRuleIDFromDisabled
(
disabledRule
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
"error generating composite rule ID"
)
continue
}
disabledRules
[
compositeRuleID
]
=
true
}
return
}

checkRedisClientReadiness method checks if Redis client has been initialized

func
(
server
*
HTTPServer
)
checkRedisClientReadiness
(
writer
http
.
ResponseWriter
)
bool
{
if
server
.
redis
==
nil
{
handleServerError
(
writer
,
errors
.
New
(
RedisNotInitializedErrorMessage
)
)
return
false
}
return
true
}

getDVONamespaceList returns a list of all DVO namespaces to which an account has access.

func
(
server
*
HTTPServer
)
getDVONamespaceList
(
writer
http
.
ResponseWriter
,
request
*
http
.
Request
)
{
tStart
:=
time
.
Now
(
)
orgID
,
err
:=
server
.
GetCurrentOrgID
(
request
)
if
err
!=
nil
{
log
.
Error
(
)
.
Msg
(
authTokenFormatError
)
handleServerError
(
writer
,
err
)
return
}

get active clusters info from AMS API

	
activeClustersInfo
,
err
:=
server
.
readClusterInfoForOrgID
(
orgID
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Int
(
orgIDTag
,
int
(
orgID
)
)
.
Msg
(
clusterListError
)
handleServerError
(
writer
,
err
)
return
}
clusterInfoMap
:=
types
.
ClusterInfoArrayToMap
(
activeClustersInfo
)
log
.
Info
(
)
.
Int
(
orgIDTag
,
int
(
orgID
)
)
.
Msgf
(
"getDVONamespaceList took %v to get %d clusters from AMS API"
,
time
.
Since
(
tStart
)
,
len
(
activeClustersInfo
)
)

get workloads for clusters

	
workloads
,
err
:=
server
.
getWorkloadsForOrganization
(
orgID
,
writer
,
activeClustersInfo
)
if
err
!=
nil
{
handleServerError
(
writer
,
err
)
return
}
log
.
Info
(
)
.
Int
(
orgIDTag
,
int
(
orgID
)
)
.
Msgf
(
"getDVONamespaceList took %v to get %d workloads from aggregator"
,
time
.
Since
(
tStart
)
,
len
(
workloads
)
)
workloadsProcessed
,
err
:=
processWorkloadsRecommendations
(
clusterInfoMap
,
workloads
)
if
err
!=
nil
{
handleServerError
(
writer
,
err
)
return
}

prepare response

	
responseData
:=
map
[
string
]
interface
{
}
{
}
responseData
[
"status"
]
=
OkMsg
responseData
[
"workloads"
]
=
workloadsProcessed
log
.
Info
(
)
.
Int
(
orgIDTag
,
int
(
orgID
)
)
.
Msgf
(
"getDVONamespaceList took %v to process response into %d results"
,
time
.
Since
(
tStart
)
,
len
(
workloadsProcessed
)
)

send response to client

	
err
=
responses
.
SendOK
(
writer
,
responseData
)
if
err
!=
nil
{
handleServerError
(
writer
,
err
)
return
}
}

processWorkloadsRecommendations filter out inactive clusters; calculate aggregations by severity

func
processWorkloadsRecommendations
(
clusterInfoMap
map
[
ctypes
.
ClusterName
]
types
.
ClusterInfo
,
workloadsForCluster
[
]
types
.
WorkloadsForNamespace
,
)
(
workloads
[
]
types
.
Workload
,
err
error
,
)
{
workloads
=
make
(
[
]
types
.
Workload
,
0
)
recommendationSeverities
,
uniqueSeverities
,
err
:=
content
.
GetExternalRuleSeverities
(
)
if
err
!=
nil
{
return
}
for
_
,
w
:=
range
workloadsForCluster
{

fill in display name

		
if
clusterInfo
,
found
:=
clusterInfoMap
[
ctypes
.
ClusterName
(
w
.
Cluster
.
UUID
)
]
;
found
{
w
.
Cluster
.
DisplayName
=
clusterInfo
.
DisplayName
}
else
{

cluster is not active, omitting

			
continue
}

fill in all unique severities

		
hitsBySeverity
:=
make
(
map
[
int
]
int
,
0
)
for
_
,
severity
:=
range
uniqueSeverities
{
hitsBySeverity
[
severity
]
=
0
}
w
.
Metadata
.
HitsBySeverity
=
hitsBySeverity

calculate hits by severity and highest severity across all recommendations

		
for
recommendation
,
hitCount
:=
range
w
.
RecommendationsHitCount
{
if
severity
,
found
:=
recommendationSeverities
[
ctypes
.
RuleID
(
recommendation
)
]
;
found
{
w
.
Metadata
.
HitsBySeverity
[
severity
]
+=
hitCount
if
severity
>
w
.
Metadata
.
HighestSeverity
{
w
.
Metadata
.
HighestSeverity
=
severity
}
}
else
{
log
.
Info
(
)
.
Msgf
(
"recommendation ID [%v] not found in content. Skipping."
,
recommendation
)
}
}
workloads
=
append
(
workloads
,
types
.
Workload
{
Cluster
:
w
.
Cluster
,
Namespace
:
w
.
Namespace
,
Metadata
:
w
.
Metadata
,
}
)
}
return
}

getDVONamespacesForCluster returns a DVO workload recommendations for a single namespace within a cluster

func
(
server
*
HTTPServer
)
getDVONamespacesForCluster
(
writer
http
.
ResponseWriter
,
request
*
http
.
Request
)
{
orgID
,
err
:=
server
.
GetCurrentOrgID
(
request
)
if
err
!=
nil
{
log
.
Error
(
)
.
Msg
(
authTokenFormatError
)
handleServerError
(
writer
,
err
)
return
}
clusterID
,
successful
:=
httputils
.
ReadClusterName
(
writer
,
request
)

Error message handled by function

	
if
!
successful
{
return
}
namespace
,
err
:=
readNamespace
(
writer
,
request
)
if
err
!=
nil
{
return
}

get cluster info from AMS API

	
if
server
.
amsClient
==
nil
&&
!
server
.
Config
.
UseOrgClustersFallback
{
log
.
Error
(
)
.
Msg
(
"unable to retrieve info about cluster"
)
handleServerError
(
writer
,
&
AMSAPIUnavailableError
{
}
)
return
}
clusterInfo
,
err
:=
server
.
amsClient
.
GetSingleClusterInfoForOrganization
(
orgID
,
clusterID
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Int
(
orgIDTag
,
int
(
orgID
)
)
.
Msg
(
clusterListError
)
handleServerError
(
writer
,
err
)
return
}

get namespace data from aggregator

	
workloads
,
err
:=
server
.
getWorkloadsForCluster
(
orgID
,
clusterID
,
namespace
)
if
err
!=
nil
{
switch
err
.
(
type
)
{
case
*
json
.
SyntaxError
:
msg
:=
"aggregator provided a wrong response"
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
msg
)
handleServerError
(
writer
,
errors
.
New
(
msg
)
)
return
case
*
url
.
Error
:
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
"aggregator is not responding"
)
handleServerError
(
writer
,
&
AggregatorServiceUnavailableError
{
}
)
return
default
:
handleServerError
(
writer
,
err
)
return
}
}
workloadsProcessed
,
err
:=
fillInWorkloadsData
(
clusterInfo
,
workloads
)
if
err
!=
nil
{
msg
:=
"unable to fill in data from content-service"
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
msg
)
handleServerError
(
writer
,
errors
.
New
(
msg
)
)
return
}

prepare response

	
responseData
:=
map
[
string
]
interface
{
}
{
}
responseData
[
"status"
]
=
OkMsg
responseData
[
"cluster"
]
=
workloadsProcessed
.
Cluster
responseData
[
"namespace"
]
=
workloadsProcessed
.
Namespace
responseData
[
"metadata"
]
=
workloadsProcessed
.
Metadata
responseData
[
"recommendations"
]
=
workloadsProcessed
.
Recommendations

send response to client

	
err
=
responses
.
SendOK
(
writer
,
responseData
)
if
err
!=
nil
{
handleServerError
(
writer
,
err
)
return
}
}

fillInWorkloadsData fills in data acquired from content-service

func
fillInWorkloadsData
(
clusterInfo
types
.
ClusterInfo
,
workloadsForCluster
types
.
WorkloadsForCluster
,
)
(
workloads
types
.
WorkloadsForCluster
,
err
error
,
)
{
recommendationSeverities
,
uniqueSeverities
,
err
:=
content
.
GetExternalRuleSeverities
(
)
if
err
!=
nil
{
return
}

fill in display name

	
workloadsForCluster
.
Cluster
.
DisplayName
=
clusterInfo
.
DisplayName

fill in all unique severities

	
hitsBySeverity
:=
make
(
map
[
int
]
int
,
len
(
uniqueSeverities
)
)
for
_
,
severity
:=
range
uniqueSeverities
{
hitsBySeverity
[
severity
]
=
0
}
workloadsForCluster
.
Metadata
.
HitsBySeverity
=
hitsBySeverity
recommendations
:=
[
]
types
.
DVORecommendation
{
}

fill in severities and other data from rule content

	
for
i
:=
range
workloadsForCluster
.
Recommendations
{
recommendation
:=
&
workloadsForCluster
.
Recommendations
[
i
]
if
severity
,
found
:=
recommendationSeverities
[
ctypes
.
RuleID
(
recommendation
.
Check
)
]
;
found
{
workloadsForCluster
.
Metadata
.
HitsBySeverity
[
severity
]
+=
len
(
recommendation
.
Objects
)
if
severity
>
workloadsForCluster
.
Metadata
.
HighestSeverity
{
workloadsForCluster
.
Metadata
.
HighestSeverity
=
severity
}
}
err
=
fillDVORecommendationRuleContent
(
recommendation
)
if
err
!=
nil
{
return
workloads
,
err
}
recommendations
=
append
(
recommendations
,
*
recommendation
)
}
workloads
=
types
.
WorkloadsForCluster
{
Cluster
:
workloadsForCluster
.
Cluster
,
Namespace
:
workloadsForCluster
.
Namespace
,
Metadata
:
workloadsForCluster
.
Metadata
,
Recommendations
:
recommendations
,
}
return
}
func
fillDVORecommendationRuleContent
(
recommendation
*
types
.
DVORecommendation
)
error
{
ruleContent
,
err
:=
content
.
GetContentForRecommendation
(
ctypes
.
RuleID
(
recommendation
.
Check
)
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Interface
(
"recommendation.Check"
,
recommendation
.
Check
)
.
Msg
(
ruleContentError
)
return
err
}

fill DVORecommendation with data from content service

	
recommendation
.
Details
=
ruleContent
.
Description
recommendation
.
Resolution
=
ruleContent
.
Resolution
recommendation
.
MoreInfo
=
ruleContent
.
MoreInfo
recommendation
.
Reason
=
ruleContent
.
Reason
recommendation
.
TotalRisk
=
ruleContent
.
TotalRisk
recommendation
.
Modified
=
ruleContent
.
PublishDate
.
UTC
(
)
.
Format
(
time
.
RFC3339
)
return
nil
}