Unverified Commit eb7bf00b authored by Alex Fedin's avatar Alex Fedin Committed by GitHub
Browse files

Merge pull request #34 from datreeio/DAT-3230-validate-k8s

Dat 3230 validate k8s (wip)
parents 2aed5987 1e66de81
Showing with 1377 additions and 924 deletions
+1377 -924
......@@ -4,6 +4,9 @@ run:
test:
go test ./...
build:
go build -tags staging -ldflags="-X github.com/datreeio/datree/cmd.CliVersion=0.0.1"
create-bin:
goreleaser --snapshot --skip-publish --rm-dist
......
package evaluation
import (
"github.com/datreeio/datree/bl/validation"
"github.com/datreeio/datree/pkg/cliClient"
"github.com/datreeio/datree/pkg/extractor"
)
type CLIClient interface {
RequestEvaluation(request *cliClient.EvaluationRequest) (*cliClient.EvaluationResponse, error)
CreateEvaluation(request *cliClient.CreateEvaluationRequest) (int, error)
UpdateEvaluationValidation(request *cliClient.UpdateEvaluationValidationRequest) error
}
type Evaluator struct {
cliClient CLIClient
osInfo *OSInfo
extractConfigurations func(path string) (*extractor.FileConfiguration, *extractor.Error)
}
func New(c CLIClient) *Evaluator {
return &Evaluator{
cliClient: c,
osInfo: NewOSInfo(),
}
}
type EvaluationResults struct {
FileNameRuleMapper map[string]map[int]*Rule
Summary struct {
RulesCount int
TotalFailedRules int
FilesCount int
TotalPassedCount int
}
}
type Error struct {
Message string
Filename string
}
func (e *Evaluator) CreateEvaluation(cliId string, cliVersion string, k8sVersion string) (int, error) {
evaluationId, err := e.cliClient.CreateEvaluation(&cliClient.CreateEvaluationRequest{
K8sVersion: k8sVersion,
CliId: cliId,
Metadata: &cliClient.Metadata{
CliVersion: cliVersion,
Os: e.osInfo.OS,
PlatformVersion: e.osInfo.PlatformVersion,
KernelVersion: e.osInfo.KernelVersion,
},
})
return evaluationId, err
}
func (e *Evaluator) Evaluate(validFilesPathsChan chan string, invalidFilesPathsChan chan *validation.InvalidFile, evaluationId int) (*EvaluationResults, []*validation.InvalidFile, []*cliClient.FileConfiguration, []*Error, error) {
filesConfigurations, invalidFiles, errors := e.extractFilesConfigurations(validFilesPathsChan, invalidFilesPathsChan)
invalidFilesPaths := []*string{}
for _, file := range invalidFiles {
invalidFilesPaths = append(invalidFilesPaths, &file.Path)
}
if len(invalidFiles) > 0 {
stopEvaluation := len(filesConfigurations) == 0 // NOTICE: validFilesPathsChan surely closed and empty
err := e.cliClient.UpdateEvaluationValidation(&cliClient.UpdateEvaluationValidationRequest{
EvaluationId: evaluationId,
InvalidFiles: invalidFilesPaths,
StopEvaluation: stopEvaluation,
})
if stopEvaluation {
return nil, invalidFiles, filesConfigurations, errors, err
}
}
if len(filesConfigurations) > 0 {
res, err := e.cliClient.RequestEvaluation(&cliClient.EvaluationRequest{
EvaluationId: evaluationId,
Files: filesConfigurations,
})
if err != nil {
return nil, invalidFiles, filesConfigurations, errors, err
}
results := e.formatEvaluationResults(res.Results, len(filesConfigurations))
return results, invalidFiles, filesConfigurations, errors, nil
}
return nil, invalidFiles, filesConfigurations, errors, nil
}
func (e *Evaluator) extractFilesConfigurations(validFilesPathsChan chan string, invalidFilesPathsChan chan *validation.InvalidFile) ([]*cliClient.FileConfiguration, []*validation.InvalidFile, []*Error) {
invalidFiles := []*validation.InvalidFile{}
var files []*cliClient.FileConfiguration
var errors []*Error
readFromValidDone := false
readFromInvalidDone := false
for {
select {
case path, ok := <-validFilesPathsChan:
if !ok {
readFromValidDone = true
} else {
file, err := extractor.ExtractConfiguration(path)
if file != nil {
files = append(files, &cliClient.FileConfiguration{
FileName: file.FileName,
Configurations: file.Configurations,
})
}
if err != nil {
errors = append(errors, &Error{
Message: err.Message,
Filename: err.Filename,
})
}
}
case file, ok := <-invalidFilesPathsChan:
if !ok {
readFromInvalidDone = true
} else {
invalidFiles = append(invalidFiles, file)
}
}
if readFromValidDone && readFromInvalidDone {
break
}
}
return files, invalidFiles, errors
}
func (e *Evaluator) formatEvaluationResults(evaluationResults []*cliClient.EvaluationResult, filesCount int) *EvaluationResults {
mapper := make(map[string]map[int]*Rule)
totalRulesCount := len(evaluationResults)
totalFailedCount := 0
totalPassedCount := filesCount
for _, result := range evaluationResults {
for _, match := range result.Results.Matches {
// file not already exists in mapper
if _, exists := mapper[match.FileName]; !exists {
mapper[match.FileName] = make(map[int]*Rule)
totalPassedCount = totalPassedCount - 1
}
// file and rule not already exists in mapper
if _, exists := mapper[match.FileName][result.Rule.ID]; !exists {
totalFailedCount++
mapper[match.FileName][result.Rule.ID] = &Rule{ID: result.Rule.ID, Name: result.Rule.Name, FailSuggestion: result.Rule.FailSuggestion, Count: 0}
}
mapper[match.FileName][result.Rule.ID].IncrementCount()
}
}
results := &EvaluationResults{
FileNameRuleMapper: mapper,
Summary: struct {
RulesCount int
TotalFailedRules int
FilesCount int
TotalPassedCount int
}{
RulesCount: totalRulesCount,
TotalFailedRules: totalFailedCount,
FilesCount: filesCount,
TotalPassedCount: totalPassedCount,
},
}
return results
}
package evaluation
//import (
// "path/filepath"
// "testing"
//
// "github.com/datreeio/datree/pkg/cliClient"
// "github.com/stretchr/testify/assert"
// "github.com/stretchr/testify/mock"
//)
//
//type mockCliClient struct {
// mock.Mock
//}
//
//func (m *mockCliClient) CreateEvaluation(createEvaluationRequest *cliClient.CreateEvaluationRequest) (int, error) {
// args := m.Called(createEvaluationRequest)
// return args.Get(0).(int), args.Error(1)
//}
//
//func (m *mockCliClient) RequestEvaluation(evaluationRequest *cliClient.EvaluationRequest) (*cliClient.EvaluationResponse, error) {
// args := m.Called(evaluationRequest)
// return args.Get(0).(*cliClient.EvaluationResponse), args.Error(1)
//}
//
//func (m *mockCliClient) UpdateEvaluationValidation(request *cliClient.UpdateEvaluationValidationRequest) error {
// args := m.Called(request)
// return args.Error(0)
//}
//
//func (m *mockCliClient) GetVersionMessage(cliVersion string, timeout int) (*cliClient.VersionMessage, error) {
// args := m.Called(cliVersion, timeout)
// return args.Get(0).(*cliClient.VersionMessage), args.Error(1)
//}
//
//type cliClientMockTestCase struct {
// createEvaluation struct {
// evaluationId int
// err error
// }
// requestEvaluation struct {
// response *cliClient.EvaluationResponse
// err error
// }
// updateEvaluationValidation struct {
// err error
// }
// getVersionMessage struct {
// response *cliClient.VersionMessage
// err error
// }
//}
//type evaluatorMock struct {
// cliClient *cliClientMockTestCase
//}
//
//func TestEvaluate(t *testing.T) {
// tests := []*evaluateTestCase{}
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// mockedCliClient := &mockCliClient{}
//
// mockedCliClient.On("CreateEvaluation", mock.Anything).Return(tt.mock.cliClient.createEvaluation.evaluationId, tt.mock.cliClient.createEvaluation.err)
// mockedCliClient.On("UpdateEvaluationValidation", mock.Anything).Return(nil)
//
// evaluator := &Evaluator{
// cliClient: mockedCliClient,
// osInfo: tt.args.osInfo,
// }
//
// actualResponse, _, _ := evaluator.Evaluate(tt.args.validFilesChan, tt.args.invalidFilesChan, tt.args.evaluationId)
//
// if tt.expected.isRequestEvaluationCalled {
// mockedCliClient.AssertCalled(t, "RequestEvaluation", mock.Anything)
// assert.Equal(t, tt.expected.response.Summary, actualResponse.Summary)
// assert.Equal(t, tt.expected.response.FileNameRuleMapper, actualResponse.FileNameRuleMapper)
// }
//
// if tt.expected.isUpdateEvaluationValidationCalled {
// mockedCliClient.AssertCalled(t, "UpdateEvaluationValidation", mock.Anything)
// }
//
// })
// }
//}
//
//func TestCreateEvaluation(t *testing.T) {
// tests := []*evaluateTestCase{
// request_evaluation_all_invalid(),
// request_evaluation_all_valid(),
// }
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// mockedCliClient := &mockCliClient{}
//
// mockedCliClient.On("RequestEvaluation", mock.Anything).Return(tt.mock.cliClient.requestEvaluation.response, tt.mock.cliClient.requestEvaluation.err)
// mockedCliClient.On("UpdateEvaluationValidation", mock.Anything).Return(nil)
//
// evaluator := &Evaluator{
// cliClient: mockedCliClient,
// osInfo: tt.args.osInfo,
// }
//
// actualResponse, _, _ := evaluator.Evaluate(tt.args.validFilesChan, tt.args.invalidFilesChan, tt.args.evaluationId)
//
// if tt.expected.isRequestEvaluationCalled {
// mockedCliClient.AssertCalled(t, "RequestEvaluation", mock.Anything)
// assert.Equal(t, tt.expected.response.Summary, actualResponse.Summary)
// assert.Equal(t, tt.expected.response.FileNameRuleMapper, actualResponse.FileNameRuleMapper)
// }
//
// if tt.expected.isUpdateEvaluationValidationCalled {
// mockedCliClient.AssertCalled(t, "UpdateEvaluationValidation", mock.Anything)
// }
//
// })
// }
//}
//
//type evaluateArgs struct {
// validFilesChan <-chan string
// invalidFilesChan []*string
// evaluationId int
// osInfo *OSInfo
//}
//
//type evaluateExpected struct {
// response *EvaluationResults
// errors []*Error
// err error
// isRequestEvaluationCalled bool
// isCreateEvaluationCalled bool
// isUpdateEvaluationValidationCalled bool
// isGetVersionMessageCalled bool
//}
//
//type evaluateTestCase struct {
// name string
// args *evaluateArgs
// mock *evaluatorMock
// expected *evaluateExpected
//}
//
//func request_evaluation_all_valid() *evaluateTestCase {
// return &evaluateTestCase{
// name: "should request validation without invalid files",
// args: &evaluateArgs{
// validFilesChan: newFilesChan(),
// invalidFilesChan: []*string{},
// evaluationId: 1,
// osInfo: &OSInfo{
// OS: "darwin",
// PlatformVersion: "1.2.3",
// KernelVersion: "4.5.6",
// },
// },
// mock: &evaluatorMock{
// cliClient: &cliClientMockTestCase{
// createEvaluation: struct {
// evaluationId int
// err error
// }{
// evaluationId: 1,
// err: nil,
// },
// requestEvaluation: struct {
// response *cliClient.EvaluationResponse
// err error
// }{
// response: &cliClient.EvaluationResponse{
// Results: []*cliClient.EvaluationResult{},
// },
// err: nil,
// },
// updateEvaluationValidation: struct{ err error }{
// err: nil,
// },
// getVersionMessage: struct {
// response *cliClient.VersionMessage
// err error
// }{
// response: nil,
// err: nil,
// },
// },
// },
// expected: &evaluateExpected{
// response: &EvaluationResults{
// FileNameRuleMapper: make(map[string]map[int]*Rule),
// Summary: struct {
// RulesCount int
// TotalFailedRules int
// FilesCount int
// }{
// RulesCount: 0,
// TotalFailedRules: 0,
// FilesCount: 1,
// },
// },
// errors: []*Error{},
// err: nil,
// isRequestEvaluationCalled: true,
// isUpdateEvaluationValidationCalled: false,
// },
// }
//}
//
//func request_evaluation_all_invalid() *evaluateTestCase {
// invalidPath := "path/path1/service.yaml"
// return &evaluateTestCase{
// name: "should request validation all files are invalid",
// args: &evaluateArgs{
// validFilesChan: newFilesChan(),
// invalidFilesChan: []*string{&invalidPath},
// evaluationId: 1,
// osInfo: &OSInfo{
// OS: "darwin",
// PlatformVersion: "1.2.3",
// KernelVersion: "4.5.6",
// },
// },
// mock: &evaluatorMock{
// cliClient: &cliClientMockTestCase{
// createEvaluation: struct {
// evaluationId int
// err error
// }{
// evaluationId: 1,
// err: nil,
// },
// requestEvaluation: struct {
// response *cliClient.EvaluationResponse
// err error
// }{
// response: &cliClient.EvaluationResponse{
// Results: []*cliClient.EvaluationResult{},
// },
// err: nil,
// },
// updateEvaluationValidation: struct{ err error }{
// err: nil,
// },
// getVersionMessage: struct {
// response *cliClient.VersionMessage
// err error
// }{
// response: nil,
// err: nil,
// },
// },
// },
// expected: &evaluateExpected{
// response: &EvaluationResults{
// FileNameRuleMapper: make(map[string]map[int]*Rule),
// Summary: struct {
// RulesCount int
// TotalFailedRules int
// FilesCount int
// }{
// RulesCount: 0,
// TotalFailedRules: 0,
// FilesCount: 1,
// },
// },
// errors: []*Error{},
// err: nil,
// isRequestEvaluationCalled: false,
// isUpdateEvaluationValidationCalled: true,
// },
// }
//}
//
//func newFilesChan() chan string {
// files := make(chan string, 1)
// p, _ := filepath.Abs("../../internal/fixtures/kube/pass-all.yaml")
// files <- p
// close(files)
// return files
//}
package bl
package evaluation
import "github.com/shirou/gopsutil/host"
......@@ -8,7 +8,7 @@ type OSInfo struct {
KernelVersion string
}
func NewOsInfo() *OSInfo {
func NewOSInfo() *OSInfo {
infoStat, _ := host.Info()
return &OSInfo{
OS: infoStat.OS,
......
package evaluation
import (
"encoding/json"
"fmt"
"github.com/datreeio/datree/bl/validation"
"os"
"path/filepath"
"github.com/datreeio/datree/pkg/printer"
"gopkg.in/yaml.v2"
)
type Printer interface {
PrintWarnings(warnings []printer.Warning)
PrintSummaryTable(summary printer.Summary)
PrintEvaluationSummary(summary printer.EvaluationSummary)
}
// func PrintAllResults(results *EvaluationResults /*, invalidFiles*/) {
// // TODO: foreach invalid file
// }
// url := "https://app.datree.io/login?cliId=" + cliId
func PrintResults(results *EvaluationResults, invalidFiles []*validation.InvalidFile, evaluationSummary printer.EvaluationSummary, loginURL string, outputFormat string, printer Printer) error {
switch {
case outputFormat == "json":
return jsonOutput(results)
case outputFormat == "yaml":
return yamlOutput(results)
default:
return textOutput(results, invalidFiles, evaluationSummary, loginURL, printer)
}
}
func jsonOutput(results *EvaluationResults) error {
jsonOutput, err := json.Marshal(results)
if err != nil {
fmt.Println(err)
return err
}
fmt.Println(string(jsonOutput))
return nil
}
func yamlOutput(results *EvaluationResults) error {
yamlOutput, err := yaml.Marshal(results)
if err != nil {
fmt.Println(err)
return err
}
fmt.Println(string(yamlOutput))
return nil
}
func textOutput(results *EvaluationResults, invalidFiles []*validation.InvalidFile, evaluationSummary printer.EvaluationSummary, url string, printer Printer) error {
pwd, err := os.Getwd()
if err != nil {
return err
}
warnings, err := parseToPrinterWarnings(results, invalidFiles, pwd)
if err != nil {
fmt.Println(err)
return err
}
printer.PrintWarnings(warnings)
summary := parseEvaluationResultsToSummary(results, evaluationSummary, url)
printer.PrintEvaluationSummary(evaluationSummary)
printer.PrintSummaryTable(summary)
return nil
}
func parseToPrinterWarnings(results *EvaluationResults, invalidFiles []*validation.InvalidFile, pwd string) ([]printer.Warning, error) {
var warnings = []printer.Warning{}
for _, invalidFile := range invalidFiles {
warnings = append(warnings, printer.Warning{
Title: fmt.Sprintf(">> File: %s\n", invalidFile.Path),
Details: []printer.WarningInfo{},
ValidationInfo: printer.ValidationInfo{
IsValid: false,
ValidationErrors: invalidFile.ValidationErrors,
K8sVersion: "1.18.0",
},
})
}
if results != nil {
for fileName, rules := range results.FileNameRuleMapper {
var warningDetails = []printer.WarningInfo{}
for _, rule := range rules {
details := printer.WarningInfo{
Caption: rule.Name,
Occurrences: rule.Count,
Suggestion: rule.FailSuggestion,
}
warningDetails = append(warningDetails, details)
}
relativePath, _ := filepath.Rel(pwd, fileName)
warnings = append(warnings, printer.Warning{
Title: fmt.Sprintf(">> File: %s\n", relativePath),
Details: warningDetails,
ValidationInfo: printer.ValidationInfo{
IsValid: true,
ValidationErrors: []error{},
K8sVersion: "1.18.0",
},
})
}
}
return warnings, nil
}
type OutputTitle int
const (
EnabledRules OutputTitle = iota
EvaluatedConfigurations
TotalRulesEvaluated
SeeAll
TotalRulesPassed
TotalRulesFailed
)
func (t OutputTitle) String() string {
return [...]string{
"Enabled rules in policy “default”",
"Configs tested against policy",
"Total rules evaluated",
"See all rules in policy",
"Total rules passed",
"Total rules failed"}[t]
}
func parseEvaluationResultsToSummary(results *EvaluationResults, evaluationSummary printer.EvaluationSummary, loginURL string) printer.Summary {
filesCount := evaluationSummary.FilesCount
rulesCount := 0
totalRulesEvaluated := 0
totalFailedRules := 0
totalPassedRules := 0
if results != nil {
rulesCount = results.Summary.RulesCount
totalRulesEvaluated = results.Summary.RulesCount * results.Summary.FilesCount
totalFailedRules = results.Summary.TotalFailedRules
totalPassedRules = totalRulesEvaluated - totalFailedRules
}
var rulesCountRightCol string
if rulesCount == 0 {
rulesCountRightCol = fmt.Sprint("N/A")
} else {
rulesCountRightCol = fmt.Sprint(rulesCount)
}
plainRows := []printer.SummaryItem{
{LeftCol: EnabledRules.String(), RightCol: rulesCountRightCol, RowIndex: 0},
{LeftCol: EvaluatedConfigurations.String(), RightCol: fmt.Sprint(filesCount), RowIndex: 1},
{LeftCol: TotalRulesEvaluated.String(), RightCol: fmt.Sprint(totalRulesEvaluated), RowIndex: 2},
{LeftCol: SeeAll.String(), RightCol: loginURL, RowIndex: 5},
}
successRow := printer.SummaryItem{LeftCol: TotalRulesPassed.String(), RightCol: fmt.Sprint(totalPassedRules), RowIndex: 4}
errorRow := printer.SummaryItem{LeftCol: TotalRulesFailed.String(), RightCol: fmt.Sprint(totalFailedRules), RowIndex: 3}
summary := &printer.Summary{
ErrorRow: errorRow,
SuccessRow: successRow,
PlainRows: plainRows,
}
return *summary
}
package evaluation
//
//import (
// "os"
// "testing"
//
// "github.com/datreeio/datree/pkg/printer"
// "github.com/stretchr/testify/mock"
//)
//
//type mockPrinter struct {
// mock.Mock
//}
//
//func (m *mockPrinter) PrintWarnings(warnings []printer.Warning) {
// m.Called(warnings)
//}
//
//func (c *mockPrinter) PrintSummaryTable(summary printer.Summary) {
// c.Called(summary)
//}
//
//type printResultsTestCaseArgs struct {
// results *EvaluationResults
// loginURL string
// outputFormat string
// printer Printer
//}
//
//type printResultsTestCase struct {
// name string
// args *printResultsTestCaseArgs
// expected error
//}
//
//func TestPrintResults(t *testing.T) {
// tests := []*printResultsTestCase{}
// for _, tt := range tests {
// mockedPrinter := &mockPrinter{}
// mockedPrinter.On("PrintWarnings", mock.Anything)
// mockedPrinter.On("PrintSummaryTable", mock.Anything)
// t.Run(tt.name, func(t *testing.T) {
// PrintResults(tt.args.results, tt.args.loginURL, tt.args.outputFormat, tt.args.printer)
//
// if tt.args.outputFormat == "json" {
// mockedPrinter.AssertNotCalled(t, "PrintWarnings")
// mockedPrinter.AssertCalled(t, "PrintSummaryTable")
// } else if tt.args.outputFormat == "yaml" {
// mockedPrinter.AssertNotCalled(t, "PrintWarnings")
// mockedPrinter.AssertCalled(t, "PrintSummaryTable")
//
// } else {
// pwd, _ := os.Getwd()
// warnings, _ := parseToPrinterWarnings(tt.args.results, pwd)
// mockedPrinter.AssertCalled(t, "PrintWarnings", warnings)
// mockedPrinter.AssertCalled(t, "PrintSummaryTable", tt.args.results, tt.args.loginURL)
// }
// })
// }
//}
package evaluation
type Rule struct {
ID int
Name string
FailSuggestion string
Count int
}
func (rp *Rule) IncrementCount() {
rp.Count++
}
package bl
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/datreeio/datree/pkg/cliClient"
"github.com/datreeio/datree/pkg/printer"
"github.com/datreeio/datree/pkg/propertiesExtractor"
"gopkg.in/yaml.v3"
)
type Printer interface {
PrintWarnings(warnings []printer.Warning)
PrintSummaryTable(summary printer.Summary)
}
type CLIClient interface {
RequestEvaluation(cliClient.EvaluationRequest) (cliClient.EvaluationResponse, error)
CreateEvaluation(request cliClient.CreateEvaluationRequest) (int, error)
}
type PropertiesExtractor interface {
ReadFilesFromPaths(paths []string, conc int) ([]*propertiesExtractor.FileProperties, []propertiesExtractor.FileError, []error)
}
type Evaluator struct {
propertiesExtractor PropertiesExtractor
cliClient CLIClient
printer Printer
osInfo *OSInfo
}
func CreateNewEvaluator(pe PropertiesExtractor, c CLIClient, p Printer) *Evaluator {
return &Evaluator{
propertiesExtractor: pe,
cliClient: c,
printer: p,
osInfo: NewOsInfo(),
}
}
type EvaluationResults struct {
FileNameRuleMapper map[string]map[int]*Rule
Summary struct {
RulesCount int
TotalFailedRules int
FilesCount int
}
}
type UserAgent struct {
OS string
PlatformVersion string
KernelVersion string
}
func (e *Evaluator) Evaluate(paths []string, cliId string, evaluationConc int, cliVersion string) (*EvaluationResults, []propertiesExtractor.FileError, error) {
files, fileErrors, errors := e.propertiesExtractor.ReadFilesFromPaths(paths, evaluationConc)
if len(errors) > 0 {
return nil, fileErrors, fmt.Errorf("failed evaluation with the following errors: %s", errors)
}
if len(files) == 0 {
return nil, fileErrors, fmt.Errorf("no files detected")
}
var filesProperties []propertiesExtractor.FileProperties
for _, file := range files {
filesProperties = append(filesProperties, *file)
}
createEvaluationRequest := cliClient.CreateEvaluationRequest{
CliId: cliId,
Metadata: cliClient.Metadata{
CliVersion: cliVersion,
Os: e.osInfo.OS,
PlatformVersion: e.osInfo.PlatformVersion,
KernelVersion: e.osInfo.KernelVersion,
},
}
evaluationId, err := e.cliClient.CreateEvaluation(createEvaluationRequest)
if err != nil {
return nil, fileErrors, err
}
evaluationRequest := cliClient.EvaluationRequest{
EvaluationId: evaluationId,
Files: filesProperties,
}
res, err := e.cliClient.RequestEvaluation(evaluationRequest)
if err != nil {
return nil, fileErrors, err
}
results := e.aggregateEvaluationResults(res.Results, len(files))
return results, fileErrors, nil
}
func (e *Evaluator) PrintResults(results *EvaluationResults, cliId string, output string) error {
if output == "json" {
jsonOutput, err := json.Marshal(results)
if err != nil {
fmt.Println(err)
return err
}
fmt.Println(string(jsonOutput))
return nil
}
if output == "yaml" {
yamlOutput, err := yaml.Marshal(results)
if err != nil {
fmt.Println(err)
return err
}
fmt.Println(string(yamlOutput))
return nil
}
warnings, err := e.parseEvaluationResultsToWarnings(results)
if err != nil {
fmt.Println(err)
return err
}
e.printer.PrintWarnings(warnings)
configurePolicyLink := "https://app.datree.io/login?cliId=" + cliId
summary := e.parseEvaluationResultsToSummary(results, configurePolicyLink)
e.printer.PrintSummaryTable(summary)
if results.Summary.TotalFailedRules > 0 {
return fmt.Errorf("failed rules count is %d (>0)", results.Summary.TotalFailedRules)
}
return nil
}
func (e *Evaluator) PrintFileParsingErrors(errors []propertiesExtractor.FileError) {
if len(errors) > 0 {
fmt.Println("The following files failed:")
for _, fileError := range errors {
fmt.Printf("\n\tFilename: %s\n\tError: %s\n\t---------------------", fileError.Filename, fileError.Message)
}
}
}
func (e *Evaluator) aggregateEvaluationResults(evaluationResults []cliClient.EvaluationResult, filesCount int) *EvaluationResults {
mapper := make(map[string]map[int]*Rule)
totalRulesCount := len(evaluationResults)
totalFailedCount := 0
for _, result := range evaluationResults {
for _, match := range result.Results.Matches {
// file not already exists in mapper
if _, exists := mapper[match.FileName]; !exists {
mapper[match.FileName] = make(map[int]*Rule)
}
// file and rule not already exists in mapper
if _, exists := mapper[match.FileName][result.Rule.ID]; !exists {
totalFailedCount++
mapper[match.FileName][result.Rule.ID] = &Rule{ID: result.Rule.ID, Name: result.Rule.Name, FailSuggestion: result.Rule.FailSuggestion, Count: 0}
}
mapper[match.FileName][result.Rule.ID].IncrementCount()
}
}
results := &EvaluationResults{
FileNameRuleMapper: mapper,
Summary: struct {
RulesCount int
TotalFailedRules int
FilesCount int
}{
RulesCount: totalRulesCount,
TotalFailedRules: totalFailedCount,
FilesCount: filesCount,
},
}
return results
}
func (e *Evaluator) parseEvaluationResultsToWarnings(results *EvaluationResults) ([]printer.Warning, error) {
pwd, err := os.Getwd()
if err != nil {
return nil, err
}
warnings := []printer.Warning{}
for fileName, rules := range results.FileNameRuleMapper {
relativePath, _ := filepath.Rel(pwd, fileName)
title := fmt.Sprintf(">> File: %s\n", relativePath)
warning := printer.Warning{
Title: title,
}
for _, rule := range rules {
details := struct {
Caption string
Occurrences int
Suggestion string
}{
Caption: rule.Name,
Occurrences: rule.Count,
Suggestion: rule.FailSuggestion,
}
warning.Details = append(warning.Details, details)
}
warnings = append(warnings, warning)
}
return warnings, nil
}
func (e *Evaluator) parseEvaluationResultsToSummary(results *EvaluationResults, loginURL string) printer.Summary {
totalRulesEvaluated := results.Summary.RulesCount * results.Summary.FilesCount
plainRows := []printer.SummaryItem{
{LeftCol: "Enabled rules in policy “default”", RightCol: fmt.Sprint(results.Summary.RulesCount), RowIndex: 0},
{LeftCol: "Configs tested against policy", RightCol: fmt.Sprint(results.Summary.FilesCount), RowIndex: 1},
{LeftCol: "Total rules evaluated", RightCol: fmt.Sprint(totalRulesEvaluated), RowIndex: 2},
{LeftCol: "See all rules in policy", RightCol: loginURL, RowIndex: 5},
}
successRow := printer.SummaryItem{LeftCol: "Total rules passed", RightCol: fmt.Sprint(totalRulesEvaluated - results.Summary.TotalFailedRules), RowIndex: 4}
errorRow := printer.SummaryItem{LeftCol: "Total rules failed", RightCol: fmt.Sprint(results.Summary.TotalFailedRules), RowIndex: 3}
summary := &printer.Summary{
ErrorRow: errorRow,
SuccessRow: successRow,
PlainRows: plainRows,
}
return *summary
}
type Rule struct {
ID int
Name string
FailSuggestion string
Count int
}
func (rp *Rule) IncrementCount() {
rp.Count++
}
package bl
import (
"fmt"
"testing"
"github.com/datreeio/datree/pkg/cliClient"
"github.com/datreeio/datree/pkg/printer"
"github.com/datreeio/datree/pkg/propertiesExtractor"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type mockPropertiesExtractor struct {
mock.Mock
}
func (m *mockPropertiesExtractor) ReadFilesFromPaths(paths []string, conc int) ([]*propertiesExtractor.FileProperties, []propertiesExtractor.FileError, []error) {
args := m.Called(paths, conc)
return args.Get(0).([]*propertiesExtractor.FileProperties), args.Get(1).([]propertiesExtractor.FileError), args.Get(2).([]error)
}
type mockCliClient struct {
mock.Mock
}
func (m *mockCliClient) CreateEvaluation(createEvaluationRequest cliClient.CreateEvaluationRequest) (int, error) {
args := m.Called(createEvaluationRequest)
return args.Get(0).(int), args.Error(1)
}
func (m *mockCliClient) RequestEvaluation(evaluationRequest cliClient.EvaluationRequest) (cliClient.EvaluationResponse, error) {
args := m.Called(evaluationRequest)
return args.Get(0).(cliClient.EvaluationResponse), args.Error(1)
}
type mockPrinter struct {
mock.Mock
}
func (m *mockPrinter) PrintWarnings(warnings []printer.Warning) {
m.Called(warnings)
}
func (c *mockPrinter) PrintSummaryTable(summary printer.Summary) {
c.Called(summary)
}
type propertiesExtractorMockTestCase struct {
readFilesFromPaths struct {
properties []*propertiesExtractor.FileProperties
filesErrors []propertiesExtractor.FileError
errors []error
}
}
type cliClientMockTestCase struct {
createEvaluation struct {
EvaluationId int
errors error
}
requestEvaluation struct {
response cliClient.EvaluationResponse
errors error
}
}
type evaluateTestCase struct {
name string
args struct {
paths []string
cliId string
evaluationConc int
evaluationRequest cliClient.EvaluationRequest
createEvaluationRequest cliClient.CreateEvaluationRequest
}
mock struct {
propertiesExtractor propertiesExtractorMockTestCase
cliClient cliClientMockTestCase
}
expected struct {
response *EvaluationResults
fileErrors []propertiesExtractor.FileError
err error
}
}
func TestEvaluate(t *testing.T) {
tests := []*evaluateTestCase{
test_create_evaluation_failedRequest(),
//test_evaluate_failedRequest(),
//test_evaluate_success(),
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
propertiesExtractor := &mockPropertiesExtractor{}
cliClient := &mockCliClient{}
printer := &mockPrinter{}
propertiesExtractor.On("ReadFilesFromPaths", mock.Anything, mock.Anything).Return(tt.mock.propertiesExtractor.readFilesFromPaths.properties, tt.mock.propertiesExtractor.readFilesFromPaths.filesErrors, tt.mock.propertiesExtractor.readFilesFromPaths.errors)
cliClient.On("RequestEvaluation", mock.Anything).Return(tt.mock.cliClient.requestEvaluation.response, tt.mock.cliClient.requestEvaluation.errors)
cliClient.On("CreateEvaluation", mock.Anything).Return(tt.mock.cliClient.createEvaluation.EvaluationId, tt.mock.cliClient.createEvaluation.errors)
evaluator := &Evaluator{
propertiesExtractor: propertiesExtractor,
cliClient: cliClient,
printer: printer,
osInfo: &OSInfo{
OS: "darwin",
PlatformVersion: "1.2.3",
KernelVersion: "4.5.6",
},
}
actualResponse, actualFilesErrs, actualErr := evaluator.Evaluate(tt.args.paths, tt.args.cliId, tt.args.evaluationConc, "0.0.1")
propertiesExtractor.AssertCalled(t, "ReadFilesFromPaths", tt.args.paths, tt.args.evaluationConc)
cliClient.AssertCalled(t, "CreateEvaluation", tt.args.createEvaluationRequest)
if tt.mock.cliClient.createEvaluation.errors == nil {
cliClient.AssertCalled(t, "RequestEvaluation", tt.args.evaluationRequest)
}
assert.Equal(t, tt.expected.response, actualResponse)
assert.Equal(t, tt.expected.fileErrors, actualFilesErrs)
assert.Equal(t, tt.expected.err, actualErr)
})
}
}
func test_evaluate_success() *evaluateTestCase {
evaluationResponse := cliClient.EvaluationResponse{
Results: []cliClient.EvaluationResult{
{
Passed: true,
Results: struct {
Matches []cliClient.Match "json:\"matches\""
Mismatches []cliClient.Match "json:\"mismatches\""
}{
Matches: []cliClient.Match{},
Mismatches: []cliClient.Match{},
},
},
},
}
createEvaluationResponse := cliClient.CreateEvaluationResponse{
EvaluationId: 432,
}
return &evaluateTestCase{
name: "success",
args: struct {
paths []string
cliId string
evaluationConc int
evaluationRequest cliClient.EvaluationRequest
createEvaluationRequest cliClient.CreateEvaluationRequest
}{
paths: []string{"path1/path2/file.yaml"},
cliId: "cliId-test",
evaluationConc: 1,
evaluationRequest: cliClient.EvaluationRequest{
EvaluationId: createEvaluationResponse.EvaluationId,
Files: []propertiesExtractor.FileProperties{{
FileName: "path1/path2/file.yaml",
Configurations: []propertiesExtractor.K8sConfiguration{{"apiVersion": "extensions/v1beta1"}},
}},
},
createEvaluationRequest: cliClient.CreateEvaluationRequest{
CliId: "cliId-test",
Metadata: cliClient.Metadata{
CliVersion: "0.0.1",
Os: "darwin",
PlatformVersion: "1.2.3",
KernelVersion: "4.5.6",
},
},
},
mock: struct {
propertiesExtractor propertiesExtractorMockTestCase
cliClient cliClientMockTestCase
}{
propertiesExtractor: propertiesExtractorMockTestCase{
readFilesFromPaths: struct {
properties []*propertiesExtractor.FileProperties
filesErrors []propertiesExtractor.FileError
errors []error
}{
properties: []*propertiesExtractor.FileProperties{{
FileName: "path1/path2/file.yaml",
Configurations: []propertiesExtractor.K8sConfiguration{{"apiVersion": "extensions/v1beta1"}},
}},
filesErrors: []propertiesExtractor.FileError{},
errors: []error{},
},
},
cliClient: cliClientMockTestCase{
requestEvaluation: struct {
response cliClient.EvaluationResponse
errors error
}{
response: evaluationResponse,
errors: nil,
},
createEvaluation: struct {
EvaluationId int
errors error
}{
EvaluationId: createEvaluationResponse.EvaluationId,
},
},
},
expected: struct {
response *EvaluationResults
fileErrors []propertiesExtractor.FileError
err error
}{
response: &EvaluationResults{
FileNameRuleMapper: map[string]map[int]*Rule{}, Summary: struct {
RulesCount int
TotalFailedRules int
FilesCount int
}{RulesCount: 1, TotalFailedRules: 0, FilesCount: 1},
},
fileErrors: []propertiesExtractor.FileError{},
err: nil,
},
}
}
func test_evaluate_failedRequest() *evaluateTestCase {
createEvaluationResponse := cliClient.CreateEvaluationResponse{
EvaluationId: 432,
}
evaluationResponse := cliClient.EvaluationResponse{}
return &evaluateTestCase{
name: "fail",
args: struct {
paths []string
cliId string
evaluationConc int
evaluationRequest cliClient.EvaluationRequest
createEvaluationRequest cliClient.CreateEvaluationRequest
}{
paths: []string{"path1/path2/file.yaml"},
cliId: "cliId-test",
evaluationConc: 1,
evaluationRequest: cliClient.EvaluationRequest{
EvaluationId: createEvaluationResponse.EvaluationId,
Files: []propertiesExtractor.FileProperties{{
FileName: "path1/path2/file.yaml",
Configurations: []propertiesExtractor.K8sConfiguration{{"apiVersion": "extensions/v1beta1"}},
}},
},
createEvaluationRequest: cliClient.CreateEvaluationRequest{
CliId: "cliId-test",
Metadata: cliClient.Metadata{
CliVersion: "0.0.1",
Os: "darwin",
PlatformVersion: "1.2.3",
KernelVersion: "4.5.6",
},
},
},
mock: struct {
propertiesExtractor propertiesExtractorMockTestCase
cliClient cliClientMockTestCase
}{
propertiesExtractor: propertiesExtractorMockTestCase{
readFilesFromPaths: struct {
properties []*propertiesExtractor.FileProperties
filesErrors []propertiesExtractor.FileError
errors []error
}{
properties: []*propertiesExtractor.FileProperties{{
FileName: "path1/path2/file.yaml",
Configurations: []propertiesExtractor.K8sConfiguration{{"apiVersion": "extensions/v1beta1"}}}},
filesErrors: []propertiesExtractor.FileError{},
errors: []error{},
},
},
cliClient: cliClientMockTestCase{
requestEvaluation: struct {
response cliClient.EvaluationResponse
errors error
}{
response: evaluationResponse,
errors: fmt.Errorf("error"),
},
createEvaluation: struct {
EvaluationId int
errors error
}{
EvaluationId: createEvaluationResponse.EvaluationId,
errors: nil,
},
},
},
expected: struct {
response *EvaluationResults
fileErrors []propertiesExtractor.FileError
err error
}{
response: nil,
fileErrors: []propertiesExtractor.FileError{},
err: fmt.Errorf("error"),
},
}
}
func test_create_evaluation_failedRequest() *evaluateTestCase {
createEvaluationResponse := cliClient.CreateEvaluationResponse{}
evaluationResponse := cliClient.EvaluationResponse{}
return &evaluateTestCase{
name: "fail",
args: struct {
paths []string
cliId string
evaluationConc int
evaluationRequest cliClient.EvaluationRequest
createEvaluationRequest cliClient.CreateEvaluationRequest
}{
paths: []string{"path1/path2/file.yaml"},
cliId: "cliId-test",
evaluationConc: 1,
evaluationRequest: cliClient.EvaluationRequest{
EvaluationId: createEvaluationResponse.EvaluationId,
Files: []propertiesExtractor.FileProperties{{
FileName: "path1/path2/file.yaml",
Configurations: []propertiesExtractor.K8sConfiguration{{"apiVersion": "extensions/v1beta1"}},
}},
},
createEvaluationRequest: cliClient.CreateEvaluationRequest{
CliId: "cliId-test",
Metadata: cliClient.Metadata{
CliVersion: "0.0.1",
Os: "darwin",
PlatformVersion: "1.2.3",
KernelVersion: "4.5.6",
},
},
},
mock: struct {
propertiesExtractor propertiesExtractorMockTestCase
cliClient cliClientMockTestCase
}{
propertiesExtractor: propertiesExtractorMockTestCase{
readFilesFromPaths: struct {
properties []*propertiesExtractor.FileProperties
filesErrors []propertiesExtractor.FileError
errors []error
}{
properties: []*propertiesExtractor.FileProperties{{
FileName: "path1/path2/file.yaml",
Configurations: []propertiesExtractor.K8sConfiguration{{"apiVersion": "extensions/v1beta1"}}}},
filesErrors: []propertiesExtractor.FileError{},
errors: []error{},
},
},
cliClient: cliClientMockTestCase{
requestEvaluation: struct {
response cliClient.EvaluationResponse
errors error
}{
response: evaluationResponse,
errors: fmt.Errorf("error"),
},
createEvaluation: struct {
EvaluationId int
errors error
}{
errors: fmt.Errorf("create evaluation error"),
},
},
},
expected: struct {
response *EvaluationResults
fileErrors []propertiesExtractor.FileError
err error
}{
response: nil,
fileErrors: []propertiesExtractor.FileError{},
err: fmt.Errorf("create evaluation error"),
},
}
}
func TestPrintResults(t *testing.T) {
printerSpy := &mockPrinter{}
printerSpy.On("PrintWarnings", mock.Anything).Return()
printerSpy.On("PrintSummaryTable", mock.Anything).Return()
evaluator := &Evaluator{
propertiesExtractor: &mockPropertiesExtractor{},
cliClient: &mockCliClient{},
printer: printerSpy,
}
results := EvaluationResults{
FileNameRuleMapper: map[string]map[int]*Rule{},
Summary: struct {
RulesCount int
TotalFailedRules int
FilesCount int
}{},
}
evaluator.PrintResults(&results, "cli_id", "")
expectedPrinterWarnings := []printer.Warning{}
printerSpy.AssertCalled(t, "PrintWarnings", expectedPrinterWarnings)
plainRows := []printer.SummaryItem{
{
RightCol: "0",
LeftCol: "Enabled rules in policy “default”",
RowIndex: 0,
},
{
RightCol: "0",
LeftCol: "Configs tested against policy",
RowIndex: 1,
},
{
RightCol: "0",
LeftCol: "Total rules evaluated",
RowIndex: 2,
},
{
RightCol: "https://app.datree.io/login?cliId=cli_id",
LeftCol: "See all rules in policy",
RowIndex: 5,
}}
expectedPrinterSummary := printer.Summary{
PlainRows: plainRows,
ErrorRow: printer.SummaryItem{
LeftCol: "Total rules failed",
RightCol: "0",
RowIndex: 3,
},
SuccessRow: printer.SummaryItem{
LeftCol: "Total rules passed",
RightCol: "0",
RowIndex: 4,
},
}
printerSpy.AssertCalled(t, "PrintSummaryTable", expectedPrinterSummary)
}
package files
import (
"fmt"
"os"
"path/filepath"
)
func toAbsolutePath(path string) (string, error) {
absolutePath, err := filepath.Abs(path)
if err != nil {
return "", err
}
fileInfo, _ := os.Stat(absolutePath)
if fileInfo != nil && !fileInfo.IsDir() {
return filepath.Abs(absolutePath)
}
return "", fmt.Errorf("failed parsing absolute path %s", path)
}
func ToAbsolutePaths(paths []string) chan string {
pathsChan := make(chan string)
go func() {
for _, p := range paths {
absolutePath, err := toAbsolutePath(p)
if err == nil {
pathsChan <- absolutePath
}
}
close(pathsChan)
}()
return pathsChan
}
package files
//
//import (
// "fmt"
// "path/filepath"
// "testing"
//
// "github.com/stretchr/testify/assert"
//)
//
//type toAbsolutePathsTestCase struct {
// name string
// args struct {
// paths []string
// }
// expected struct {
// path string
// err error
// }
//}
//
//func TestToAbsolutePaths(t *testing.T) {
// tests := []*toAbsolutePathsTestCase{
// test_existed_file(),
// test_directory_file(),
// test_not_existed_file(),
// }
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// pathsChan, errors := ToAbsolutePaths(tt.args.paths)
// assert.Equal(t, tt.expected.path, <-pathsChan)
// assert.Equal(t, tt.expected.err, <-errors)
// })
// }
//
//}
//
//func test_existed_file() *toAbsolutePathsTestCase {
// p, _ := filepath.Abs("../../internal/fixtures/kube/pass-all.yaml")
// return &toAbsolutePathsTestCase{
// name: "existed file, should return abs path with no errors",
// args: struct{ paths []string }{
// paths: []string{"../../internal/fixtures/kube/pass-all.yaml"},
// },
// expected: struct {
// path string
// err error
// }{
// path: p,
// err: nil,
// },
// }
//}
//
//func test_not_existed_file() *toAbsolutePathsTestCase {
// return &toAbsolutePathsTestCase{
// name: "test not existed file, should return an error",
// args: struct{ paths []string }{
// paths: []string{"../../internal/fixtures/kube/bla.yaml"},
// },
// expected: struct {
// path string
// err error
// }{
// path: "",
// err: fmt.Errorf("failed parsing absolute path ../../internal/fixtures/kube/bla.yaml"),
// },
// }
//}
//
//func test_directory_file() *toAbsolutePathsTestCase {
// return &toAbsolutePathsTestCase{
// args: struct{ paths []string }{
// paths: []string{"../../internal/fixtures/kube"},
// },
// expected: struct {
// path string
// err error
// }{
// path: "",
// err: fmt.Errorf("failed parsing absolute path ../../internal/fixtures/kube"),
// },
// }
//}
package messager
import (
"github.com/datreeio/datree/pkg/cliClient"
)
type MessagesClient interface {
GetVersionMessage(cliVersion string, timeout int) (*cliClient.VersionMessage, error)
}
type Messager struct {
defaultTimeout int
messagesClient MessagesClient
}
func New(c MessagesClient) *Messager {
return &Messager{
defaultTimeout: 900,
messagesClient: c,
}
}
type VersionMessage struct {
CliVersion string
MessageText string
MessageColor string
}
func (m *Messager) LoadVersionMessages(messages chan *VersionMessage, cliVersion string) {
go func() {
msg, _ := m.messagesClient.GetVersionMessage(cliVersion, 900)
if msg != nil {
messages <- m.toVersionMessage(msg)
}
close(messages)
}()
}
func (m *Messager) toVersionMessage(msg *cliClient.VersionMessage) *VersionMessage {
if msg != nil {
return &VersionMessage{
CliVersion: msg.CliVersion,
MessageText: msg.MessageText,
MessageColor: msg.MessageColor,
}
}
return nil
}
package messager
import (
"testing"
"github.com/datreeio/datree/pkg/cliClient"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type mockMessagesClient struct {
mock.Mock
}
func (m *mockMessagesClient) GetVersionMessage(cliVersion string, timeout int) (*cliClient.VersionMessage, error) {
args := m.Called(cliVersion, timeout)
return args.Get(0).(*cliClient.VersionMessage), args.Error(1)
}
type LoadVersionMessagesTestCaseArgs struct {
messagesChan chan *VersionMessage
cliVersion string
}
type LoadVersionMessagesTestCaseMock struct {
message *cliClient.VersionMessage
timeout int
}
type LoadVersionMessagesTestCaseExpected struct {
message *VersionMessage
}
type LoadVersionMessagesTestCase struct {
name string
mock *LoadVersionMessagesTestCaseMock
args *LoadVersionMessagesTestCaseArgs
expected *LoadVersionMessagesTestCaseExpected
}
func TestLoadVersionMessages(t *testing.T) {
tests := []*LoadVersionMessagesTestCase{
load_message_success(),
load_message_failed(),
}
for _, tt := range tests {
mockedMessagesClient := &mockMessagesClient{}
t.Run(tt.name, func(t *testing.T) {
mockedMessagesClient.On("GetVersionMessage", mock.Anything, mock.Anything).Return(tt.mock.message, nil)
messager := &Messager{
defaultTimeout: tt.mock.timeout,
messagesClient: mockedMessagesClient,
}
messager.LoadVersionMessages(tt.args.messagesChan, tt.args.cliVersion)
msg := <-tt.args.messagesChan
if tt.mock.message != nil {
assert.Equal(t, tt.mock.message.CliVersion, msg.CliVersion)
assert.Equal(t, tt.mock.message.MessageColor, msg.MessageColor)
assert.Equal(t, tt.mock.message.MessageText, msg.MessageText)
mockedMessagesClient.AssertCalled(t, "GetVersionMessage", tt.args.cliVersion, tt.mock.timeout)
}
})
}
}
func load_message_success() *LoadVersionMessagesTestCase {
return &LoadVersionMessagesTestCase{
name: "should load mock message into channel",
mock: &LoadVersionMessagesTestCaseMock{
message: &cliClient.VersionMessage{
CliVersion: "0.1.1",
MessageText: "message",
MessageColor: "yellow",
},
timeout: 900,
},
args: &LoadVersionMessagesTestCaseArgs{
messagesChan: make(chan *VersionMessage),
cliVersion: "0.1.2",
},
expected: &LoadVersionMessagesTestCaseExpected{
message: &VersionMessage{
CliVersion: "0.1.1",
MessageText: "message",
MessageColor: "yellow",
},
},
}
}
func load_message_failed() *LoadVersionMessagesTestCase {
return &LoadVersionMessagesTestCase{
name: "should load mock message into channel",
mock: &LoadVersionMessagesTestCaseMock{
message: nil,
timeout: 900,
},
args: &LoadVersionMessagesTestCaseArgs{
messagesChan: make(chan *VersionMessage),
cliVersion: "0.1.2",
},
expected: &LoadVersionMessagesTestCaseExpected{
message: nil,
},
}
}
package validation
import (
"fmt"
"io"
"os"
"github.com/datreeio/datree/bl/files"
kubeconformValidator "github.com/yannh/kubeconform/pkg/validator"
)
type ValidationClient interface {
Validate(filename string, r io.ReadCloser) []kubeconformValidator.Result
}
type K8sValidator struct {
validationClient ValidationClient
}
func New(k8sVersion string) *K8sValidator {
kubconformClient := newKubconformValidator(k8sVersion)
return &K8sValidator{
validationClient: kubconformClient,
}
}
type InvalidFile struct {
Path string
ValidationErrors []error
}
func (val *K8sValidator) ValidateResources(paths []string) (chan string, chan *InvalidFile, chan error) {
pathsChan := files.ToAbsolutePaths(paths)
errorChan := make(chan error)
validFilesPathChan := make(chan string)
invalidFilesPathsChan := make(chan *InvalidFile)
go func() {
for path := range pathsChan {
isValid, validationErrors, err := val.validateResource(path)
if isValid {
validFilesPathChan <- path
} else {
invalidFilesPathsChan <- &InvalidFile{
Path: path,
ValidationErrors: validationErrors,
}
}
if err != nil {
errorChan <- err
}
}
close(invalidFilesPathsChan)
close(validFilesPathChan)
close(errorChan)
}()
return validFilesPathChan, invalidFilesPathsChan, errorChan
}
func (val *K8sValidator) validateResource(filepath string) (bool, []error, error) {
f, err := os.Open(filepath)
if err != nil {
return false, []error{}, fmt.Errorf("failed opening %s: %s", filepath, err)
}
results := val.validationClient.Validate(filepath, f)
isValid := true
var validationErrors []error
for _, res := range results {
// A file might contain multiple resources
// File starts with ---, the parser assumes a first empty resource
if res.Status == kubeconformValidator.Invalid || res.Status == kubeconformValidator.Error {
isValid = false
validationErrors = append(validationErrors, res.Err)
}
}
return isValid, validationErrors, nil
}
func newKubconformValidator(k8sVersion string) ValidationClient {
v, _ := kubeconformValidator.New(nil, kubeconformValidator.Opts{Strict: true, KubernetesVersion: k8sVersion})
return v
}
package validation
import (
"io"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
kubeconformValidator "github.com/yannh/kubeconform/pkg/validator"
)
type mockValidationClient struct {
mock.Mock
}
func (m *mockValidationClient) Validate(filename string, r io.ReadCloser) []kubeconformValidator.Result {
args := m.Called(filename, r)
return args.Get(0).([]kubeconformValidator.Result)
}
func TestValidateResources(t *testing.T) {
test_valid_multiple_resources(t)
}
func test_valid_multiple_resources(t *testing.T) {
validationClient := &mockValidationClient{}
validationClient.On("Validate", mock.Anything, mock.Anything).Return([]kubeconformValidator.Result{
{Status: kubeconformValidator.Valid},
})
k8sValidator := K8sValidator{
validationClient: validationClient,
}
path := "../../internal/fixtures/kube/pass-all.yaml"
valid, _, _ := k8sValidator.ValidateResources([]string{path})
expectedPath, _ := filepath.Abs(path)
for p := range valid {
assert.Equal(t, expectedPath, p)
}
// assert.Equal(t, nil)
}
package bl
import (
"github.com/datreeio/datree/pkg/cliClient"
"github.com/datreeio/datree/pkg/printer"
)
type VersionMessageClient interface {
GetVersionMessage(cliVersion string) (*cliClient.VersionMessage, error)
}
func PopulateVersionMessageChan(c VersionMessageClient, cliVersion string) <-chan *cliClient.VersionMessage {
messageChannel := make(chan *cliClient.VersionMessage)
go func() {
msg, _ := c.GetVersionMessage(cliVersion)
if msg != nil {
messageChannel <- msg
}
close(messageChannel)
}()
return messageChannel
}
func HandleVersionMessage(messageChannel <-chan *cliClient.VersionMessage) {
msg, ok := <-messageChannel
if ok {
p := printer.CreateNewPrinter()
p.PrintVersionMessage(msg.MessageText+"\n", msg.MessageColor)
}
}
package cmd
import (
"github.com/datreeio/datree/bl"
"github.com/datreeio/datree/bl/evaluation"
"github.com/datreeio/datree/bl/messager"
"github.com/datreeio/datree/cmd/test"
"github.com/datreeio/datree/cmd/version"
"github.com/datreeio/datree/pkg/cliClient"
"github.com/datreeio/datree/pkg/deploymentConfig"
"github.com/datreeio/datree/pkg/localConfig"
"github.com/datreeio/datree/pkg/printer"
"github.com/datreeio/datree/pkg/propertiesExtractor"
"github.com/spf13/cobra"
)
......@@ -23,16 +23,18 @@ var CliVersion string
func init() {
app := startup()
rootCmd.AddCommand(test.NewTestCommand(&test.TestCommandContext{
CliVersion: CliVersion,
Evaluator: app.Context.Evaluator,
LocalConfig: app.Context.LocalConfig,
VersionMessageClient: app.Context.VersionMessageClient,
rootCmd.AddCommand(test.New(&test.TestCommandContext{
CliVersion: CliVersion,
Evaluator: app.context.Evaluator,
LocalConfig: app.context.LocalConfig,
Messager: app.context.Messager,
Printer: app.context.Printer,
}))
rootCmd.AddCommand(version.NewVersionCommand(&version.VersionCommandContext{
CliVersion: CliVersion,
VersionMessageClient: app.Context.VersionMessageClient,
rootCmd.AddCommand(version.New(&version.VersionCommandContext{
CliVersion: CliVersion,
Messager: app.context.Messager,
Printer: app.context.Printer,
}))
}
......@@ -40,34 +42,29 @@ func Execute() error {
return rootCmd.Execute()
}
type context struct {
LocalConfig *localConfig.LocalConfiguration
Evaluator *evaluation.Evaluator
CliClient *cliClient.CliClient
Messager *messager.Messager
Printer *printer.Printer
}
type app struct {
Context struct {
LocalConfig *localConfig.LocalConfiguration
Evaluator *bl.Evaluator
CliClient *cliClient.CliClient
VersionMessageClient cliClient.VersionMessageClient
}
context *context
}
func startup() *app {
app := &app{
Context: struct {
LocalConfig *localConfig.LocalConfiguration
Evaluator *bl.Evaluator
CliClient *cliClient.CliClient
VersionMessageClient cliClient.VersionMessageClient
}{},
}
client := cliClient.NewCliClient(deploymentConfig.URL)
versionMessageClient := cliClient.NewVersionMessageClient(deploymentConfig.URL)
extractor := propertiesExtractor.NewPropertiesExtractor(nil)
config, _ := localConfig.GetLocalConfiguration()
cliClient := cliClient.NewCliClient(deploymentConfig.URL)
printer := printer.CreateNewPrinter()
evaluator := bl.CreateNewEvaluator(extractor, client, printer)
app.Context.LocalConfig = &localConfig.LocalConfiguration{}
app.Context.Evaluator = evaluator
app.Context.VersionMessageClient = versionMessageClient
return app
return &app{
context: &context{
LocalConfig: config,
Evaluator: evaluation.New(cliClient),
CliClient: cliClient,
Messager: messager.New(cliClient),
Printer: printer,
},
}
}
......@@ -4,35 +4,51 @@ import (
"fmt"
"time"
"github.com/datreeio/datree/pkg/cliClient"
"github.com/briandowns/spinner"
"github.com/datreeio/datree/bl"
"github.com/datreeio/datree/bl/evaluation"
"github.com/datreeio/datree/bl/messager"
"github.com/datreeio/datree/bl/validation"
"github.com/datreeio/datree/pkg/localConfig"
"github.com/datreeio/datree/pkg/propertiesExtractor"
"github.com/datreeio/datree/pkg/printer"
"github.com/spf13/cobra"
)
type LocalConfigManager interface {
GetConfiguration() (localConfig.LocalConfiguration, error)
type Evaluator interface {
Evaluate(validFilesPathsChan chan string, invalidFilesPaths chan *validation.InvalidFile, evaluationId int) (*evaluation.EvaluationResults, []*validation.InvalidFile, []*cliClient.FileConfiguration, []*evaluation.Error, error)
CreateEvaluation(cliId string, cliVersion string, k8sVersion string) (int, error)
}
type Evaluator interface {
PrintResults(results *bl.EvaluationResults, cliId string, output string) error
PrintFileParsingErrors(errors []propertiesExtractor.FileError)
Evaluate(paths []string, cliId string, evaluationConc int, cliVersion string) (*bl.EvaluationResults, []propertiesExtractor.FileError, error)
type Messager interface {
LoadVersionMessages(messages chan *messager.VersionMessage, cliVersion string)
}
type TestCommandContext struct {
CliVersion string
LocalConfig LocalConfigManager
Evaluator Evaluator
VersionMessageClient bl.VersionMessageClient
type K8sValidator interface {
ValidateResources(paths []string) (chan string, chan *validation.InvalidFile, chan error)
}
type TestCommandFlags struct {
Output string
Output string
K8sVersion string
}
type EvaluationPrinter interface {
PrintWarnings(warnings []printer.Warning)
PrintSummaryTable(summary printer.Summary)
PrintMessage(messageText string, messageColor string)
PrintEvaluationSummary(evaluationSummary printer.EvaluationSummary)
}
type TestCommandContext struct {
CliVersion string
LocalConfig *localConfig.LocalConfiguration
Evaluator Evaluator
Messager Messager
K8sValidator K8sValidator
Printer EvaluationPrinter
}
func NewTestCommand(ctx *TestCommandContext) *cobra.Command {
func New(ctx *TestCommandContext) *cobra.Command {
testCommand := &cobra.Command{
Use: "test",
Short: "Execute static analysis for pattern",
......@@ -44,7 +60,14 @@ func NewTestCommand(ctx *TestCommandContext) *cobra.Command {
return err
}
testCommandFlags := TestCommandFlags{Output: outputFlag}
k8sVersion, err := cmd.Flags().GetString("schema-version")
if err != nil {
fmt.Println(err)
return err
}
testCommandFlags := TestCommandFlags{Output: outputFlag, K8sVersion: k8sVersion}
ctx.K8sValidator = validation.New(k8sVersion)
return test(ctx, args, testCommandFlags)
},
SilenceUsage: true,
......@@ -52,42 +75,74 @@ func NewTestCommand(ctx *TestCommandContext) *cobra.Command {
}
testCommand.Flags().StringP("output", "o", "", "Define output format")
testCommand.Flags().StringP("schema-version", "s", "1.18.0", "Set kubernetes version to validate against. Defaults to 1.18.0")
return testCommand
}
func test(ctx *TestCommandContext, paths []string, flags TestCommandFlags) error {
s := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
spinner := createSpinner(" Loading...", "cyan")
spinner.Start()
s.Suffix = " Loading..."
s.Color("cyan")
s.Start()
messages := make(chan *messager.VersionMessage, 1)
go ctx.Messager.LoadVersionMessages(messages, ctx.CliVersion)
messageChannel := bl.PopulateVersionMessageChan(ctx.VersionMessageClient, ctx.CliVersion)
config, err := ctx.LocalConfig.GetConfiguration()
evaluationId, err := ctx.Evaluator.CreateEvaluation(ctx.LocalConfig.CliId, ctx.CliVersion, flags.K8sVersion)
if err != nil {
fmt.Println(err.Error())
return err
}
evaluationResponse, fileParsingErrors, err := ctx.Evaluator.Evaluate(paths, config.CliId, 50, ctx.CliVersion)
s.Stop()
validFilesPaths, invalidFilesPathsChan, errorsChan := ctx.K8sValidator.ValidateResources(paths)
go func() {
for err := range errorsChan {
fmt.Println(err)
}
}()
results, invalidFiles, filesConfigurations, errors, err := ctx.Evaluator.Evaluate(validFilesPaths, invalidFilesPathsChan, evaluationId)
spinner.Stop()
if err != nil {
if len(fileParsingErrors) > 0 {
ctx.Evaluator.PrintFileParsingErrors(fileParsingErrors)
}
fmt.Println(err.Error())
return err
}
if evaluationResponse == nil {
err := fmt.Errorf("no response received")
return err
if len(errors) > 0 {
printEvaluationErrors(errors)
}
err = ctx.Evaluator.PrintResults(evaluationResponse, config.CliId, flags.Output)
bl.HandleVersionMessage(messageChannel)
passedPolicyCheckCount := 0
if results != nil {
passedPolicyCheckCount = results.Summary.TotalPassedCount
}
evaluationSummary := printer.EvaluationSummary{
FilesCount: len(paths),
PassedYamlValidationCount: len(paths),
PassedK8sValidationCount: len(filesConfigurations),
PassedPolicyCheckCount: passedPolicyCheckCount,
}
evaluation.PrintResults(results, invalidFiles, evaluationSummary, fmt.Sprintf("https://app.datree.io/login?cliId=%s", ctx.LocalConfig.CliId), flags.Output, ctx.Printer)
msg, ok := <-messages
if ok {
ctx.Printer.PrintMessage(msg.MessageText+"\n", msg.MessageColor)
}
return err
}
func createSpinner(text string, color string) *spinner.Spinner {
s := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
s.Suffix = text
s.Color(color)
return s
}
func printEvaluationErrors(errors []*evaluation.Error) {
fmt.Println("The following files failed:")
for _, fileError := range errors {
fmt.Printf("\n\tFilename: %s\n\tError: %s\n\t---------------------", fileError.Filename, fileError.Message)
}
}
package test
import (
"testing"
"github.com/datreeio/datree/bl"
"github.com/datreeio/datree/pkg/cliClient"
"github.com/datreeio/datree/pkg/localConfig"
"github.com/datreeio/datree/pkg/propertiesExtractor"
"github.com/stretchr/testify/mock"
)
type mockLocalConfigManager struct {
mock.Mock
}
func (m *mockLocalConfigManager) GetConfiguration() (localConfig.LocalConfiguration, error) {
args := m.Called()
return args.Get(0).(localConfig.LocalConfiguration), args.Error(1)
}
type mockEvaluator struct {
mock.Mock
}
func (m *mockEvaluator) PrintResults(results *bl.EvaluationResults, cliId string, output string) error {
m.Called(results, cliId, output)
return nil
}
func (m *mockEvaluator) PrintFileParsingErrors(errors []propertiesExtractor.FileError) {
m.Called(errors)
}
func (m *mockEvaluator) Evaluate(paths []string, cliId string, evaluationConc int, cliVersion string) (*bl.EvaluationResults, []propertiesExtractor.FileError, error) {
args := m.Called(paths, cliId, evaluationConc)
return args.Get(0).(*bl.EvaluationResults), args.Get(1).([]propertiesExtractor.FileError), args.Error(2)
}
type mockVersionMessageClient struct {
mock.Mock
}
func (m *mockVersionMessageClient) GetVersionMessage(cliVersion string) (*cliClient.VersionMessage, error) {
args := m.Called(cliVersion)
return args.Get(0).(*cliClient.VersionMessage), nil
}
func TestTestCommand(t *testing.T) {
evaluator := &mockEvaluator{}
mockedEvaluateResponse := &bl.EvaluationResults{
FileNameRuleMapper: map[string]map[int]*bl.Rule{}, Summary: struct {
RulesCount int
TotalFailedRules int
FilesCount int
}{RulesCount: 1, TotalFailedRules: 0, FilesCount: 0},
}
evaluator.On("Evaluate", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(mockedEvaluateResponse, []propertiesExtractor.FileError{}, nil)
evaluator.On("PrintFileParsingErrors", mock.Anything).Return()
evaluator.On("PrintResults", mock.Anything, mock.Anything, mock.Anything).Return()
localConfigManager := &mockLocalConfigManager{}
localConfigManager.On("GetConfiguration").Return(localConfig.LocalConfiguration{CliId: "134kh"}, nil)
versionMessageClient := &mockVersionMessageClient{}
versionMessageClient.On("GetVersionMessage", mock.Anything).Return(
&cliClient.VersionMessage{
CliVersion: "1.2.3",
MessageText: "version message mock",
MessageColor: "green"},
)
ctx := &TestCommandContext{
Evaluator: evaluator,
LocalConfig: localConfigManager,
VersionMessageClient: versionMessageClient,
}
test_testCommand_no_flags(t, localConfigManager, evaluator, mockedEvaluateResponse, ctx)
test_testCommand_json_output(t, localConfigManager, evaluator, mockedEvaluateResponse, ctx)
test_testCommand_yaml_output(t, localConfigManager, evaluator, mockedEvaluateResponse, ctx)
}
func test_testCommand_no_flags(t *testing.T, localConfigManager *mockLocalConfigManager, evaluator *mockEvaluator, mockedEvaluateResponse *bl.EvaluationResults, ctx *TestCommandContext) {
test(ctx, []string{"8/*"}, TestCommandFlags{})
localConfigManager.AssertCalled(t, "GetConfiguration")
evaluator.AssertCalled(t, "Evaluate", []string{"8/*"}, "134kh", 50)
evaluator.AssertNotCalled(t, "PrintFileParsingErrors")
evaluator.AssertCalled(t, "PrintResults", mockedEvaluateResponse, "134kh", "")
}
func test_testCommand_json_output(t *testing.T, localConfigManager *mockLocalConfigManager, evaluator *mockEvaluator, mockedEvaluateResponse *bl.EvaluationResults, ctx *TestCommandContext) {
test(ctx, []string{"8/*"}, TestCommandFlags{Output: "json"})
localConfigManager.AssertCalled(t, "GetConfiguration")
evaluator.AssertCalled(t, "Evaluate", []string{"8/*"}, "134kh", 50)
evaluator.AssertNotCalled(t, "PrintFileParsingErrors")
evaluator.AssertCalled(t, "PrintResults", mockedEvaluateResponse, "134kh", "json")
}
func test_testCommand_yaml_output(t *testing.T, localConfigManager *mockLocalConfigManager, evaluator *mockEvaluator, mockedEvaluateResponse *bl.EvaluationResults, ctx *TestCommandContext) {
test(ctx, []string{"8/*"}, TestCommandFlags{Output: "yaml"})
localConfigManager.AssertCalled(t, "GetConfiguration")
evaluator.AssertCalled(t, "Evaluate", []string{"8/*"}, "134kh", 50)
evaluator.AssertNotCalled(t, "PrintFileParsingErrors")
evaluator.AssertCalled(t, "PrintResults", mockedEvaluateResponse, "134kh", "yaml")
}
//import (
// "testing"
//
// "github.com/datreeio/datree/bl/evaluation"
// "github.com/datreeio/datree/bl/messager"
// "github.com/datreeio/datree/pkg/cliClient"
// "github.com/datreeio/datree/pkg/localConfig"
// "github.com/stretchr/testify/mock"
//)
//
//type mockLocalConfigManager struct {
// mock.Mock
//}
//
//func (m *mockLocalConfigManager) GetConfiguration() (localConfig.LocalConfiguration, error) {
// args := m.Called()
// return args.Get(0).(localConfig.LocalConfiguration), args.Error(1)
//}
//
//type mockEvaluator struct {
// mock.Mock
//}
//
//func (m *mockEvaluator) Evaluate(validFilesPathsChan chan string, invalidFilesPaths []*string, evaluationId int) (*evaluation.EvaluationResults, []*evaluation.Error, error) {
// args := m.Called(validFilesPathsChan, invalidFilesPaths, evaluationId)
// return args.Get(0).(*evaluation.EvaluationResults), args.Get(1).([]*evaluation.Error), args.Error(2)
//}
//
//func (m *mockEvaluator) CreateEvaluation(cliId string, cliVersion string) (int, error) {
// args := m.Called(cliId, cliVersion)
// return args.Get(0).(int), args.Error(1)
//}
//
//type mockMessager struct {
// mock.Mock
//}
//
//func (m *mockMessager) LoadVersionMessages(messages chan *messager.VersionMessage, cliVersion string) {
// m.Called(messages, cliVersion)
//}
//
//func (m *mockMessager) HandleVersionMessage(messageChannel <-chan *messager.VersionMessage) {
// m.Called(messageChannel)
//}
//func TestTestCommand(t *testing.T) {
// mockedEvaluator := &mockEvaluator{}
//
// mockedEvaluateResponse := &evaluation.EvaluationResults{
// FileNameRuleMapper: map[string]map[int]*evaluation.Rule{}, Summary: struct {
// RulesCount int
// TotalFailedRules int
// FilesCount int
// }{RulesCount: 1, TotalFailedRules: 0, FilesCount: 0},
// }
//
// mockedEvaluator.On("Evaluate", mock.Anything, mock.Anything, mock.Anything).Return(mockedEvaluateResponse)
// mockedEvaluator.On("CreateEvaluation", mock.Anything, mock.Anything).Return(mockedEvaluateResponse)
//
// localConfigManager := &mockLocalConfigManager{}
// localConfigManager.On("GetConfiguration").Return(localConfig.LocalConfiguration{CliId: "134kh"}, nil)
//
// messager := &mockMessager{}
// messager.On("LoadVersionMessages", mock.Anything).Return(mockedMessagesChannel())
//
// ctx := &TestCommandContext{
// Evaluator: mockedEvaluator,
// LocalConfig: &localConfig.LocalConfiguration{},
// Messager: messager,
// }
//
// test_testCommand_no_flags(t, localConfigManager, mockedEvaluator, mockedEvaluateResponse, ctx)
// test_testCommand_json_output(t, localConfigManager, mockedEvaluator, mockedEvaluateResponse, ctx)
// test_testCommand_yaml_output(t, localConfigManager, mockedEvaluator, mockedEvaluateResponse, ctx)
//}
//
//func test_testCommand_no_flags(t *testing.T, localConfigManager *mockLocalConfigManager, evaluator *mockEvaluator, mockedEvaluateResponse *evaluation.EvaluationResults, ctx *TestCommandContext) {
// test(ctx, []string{"8/*"}, TestCommandFlags{})
// localConfigManager.AssertCalled(t, "GetConfiguration")
//
// evaluator.AssertCalled(t, "Evaluate", []string{"8/*"}, "134kh", 50)
// evaluator.AssertNotCalled(t, "PrintFileParsingErrors")
// evaluator.AssertCalled(t, "PrintResults", mockedEvaluateResponse, "134kh", "")
//}
//
//func test_testCommand_json_output(t *testing.T, localConfigManager *mockLocalConfigManager, evaluator *mockEvaluator, mockedEvaluateResponse *evaluation.EvaluationResults, ctx *TestCommandContext) {
// test(ctx, []string{"8/*"}, TestCommandFlags{Output: "json"})
// localConfigManager.AssertCalled(t, "GetConfiguration")
//
// evaluator.AssertCalled(t, "Evaluate", []string{"8/*"}, "134kh", 50)
// evaluator.AssertNotCalled(t, "PrintFileParsingErrors")
// evaluator.AssertCalled(t, "PrintResults", mockedEvaluateResponse, "134kh", "json")
//}
//
//func test_testCommand_yaml_output(t *testing.T, localConfigManager *mockLocalConfigManager, evaluator *mockEvaluator, mockedEvaluateResponse *evaluation.EvaluationResults, ctx *TestCommandContext) {
// test(ctx, []string{"8/*"}, TestCommandFlags{Output: "yaml"})
// localConfigManager.AssertCalled(t, "GetConfiguration")
//
// evaluator.AssertCalled(t, "Evaluate", []string{"8/*"}, "134kh", 50)
// evaluator.AssertNotCalled(t, "PrintFileParsingErrors")
// evaluator.AssertCalled(t, "PrintResults", mockedEvaluateResponse, "134kh", "yaml")
//}
//
//func mockedMessagesChannel() <-chan *cliClient.VersionMessage {
// mock := make(chan *cliClient.VersionMessage)
// mock <- &cliClient.VersionMessage{
// CliVersion: "1.2.3",
// MessageText: "version message mock",
// MessageColor: "green"}
// close(mock)
//
// return mock
//}
......@@ -3,22 +3,34 @@ package version
import (
"fmt"
"github.com/datreeio/datree/bl"
"github.com/datreeio/datree/bl/messager"
"github.com/spf13/cobra"
)
type Messager interface {
LoadVersionMessages(messages chan *messager.VersionMessage, cliVersion string)
}
type Printer interface {
PrintMessage(messageText string, messageColor string)
}
type VersionCommandContext struct {
CliVersion string
VersionMessageClient bl.VersionMessageClient
CliVersion string
Messager Messager
Printer Printer
}
func version(ctx *VersionCommandContext) {
messageChannel := bl.PopulateVersionMessageChan(ctx.VersionMessageClient, ctx.CliVersion)
messages := make(chan *messager.VersionMessage, 1)
go ctx.Messager.LoadVersionMessages(messages, ctx.CliVersion)
fmt.Println(ctx.CliVersion)
bl.HandleVersionMessage(messageChannel)
msg, ok := <-messages
if ok {
ctx.Printer.PrintMessage(msg.MessageText+"\n", msg.MessageColor)
}
}
func NewVersionCommand(ctx *VersionCommandContext) *cobra.Command {
func New(ctx *VersionCommandContext) *cobra.Command {
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the version number",
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment