diff --git a/chartvalidator/checker/engine_app_checker.go b/chartvalidator/checker/engine_app_checker.go index 6b9f261..adf46ad 100644 --- a/chartvalidator/checker/engine_app_checker.go +++ b/chartvalidator/checker/engine_app_checker.go @@ -92,7 +92,7 @@ func NewAppCheckerEngine(context context.Context, outputDir string, apiVersions return &AppCheckerEngine{ inputChan: make(chan AppCheckInstruction), resultChan: make(chan AppCheckResult), - errorChan: make(chan ErrorResult), + errorChan: errorChan, context: context, executor: &RealCommandExecutor{}, @@ -120,11 +120,15 @@ func (engine *AppCheckerEngine) Start(workerCount int) { engine.ImageExtractionEngine.Start(workerCount) engine.DockerValidationEngine.Start(workerCount) + go engine.closeErrorChanWhenEnginesDone() + // Pour the input instructions into the chart renderer engine.workerWaitGroup.Add(1) go engine.pumpAppCheckInstructionsToChartRenderer() engine.workerWaitGroup.Add(1) go engine.pumpOutputsToAppCheckResults() + engine.workerWaitGroup.Add(1) + go engine.pumpErrorsToAppCheckResults() go engine.allDoneWorker() } @@ -154,6 +158,18 @@ func (engine *AppCheckerEngine) pumpOutputsToAppCheckResults() { logEngineDebug(engine.name, -1, "docker validation output closed") } +func (engine *AppCheckerEngine) pumpErrorsToAppCheckResults() { + defer engine.workerWaitGroup.Done() + for errResult := range engine.errorChan { + engine.resultChan <- AppCheckResult{ + Chart: errResult.Chart, + Image: "", + Error: errResult.Error, + } + } + logEngineDebug(engine.name, -1, "error channel closed") +} + func (engine *AppCheckerEngine) pumpAppCheckInstructionsToChartRenderer() { defer engine.workerWaitGroup.Done() for instruction := range engine.inputChan { @@ -161,3 +177,12 @@ func (engine *AppCheckerEngine) pumpAppCheckInstructionsToChartRenderer() { } close(engine.ChartRenderingEngine.inputChan) } + +func (engine *AppCheckerEngine) closeErrorChanWhenEnginesDone() { + engine.ChartRenderingEngine.workerWaitGroup.Wait() + engine.ManifestValidationEngine.workerWaitGroup.Wait() + engine.ImageExtractionEngine.workerWaitGroup.Wait() + engine.DockerValidationEngine.workerWaitGroup.Wait() + logEngineDebug(engine.name, -1, "all child engines done, closing error channel") + close(engine.errorChan) +} diff --git a/chartvalidator/checker/engine_app_checker_test.go b/chartvalidator/checker/engine_app_checker_test.go new file mode 100644 index 0000000..204ed71 --- /dev/null +++ b/chartvalidator/checker/engine_app_checker_test.go @@ -0,0 +1,74 @@ +package main + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestAppCheckerEnginePropagatesRenderErrors(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + outputDir := t.TempDir() + + engine := NewAppCheckerEngine(ctx, outputDir, nil) + + mockExecutor := createMockExecutor() + mockExecutor.Error = fmt.Errorf("helm exploded") + mockExecutor.Output = []byte("nope") + mockExecutor.FileExistsMap = map[string]bool{ + "values.yaml": true, + "override.yaml": true, + } + + engine.ChartRenderingEngine.executor = mockExecutor + + engine.Start(1) + + testChart := ChartRenderParams{ + Env: "test", + ChartName: "chart", + RepoURL: "https://example.com/charts", + ChartVersion: "1.0.0", + BaseValuesFile: "values.yaml", + ValuesOverride: "override.yaml", + } + + go func() { + engine.inputChan <- AppCheckInstruction{Chart: testChart} + close(engine.inputChan) + }() + + var ( + result AppCheckResult + ok bool + ) + + select { + case result, ok = <-engine.resultChan: + if !ok { + t.Fatalf("result channel closed before emitting result") + } + case <-time.After(2 * time.Second): + t.Fatalf("timeout waiting for app checker result (possible deadlock)") + } + + assert.Equal(t, testChart.ChartName, result.Chart.ChartName) + assert.Error(t, result.Error) + if result.Error != nil { + assert.Contains(t, result.Error.Error(), "helm command failed") + } + + select { + case _, ok = <-engine.resultChan: + if ok { + t.Fatalf("expected result channel to close after delivering error result") + } + case <-time.After(2 * time.Second): + t.Fatalf("timeout waiting for result channel to close") + } +}