dvo_handlers.go

/* Copyright © 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
import
(
"encoding/json"
"errors"
"fmt"
"net/http"
"regexp"
"strings"
"time"
"github.com/RedHatInsights/insights-operator-utils/generators"
httputils
"github.com/RedHatInsights/insights-operator-utils/http"
"github.com/RedHatInsights/insights-operator-utils/responses"
"github.com/RedHatInsights/insights-results-aggregator/types"
"github.com/rs/zerolog/log"
)
const
(
namespaceIDParam
=
"namespace"
workloadsParam
=
"workloads"
)

WorkloadsForCluster structure represents workload for one selected cluster

type
WorkloadsForCluster
struct
{
Status
string
`json:"status"`
Cluster
types
.
Cluster
`json:"cluster"`
Namespace
types
.
Namespace
`json:"namespace"`
Metadata
types
.
DVOMetadata
`json:"metadata"`
Recommendations
[
]
DVORecommendation
`json:"recommendations"`
}

DVORecommendation structure represents one DVO-related recommendation

type
DVORecommendation
struct
{
Check
string
`json:"check"`
Details
string
`json:"details"`
Resolution
string
`json:"resolution"`
Modified
string
`json:"modified"`
MoreInfo
string
`json:"more_info"`
TemplateData
map
[
string
]
interface
{
}
`json:"extra_data"`
Objects
[
]
DVOObject
`json:"objects"`
}

DVOObject structure

type
DVOObject
struct
{
Kind
string
`json:"kind"`
UID
string
`json:"uid"`
DisplayName
string
`json:"display_name"`
}

readNamespace retrieves namespace UUID from request if it's not possible, it writes http error to the writer and returns error

func
readNamespace
(
writer
http
.
ResponseWriter
,
request
*
http
.
Request
)
(
namespace
string
,
err
error
,
)
{
namespaceID
,
err
:=
httputils
.
GetRouterParam
(
request
,
namespaceIDParam
)
if
err
!=
nil
{
handleServerError
(
writer
,
err
)
return
}
validatedNamespaceID
,
err
:=
validateNamespaceID
(
namespaceID
)
if
err
!=
nil
{
err
=
&
RouterParsingError
{
ParamName
:
namespaceIDParam
,
ParamValue
:
namespaceID
,
ErrString
:
err
.
Error
(
)
,
}
handleServerError
(
writer
,
err
)
return
}
return
validatedNamespaceID
,
nil
}
func
validateNamespaceID
(
namespace
string
)
(
string
,
error
)
{
IDValidator
:=
regexp
.
MustCompile
(
`^.{1,256}$`
)
if
!
IDValidator
.
MatchString
(
namespace
)
{
message
:=
fmt
.
Sprintf
(
"invalid namespace ID: '%s'"
,
namespace
)
err
:=
errors
.
New
(
message
)
log
.
Warn
(
)
.
Err
(
err
)
.
Msg
(
message
)
return
""
,
err
}
return
namespace
,
nil
}

getWorkloads retrieves all namespaces and workloads for given organization

func
(
server
*
HTTPServer
)
getWorkloads
(
writer
http
.
ResponseWriter
,
request
*
http
.
Request
)
{
tStart
:=
time
.
Now
(
)

extract org_id from URL

	
orgID
,
ok
:=
readOrgID
(
writer
,
request
)
if
!
ok
{

everything has been handled

		
return
}
log
.
Debug
(
)
.
Int
(
orgIDStr
,
int
(
orgID
)
)
.
Msg
(
"getWorkloads"
)
var
clusterMap
map
[
types
.
ClusterName
]
struct
{
}
if
request
.
Method
==
http
.
MethodPost
{

try to read map of cluster IDs from request body

		
var
successful
bool
clusterMap
,
successful
=
ReadClusterMapFromBody
(
writer
,
request
)
if
!
successful
{

wrong state has been handled already

			
return
}

skip storage part if there are no active clusters (the results would be filtered out)

		
if
len
(
clusterMap
)
==
0
{
err
:=
responses
.
SendOK
(
writer
,
responses
.
BuildOkResponseWithData
(
workloadsParam
,
[
]
types
.
WorkloadsForNamespace
{
}
)
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
responseDataError
)
}
return
}
}
workloads
,
err
:=
server
.
StorageDvo
.
ReadWorkloadsForOrganization
(
orgID
,
clusterMap
,
request
.
Method
==
http
.
MethodPost
)
if
err
!=
nil
{
log
.
Warn
(
)
.
Err
(
err
)
.
Msg
(
"Errors retrieving DVO workload recommendations from storage"
)
handleServerError
(
writer
,
err
)
return
}
err
=
responses
.
SendOK
(
writer
,
responses
.
BuildOkResponseWithData
(
workloadsParam
,
workloads
)
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
responseDataError
)
}
log
.
Info
(
)
.
Uint32
(
orgIDStr
,
uint32
(
orgID
)
)
.
Msgf
(
"getWorkloads took %s overall"
,
time
.
Since
(
tStart
)
,
)
}

getWorkloadsForNamespace retrieves data about a single namespace within a cluster

func
(
server
*
HTTPServer
)
getWorkloadsForNamespace
(
writer
http
.
ResponseWriter
,
request
*
http
.
Request
)
{
tStart
:=
time
.
Now
(
)
orgID
,
ok
:=
readOrgID
(
writer
,
request
)
if
!
ok
{

everything has been handled

		
return
}
clusterName
,
successful
:=
readClusterName
(
writer
,
request
)
if
!
successful
{

everything has been handled already

		
return
}
namespaceID
,
err
:=
readNamespace
(
writer
,
request
)
if
err
!=
nil
{
return
}
log
.
Debug
(
)
.
Int
(
orgIDStr
,
int
(
orgID
)
)
.
Str
(
"namespaceID"
,
namespaceID
)
.
Msgf
(
"getWorkloadsForNamespace cluster %v"
,
clusterName
)
workload
,
err
:=
server
.
StorageDvo
.
ReadWorkloadsForClusterAndNamespace
(
orgID
,
clusterName
,
namespaceID
)
if
err
!=
nil
{

err received at this point can be either TableNotFoundError (500) or ItemNotFoundError (404)

		
logLevel
:=
log
.
Error
errMsg
:=
"Errors retrieving DVO workload recommendations from storage"
if
errors
.
Is
(
err
,
&
types
.
ItemNotFoundError
{
}
)
{

If the item is not found, we shouldn't treat it as an error

			
logLevel
=
log
.
Warn
errMsg
=
"DVO workload recommendations not found in storage"
}
logLevel
(
)
.
Int
(
orgIDStr
,
int
(
orgID
)
)
.
Str
(
"namespaceID"
,
namespaceID
)
.
Msg
(
errMsg
)
handleServerError
(
writer
,
err
)
return
}
processedWorkload
:=
server
.
ProcessSingleDVONamespace
(
workload
)
log
.
Info
(
)
.
Uint32
(
orgIDStr
,
uint32
(
orgID
)
)
.
Msgf
(
"getWorkloadsForNamespace took %s"
,
time
.
Since
(
tStart
)
,
)
err
=
responses
.
SendOK
(
writer
,
responses
.
BuildOkResponseWithData
(
workloadsParam
,
processedWorkload
)
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Msg
(
responseDataError
)
}
}

ProcessSingleDVONamespace processes a report, filters out mismatching namespaces, returns processed results

func
(
server
*
HTTPServer
)
ProcessSingleDVONamespace
(
workload
types
.
DVOReport
)
(
processedWorkloads
WorkloadsForCluster
,
)
{
firstBytesStr
:=
"first bytes"
processedWorkloads
=
WorkloadsForCluster
{
Cluster
:
types
.
Cluster
{
UUID
:
workload
.
ClusterID
,
}
,
Namespace
:
types
.
Namespace
{
UUID
:
workload
.
NamespaceID
,
Name
:
workload
.
NamespaceName
,
}
,
Metadata
:
types
.
DVOMetadata
{
Recommendations
:
workload
.
Recommendations
,
Objects
:
workload
.
Objects
,
ReportedAt
:
string
(
workload
.
ReportedAt
)
,
LastCheckedAt
:
string
(
workload
.
LastCheckedAt
)
,
}
,
Recommendations
:
[
]
DVORecommendation
{
}
,
}
var
report
string
switch
string
(
[
]
rune
(
workload
.
Report
)
[
:
1
]
)
{
case
`"`
:

we're dealing with a quoted "{\"system\":{}}" string unmarshalling into a string first before unmarshalling into a struct will remove the leading/trailing quotes and also take care of the escaped \" quotes and replaces them with valid ", producing a valid JSON

		
err
:=
json
.
Unmarshal
(
json
.
RawMessage
(
workload
.
Report
)
,
&
report
)
if
err
!=
nil
{
log
.
Error
(
)
.
Err
(
err
)
.
Str
(
firstBytesStr
,
string
(
[
]
rune
(
workload
.
Report
)
[
:
100
]
)
)
.
Msg
(
"report has unknown structure"
)
}
case
`{`
:

we're dealing with either a valid JSON {"system":{}} or a string with escaped quotes {\"system\":{}}. Stripping escape chars \ if any, produces a valid JSON

		
report
=
strings
.
Replace
(
workload
.
Report
,
`\`
,
""
,
-
1
)
default
:
log
.
Error
(
)
.
Str
(
firstBytesStr
,
string
(
[
]
rune
(
workload
.
Report
)
[
:
100
]
)
)
.
Msg
(
"report has unknown structure"
)
return
}
var
dvoReport
types
.
DVOMetrics
err
:=
json
.
Unmarshal
(
[
]
byte
(
report
)
,
&
dvoReport
)
if
err
!=
nil
{
log
.
Error
(
)
.
Str
(
firstBytesStr
,
string
(
[
]
rune
(
report
)
[
:
100
]
)
)
.
Msg
(
"error unmarshalling full report"
)
log
.
Info
(
)
.
Msgf
(
"report without escape %v"
,
string
(
[
]
rune
(
workload
.
Report
)
[
:
100
]
)
)
return
}
for
_
,
recommendation
:=
range
dvoReport
.
WorkloadRecommendations
{
filteredObjects
:=
make
(
[
]
DVOObject
,
0
)
for
i
:=
range
recommendation
.
Workloads
{
object
:=
&
recommendation
.
Workloads
[
i
]

filter out other namespaces

			
if
object
.
NamespaceUID
!=
processedWorkloads
.
Namespace
.
UUID
{
continue
}
filteredObjects
=
append
(
filteredObjects
,
DVOObject
{
Kind
:
object
.
Kind
,
UID
:
object
.
UID
,
DisplayName
:
object
.
Name
,
}
)
}

because the whole report contains a list of recommendations and each rec. contains a list of objects + namespaces, it can happen that upon filtering the objects to get rid of namespaces that weren't requested, we can end up with 0 hitting objects in that namespace

		
if
len
(
filteredObjects
)
==
0
{
continue
}

recommendation.ResponseID doesn't contain the full rule ID, so smart-proxy was unable to retrieve content, we need to build it

		
compositeRuleID
,
err
:=
generators
.
GenerateCompositeRuleID
(

for some unknown reason, there's a .recommendation suffix for each rule hit instead of the usual .report

			
types
.
RuleFQDN
(
strings
.
TrimSuffix
(
recommendation
.
Component
,
types
.
WorkloadRecommendationSuffix
)
)
,
types
.
ErrorKey
(
recommendation
.
Key
)
,
)
if
err
!=
nil
{
log
.
Warn
(
)
.
Err
(
err
)
.
Msg
(
"error generating composite rule ID for rule"
)
continue
}
processedWorkloads
.
Recommendations
=
append
(
processedWorkloads
.
Recommendations
,
DVORecommendation
{
Check
:
string
(
compositeRuleID
)
,
Objects
:
filteredObjects
,
TemplateData
:
recommendation
.
Details
,
}
)
}
return
}