Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Multi Node Analyzers #1646

Merged
merged 10 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading