//go:build integration // NOTE: requires a Kubernetes cluster in place currently, hence hidden behind a tag above // TODOLATER: get a mocked or ephemeral/Docker based K8s for use? see below some approaches which haven't played out yet // Test using: go test --tags=integration package preflight import ( "errors" "os" "path/filepath" "testing" "github.com/replicatedhq/troubleshoot/internal/testutils" analyzerunner "github.com/replicatedhq/troubleshoot/pkg/analyze" "github.com/replicatedhq/troubleshoot/pkg/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" /* See TODOLATER below "k8s.io/client-go/kubernetes/fake" k8sruntime "k8s.io/apimachinery/pkg/runtime" discoveryfake "k8s.io/client-go/discovery/fake" */) func TestRunPreflights(t *testing.T) { t.Parallel() // A very simple preflight spec (local file) preflightFile := filepath.Join(testutils.FileDir(), "../../testdata/preflightspec/troubleshoot_v1beta2_preflight_gotest.yaml") wantOutputContentHuman := ` --- PASS Compare JSON Example --- The collected data matches the value. --- PASS go-test-preflight PASS ` wantOutputContentJson := `{ "pass": [ { "title": "Compare JSON Example", "message": "The collected data matches the value." } ] } ` wantOutputContentYaml := `pass: - title: Compare JSON Example message: The collected data matches the value. ` tests := []struct { name string interactive bool output string format string args []string // wantExitCode int wantErr bool // May be in stdout or file, depending on above value wantOutputContent string }{ // TODOLATER: test interactive true as well { name: "noninteractive-stdout-human", interactive: false, output: "", format: "human", args: []string{preflightFile}, wantExitCode: 0, wantErr: false, wantOutputContent: wantOutputContentHuman, }, { name: "noninteractive-stdout-json", interactive: false, output: "", format: "json", args: []string{preflightFile}, wantExitCode: 0, wantErr: false, wantOutputContent: wantOutputContentJson, }, { name: "noninteractive-stdout-yaml", interactive: false, output: "", format: "yaml", args: []string{preflightFile}, wantExitCode: 0, wantErr: false, wantOutputContent: wantOutputContentYaml, }, { name: "noninteractive-tofile-human", interactive: false, output: testutils.TempFilename("preflight_out_test_"), format: "human", args: []string{preflightFile}, wantExitCode: 0, wantErr: false, wantOutputContent: wantOutputContentHuman, }, { name: "noninteractive-tofile-json", interactive: false, output: testutils.TempFilename("preflight_out_test_"), format: "json", args: []string{preflightFile}, wantExitCode: 0, wantErr: false, wantOutputContent: wantOutputContentJson, }, { name: "noninteractive-tofile-yaml", interactive: false, output: testutils.TempFilename("preflight_out_test_"), format: "yaml", args: []string{preflightFile}, wantExitCode: 0, wantErr: false, wantOutputContent: wantOutputContentYaml, }, } // Use a fake/mocked K8s API, since some collectors are mandatory and need an API server to hit // TODOLATER: for this to work, we need to refactor all of the collectors and analyzers to allow passing in a fake clientset? // ...or we need to find a way to expose the fake clientset with a mocked API server and a respective kubeconfig for use // Using gnomock k3s for now instead (requires Docker for test execution) /* k8sObjects := []k8sruntime.Object{ // TODO: populate with things that mandatory collectors need } k8sApi := fake.NewSimpleClientset(k8sObjects...) k8sApi.Discovery().(*discoveryfake.FakeDiscovery).FakedServerVersion = &version.Info{ Major: "1", Minor: "26", } fmt.Printf("%+v\n", k8sApi) */ // A K3s instance in Docker for testing, for mandatory collectors // NOTE: only has amd64 images, doesn't work on arm64 (MacOS)? need to build and specify custom images for it // ...plus there's no new images since 2020~? /* p := k3s.Preset(k3s.WithVersion("v1.19.12")) c, err := gnomock.Start( p, gnomock.WithContainerName("k3s"), gnomock.WithDebugMode(), ) if err != nil { t.Fatal(err) } kubeconfig, err := k3s.Config(c) if err != nil { t.Fatal(err) } fmt.Printf("kubeconfig: %+v\n", kubeconfig) */ for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Capture stdout/stderr along the way rOut, wOut, err := os.Pipe() require.Nil(t, err) rErr, wErr, err := os.Pipe() require.Nil(t, err) // Redirect stdout/err to the pipes temporarily origStdout := os.Stdout os.Stdout = wOut origStderr := os.Stderr os.Stderr = wErr tErr := RunPreflights(tt.interactive, tt.output, tt.format, tt.args) // Stop redirection of stdout/stderr bufOut := make([]byte, 1024) nOut, err := rOut.Read(bufOut) require.Nil(t, err) bufErr := make([]byte, 1024) // nErr _, err = rErr.Read(bufErr) require.Nil(t, err) os.Stdout = origStdout os.Stderr = origStderr if tt.wantErr { // It's always an error of some form assert.Error(t, tErr) var exitErr types.ExitError // Make sure we can always turn it into an ExitError assert.True(t, errors.As(tErr, &exitErr)) assert.Equal(t, tt.wantExitCode, exitErr.ExitStatus()) assert.NotEmpty(t, exitErr.Error()) } else { assert.Nil(t, tErr) } useBufOut := string(bufOut[:nOut]) //useBufErr := string(bufErr[:nErr]) //fmt.Printf("stdout: %+v\n", useBufOut) //fmt.Printf("stderr: %+v\n", useBufErr) if tt.output != "" { // Output file is expected, make sure it exists assert.FileExists(t, tt.output) // If it exists, check contents of output file against expected readOutputFile, err := os.ReadFile(tt.output) require.NoError(t, err) assert.Equal(t, string(readOutputFile), tt.wantOutputContent) } else { // Expected no output file, make sure it doesn't exist assert.NoFileExists(t, tt.output) // Check stdout against expected output assert.Equal(t, useBufOut, tt.wantOutputContent) } // Remove the (temp) output file if it exists if _, err := os.Stat(tt.output); err == nil { err = os.Remove(tt.output) require.NoError(t, err) } }) } } func TestCheckOutcomesToExitCode(t *testing.T) { tests := []struct { name string results []*analyzerunner.AnalyzeResult wantExitCode int }{ { name: "all-pass", results: []*analyzerunner.AnalyzeResult{ &analyzerunner.AnalyzeResult{ IsPass: true, IsFail: false, IsWarn: false, }, &analyzerunner.AnalyzeResult{ IsPass: true, IsFail: false, IsWarn: false, }, }, wantExitCode: 0, }, { name: "one-warn", results: []*analyzerunner.AnalyzeResult{ &analyzerunner.AnalyzeResult{ IsPass: true, IsFail: false, IsWarn: false, }, &analyzerunner.AnalyzeResult{ IsPass: false, IsFail: false, IsWarn: true, }, }, wantExitCode: 4, }, { name: "one-fail", results: []*analyzerunner.AnalyzeResult{ &analyzerunner.AnalyzeResult{ IsPass: true, IsFail: false, IsWarn: false, }, &analyzerunner.AnalyzeResult{ IsPass: false, IsFail: true, IsWarn: false, }, }, wantExitCode: 3, }, { name: "one-warn-one-fail", results: []*analyzerunner.AnalyzeResult{ &analyzerunner.AnalyzeResult{ IsPass: true, IsFail: false, IsWarn: false, }, &analyzerunner.AnalyzeResult{ IsPass: false, IsFail: true, IsWarn: false, }, &analyzerunner.AnalyzeResult{ IsPass: false, IsFail: false, IsWarn: true, }, }, // A fail is a fail... wantExitCode: 3, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotExitCode := checkOutcomesToExitCode(tt.results) assert.Equal(t, tt.wantExitCode, gotExitCode) }) } }