Skip to content

Commit

Permalink
Refactor Multi Node Analyzers (#1646)
Browse files Browse the repository at this point in the history
* initial refactor of host os analyzer

* refactor remote collect analysis

---------

Signed-off-by: Evans Mungai <[email protected]>
Co-authored-by: Gerard Nguyen <[email protected]>
Co-authored-by: Evans Mungai <[email protected]>
  • Loading branch information
3 people authored Oct 21, 2024
1 parent 9c24ab6 commit b88bc8d
Show file tree
Hide file tree
Showing 11 changed files with 1,128 additions and 765 deletions.
58 changes: 58 additions & 0 deletions pkg/analyze/collected_contents.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package analyzer

import (
"encoding/json"
"fmt"

"github.com/pkg/errors"
"github.com/replicatedhq/troubleshoot/pkg/constants"
)

type collectedContent struct {
NodeName string
Data collectorData
}

type collectorData interface{}

type nodeNames struct {
Nodes []string `json:"nodes"`
}

func retrieveCollectedContents(
getCollectedFileContents func(string) ([]byte, error),
localPath string, remoteNodeBaseDir string, remoteFileName string,
) ([]collectedContent, error) {
var collectedContents []collectedContent

// Try to retrieve local data first
if contents, err := getCollectedFileContents(localPath); err == nil {
collectedContents = append(collectedContents, collectedContent{NodeName: "", Data: contents})
// Return immediately if local content is available
return collectedContents, nil
}

// Local data not available, move to remote collection
nodeListContents, err := getCollectedFileContents(constants.NODE_LIST_FILE)
if err != nil {
return nil, errors.Wrap(err, "failed to get node list")
}

var nodeNames nodeNames
if err := json.Unmarshal(nodeListContents, &nodeNames); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal node names")
}

// Collect data for each node
for _, node := range nodeNames.Nodes {
nodeFilePath := fmt.Sprintf("%s/%s/%s", remoteNodeBaseDir, node, remoteFileName)
nodeContents, err := getCollectedFileContents(nodeFilePath)
if err != nil {
return nil, errors.Wrapf(err, "failed to retrieve content for node %s", node)
}

collectedContents = append(collectedContents, collectedContent{NodeName: node, Data: nodeContents})
}

return collectedContents, nil
}
138 changes: 138 additions & 0 deletions pkg/analyze/collected_contents_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package analyzer

import (
"encoding/json"
"testing"

"github.com/replicatedhq/troubleshoot/pkg/constants"
"github.com/replicatedhq/troubleshoot/pkg/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestRetrieveCollectedContents(t *testing.T) {
tests := []struct {
name string
getCollectedFileContents func(string) ([]byte, error) // Mock function
localPath string
remoteNodeBaseDir string
remoteFileName string
expectedResult []collectedContent
expectedError string
}{
{
name: "successfully retrieve local content",
getCollectedFileContents: func(path string) ([]byte, error) {
if path == "localPath" {
return []byte("localContent"), nil
}
return nil, &types.NotFoundError{Name: path}
},
localPath: "localPath",
remoteNodeBaseDir: "remoteBaseDir",
remoteFileName: "remoteFileName",
expectedResult: []collectedContent{
{
NodeName: "",
Data: []byte("localContent"),
},
},
expectedError: "",
},
{
name: "local content not found, retrieve remote node content successfully",
getCollectedFileContents: func(path string) ([]byte, error) {
if path == constants.NODE_LIST_FILE {
nodeNames := nodeNames{Nodes: []string{"node1", "node2"}}
return json.Marshal(nodeNames)
}
if path == "remoteBaseDir/node1/remoteFileName" {
return []byte("remoteContent1"), nil
}
if path == "remoteBaseDir/node2/remoteFileName" {
return []byte("remoteContent2"), nil
}
return nil, &types.NotFoundError{Name: path}
},
localPath: "localPath",
remoteNodeBaseDir: "remoteBaseDir",
remoteFileName: "remoteFileName",
expectedResult: []collectedContent{
{
NodeName: "node1",
Data: []byte("remoteContent1"),
},
{
NodeName: "node2",
Data: []byte("remoteContent2"),
},
},
expectedError: "",
},
{
name: "fail to retrieve local content and node list",
getCollectedFileContents: func(path string) ([]byte, error) {
return nil, &types.NotFoundError{Name: path}
},
localPath: "localPath",
remoteNodeBaseDir: "remoteBaseDir",
remoteFileName: "remoteFileName",
expectedResult: nil,
expectedError: "failed to get node list",
},
{
name: "fail to retrieve content for one of the nodes",
getCollectedFileContents: func(path string) ([]byte, error) {
if path == constants.NODE_LIST_FILE {
nodeNames := nodeNames{Nodes: []string{"node1", "node2"}}
return json.Marshal(nodeNames)
}
if path == "remoteBaseDir/node1/remoteFileName" {
return []byte("remoteContent1"), nil
}
if path == "remoteBaseDir/node2/remoteFileName" {
return nil, &types.NotFoundError{Name: path}
}
return nil, &types.NotFoundError{Name: path}
},
localPath: "localPath",
remoteNodeBaseDir: "remoteBaseDir",
remoteFileName: "remoteFileName",
expectedResult: nil,
expectedError: "failed to retrieve content for node node2",
},
{
name: "fail to unmarshal node list",
getCollectedFileContents: func(path string) ([]byte, error) {
if path == constants.NODE_LIST_FILE {
return []byte("invalidJSON"), nil
}
return nil, &types.NotFoundError{Name: path}
},
localPath: "localPath",
remoteNodeBaseDir: "remoteBaseDir",
remoteFileName: "remoteFileName",
expectedResult: nil,
expectedError: "failed to unmarshal node names",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := retrieveCollectedContents(
test.getCollectedFileContents,
test.localPath,
test.remoteNodeBaseDir,
test.remoteFileName,
)

if test.expectedError != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), test.expectedError)
} else {
require.NoError(t, err)
assert.Equal(t, test.expectedResult, result)
}
})
}
}
106 changes: 105 additions & 1 deletion pkg/analyze/host_analyzer.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package analyzer

import troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
import (
"fmt"

"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
)

type HostAnalyzer interface {
Title() string
Expand Down Expand Up @@ -83,3 +88,102 @@ func (c *resultCollector) get(title string) []*AnalyzeResult {
}
return []*AnalyzeResult{{Title: title, IsWarn: true, Message: "no results"}}
}

func analyzeHostCollectorResults(collectedContent []collectedContent, outcomes []*troubleshootv1beta2.Outcome, checkCondition func(string, collectorData) (bool, error), title string) ([]*AnalyzeResult, error) {
var results []*AnalyzeResult
for _, content := range collectedContent {
currentTitle := title
if content.NodeName != "" {
currentTitle = fmt.Sprintf("%s - Node %s", title, content.NodeName)
}

analyzeResult, err := evaluateOutcomes(outcomes, checkCondition, content.Data, currentTitle)
if err != nil {
return nil, errors.Wrap(err, "failed to evaluate outcomes")
}
if analyzeResult != nil {
results = append(results, analyzeResult...)
}
}
return results, nil
}

func evaluateOutcomes(outcomes []*troubleshootv1beta2.Outcome, checkCondition func(string, collectorData) (bool, error), data collectorData, title string) ([]*AnalyzeResult, error) {
var results []*AnalyzeResult

for _, outcome := range outcomes {
result := AnalyzeResult{
Title: title,
}

switch {
case outcome.Fail != nil:
if outcome.Fail.When == "" {
result.IsFail = true
result.Message = outcome.Fail.Message
result.URI = outcome.Fail.URI
results = append(results, &result)
return results, nil
}

isMatch, err := checkCondition(outcome.Fail.When, data)
if err != nil {
return []*AnalyzeResult{&result}, errors.Wrapf(err, "failed to compare %s", outcome.Fail.When)
}

if isMatch {
result.IsFail = true
result.Message = outcome.Fail.Message
result.URI = outcome.Fail.URI
results = append(results, &result)
return results, nil
}

case outcome.Warn != nil:
if outcome.Warn.When == "" {
result.IsWarn = true
result.Message = outcome.Warn.Message
result.URI = outcome.Warn.URI
results = append(results, &result)
return results, nil
}

isMatch, err := checkCondition(outcome.Warn.When, data)
if err != nil {
return []*AnalyzeResult{&result}, errors.Wrapf(err, "failed to compare %s", outcome.Warn.When)
}

if isMatch {
result.IsWarn = true
result.Message = outcome.Warn.Message
result.URI = outcome.Warn.URI
results = append(results, &result)
return results, nil
}

case outcome.Pass != nil:
if outcome.Pass.When == "" {
result.IsPass = true
result.Message = outcome.Pass.Message
result.URI = outcome.Pass.URI
results = append(results, &result)
return results, nil
}

isMatch, err := checkCondition(outcome.Pass.When, data)
if err != nil {
return []*AnalyzeResult{&result}, errors.Wrapf(err, "failed to compare %s", outcome.Pass.When)
}

if isMatch {
result.IsPass = true
result.Message = outcome.Pass.Message
result.URI = outcome.Pass.URI
results = append(results, &result)
return results, nil
}
}
}

return nil, nil
}
Loading

0 comments on commit b88bc8d

Please sign in to comment.