Repository: replicatedhq/troubleshoot
Pull request #872 (2022-11-25 13:50:56)

chore: Add GH workflow to ensure schemas are generated in a PR
## Description, Motivation and Context

Often after making changes to a code base we forget to run `make schemas` which generates k8s schemas for troubleshoot CRDs. Some external projects such as [kots lint](https://github.com/replicatedhq/kots-lint) depend on these schemas being in sync with the code base.

<!--- If it relates to an open issue, please link to the issue here.
e.g.
Fixes: #414
-->

## Checklist

- [x] New and existing tests pass locally with the changes.
- [x] Tests for the changes have been added (for bug fixes / features)
- [x] The commit message(s) are informative and highlight any breaking changes

## Does this PR introduce a breaking change?
- [ ] Yes
- [x] No

<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->



banjoh: And here is my test case of a failed PR => https://github.com/replicatedhq/troubleshoot/actions/runs/3548513932/jobs/5959805671


Patch:
diff --git a/Makefile b/Makefile
index 69888fa6..641fe9e4 100644
--- a/Makefile
+++ b/Makefile
@@ -95,6 +95,13 @@ openapischema: controller-gen
 	controller-gen crd +output:dir=./config/crds  paths=./pkg/apis/troubleshoot/v1beta1
 	controller-gen crd +output:dir=./config/crds  paths=./pkg/apis/troubleshoot/v1beta2
 
+check-schemas: generate schemas
+	@if [ -n "$(shell git status --short)" ]; then \
+    	echo -e "\033[31mThe git repo is dirty :( Ensure all generated files are committed e.g CRD schema files\033[0;m"; \
+    	git status --short; \
+    	exit 1; \
+	fi
+
 .PHONY: schemas
 schemas: fmt vet openapischema
 	go build ${LDFLAGS} -o bin/schemagen github.com/replicatedhq/troubleshoot/cmd/schemagen
diff --git a/config/crds/troubleshoot.sh_analyzers.yaml b/config/crds/troubleshoot.sh_analyzers.yaml
index 24acc7b4..52d562e8 100644
--- a/config/crds/troubleshoot.sh_analyzers.yaml
+++ b/config/crds/troubleshoot.sh_analyzers.yaml
@@ -1269,6 +1269,10 @@ spec:
                           type: string
                         exclude:
                           type: BoolString
+                        excludeFiles:
+                          items:
+                            type: string
+                          type: array
                         fileName:
                           type: string
                         ignoreIfNoFiles:
diff --git a/config/crds/troubleshoot.sh_preflights.yaml b/config/crds/troubleshoot.sh_preflights.yaml
index 3797c56b..93328fe8 100644
--- a/config/crds/troubleshoot.sh_preflights.yaml
+++ b/config/crds/troubleshoot.sh_preflights.yaml
@@ -1269,6 +1269,10 @@ spec:
                           type: string
                         exclude:
                           type: BoolString
+                        excludeFiles:
+                          items:
+                            type: string
+                          type: array
                         fileName:
                           type: string
                         ignoreIfNoFiles:
diff --git a/config/crds/troubleshoot.sh_supportbundles.yaml b/config/crds/troubleshoot.sh_supportbundles.yaml
index 5c092b7c..00f30dcf 100644
--- a/config/crds/troubleshoot.sh_supportbundles.yaml
+++ b/config/crds/troubleshoot.sh_supportbundles.yaml
@@ -1300,6 +1300,10 @@ spec:
                           type: string
                         exclude:
                           type: BoolString
+                        excludeFiles:
+                          items:
+                            type: string
+                          type: array
                         fileName:
                           type: string
                         ignoreIfNoFiles:
diff --git a/pkg/analyze/analyzer.go b/pkg/analyze/analyzer.go
index b0bcf114..a96fa989 100644
--- a/pkg/analyze/analyzer.go
+++ b/pkg/analyze/analyzer.go
@@ -27,7 +27,7 @@ type AnalyzeResult struct {
 }
 
 type getCollectedFileContents func(string) ([]byte, error)
-type getChildCollectedFileContents func(string) (map[string][]byte, error)
+type getChildCollectedFileContents func(string, []string) (map[string][]byte, error)
 
 func isExcluded(excludeVal *multitype.BoolOrString) (bool, error) {
 	if excludeVal == nil {
diff --git a/pkg/analyze/cluster_pod_statuses.go b/pkg/analyze/cluster_pod_statuses.go
index 3d3d8b25..3b5a9c0c 100644
--- a/pkg/analyze/cluster_pod_statuses.go
+++ b/pkg/analyze/cluster_pod_statuses.go
@@ -14,8 +14,9 @@ import (
 	corev1 "k8s.io/api/core/v1"
 )
 
-func clusterPodStatuses(analyzer *troubleshootv1beta2.ClusterPodStatuses, getChildCollectedFileContents func(string) (map[string][]byte, error)) ([]*AnalyzeResult, error) {
-	collected, err := getChildCollectedFileContents(filepath.Join("cluster-resources", "pods", "*.json"))
+func clusterPodStatuses(analyzer *troubleshootv1beta2.ClusterPodStatuses, getChildCollectedFileContents getChildCollectedFileContents) ([]*AnalyzeResult, error) {
+	excludeFiles := []string{}
+	collected, err := getChildCollectedFileContents(filepath.Join("cluster-resources", "pods", "*.json"), excludeFiles)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to read collected pods")
 	}
diff --git a/pkg/analyze/deployment_status.go b/pkg/analyze/deployment_status.go
index e55fb9e7..1c61aa7b 100644
--- a/pkg/analyze/deployment_status.go
+++ b/pkg/analyze/deployment_status.go
@@ -10,7 +10,7 @@ import (
 	appsv1 "k8s.io/api/apps/v1"
 )
 
-func analyzeDeploymentStatus(analyzer *troubleshootv1beta2.DeploymentStatus, getFileContents func(string) (map[string][]byte, error)) ([]*AnalyzeResult, error) {
+func analyzeDeploymentStatus(analyzer *troubleshootv1beta2.DeploymentStatus, getFileContents getChildCollectedFileContents) ([]*AnalyzeResult, error) {
 	if analyzer.Name == "" {
 		return analyzeAllDeploymentStatuses(analyzer, getFileContents)
 	} else {
@@ -18,8 +18,9 @@ func analyzeDeploymentStatus(analyzer *troubleshootv1beta2.DeploymentStatus, get
 	}
 }
 
-func analyzeOneDeploymentStatus(analyzer *troubleshootv1beta2.DeploymentStatus, getFileContents func(string) (map[string][]byte, error)) ([]*AnalyzeResult, error) {
-	files, err := getFileContents(filepath.Join("cluster-resources", "deployments", fmt.Sprintf("%s.json", analyzer.Namespace)))
+func analyzeOneDeploymentStatus(analyzer *troubleshootv1beta2.DeploymentStatus, getFileContents getChildCollectedFileContents) ([]*AnalyzeResult, error) {
+	excludeFiles := []string{}
+	files, err := getFileContents(filepath.Join("cluster-resources", "deployments", fmt.Sprintf("%s.json", analyzer.Namespace)), excludeFiles)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to read collected deployments from namespace")
 	}
@@ -61,7 +62,8 @@ func analyzeOneDeploymentStatus(analyzer *troubleshootv1beta2.DeploymentStatus,
 	return []*AnalyzeResult{result}, nil
 }
 
-func analyzeAllDeploymentStatuses(analyzer *troubleshootv1beta2.DeploymentStatus, getFileContents func(string) (map[string][]byte, error)) ([]*AnalyzeResult, error) {
+func analyzeAllDeploymentStatuses(analyzer *troubleshootv1beta2.DeploymentStatus, getFileContents getChildCollectedFileContents) ([]*AnalyzeResult, error) {
+	excludeFiles := []string{}
 	fileNames := make([]string, 0)
 	if analyzer.Namespace != "" {
 		fileNames = append(fileNames, filepath.Join("cluster-resources", "deployments", fmt.Sprintf("%s.json", analyzer.Namespace)))
@@ -77,7 +79,7 @@ func analyzeAllDeploymentStatuses(analyzer *troubleshootv1beta2.DeploymentStatus
 
 	results := []*AnalyzeResult{}
 	for _, fileName := range fileNames {
-		files, err := getFileContents(fileName)
+		files, err := getFileContents(fileName, excludeFiles)
 		if err != nil {
 			return nil, errors.Wrap(err, "failed to read collected deployments from file")
 		}
diff --git a/pkg/analyze/download.go b/pkg/analyze/download.go
index b9bae87d..6b5cbe7c 100644
--- a/pkg/analyze/download.go
+++ b/pkg/analyze/download.go
@@ -268,11 +268,42 @@ func (f fileContentProvider) getFileContents(fileName string) ([]byte, error) {
 	return ioutil.ReadFile(filepath.Join(f.rootDir, fileName))
 }
 
-func (f fileContentProvider) getChildFileContents(dirName string) (map[string][]byte, error) {
+func excludeFilePaths(files, excludeFiles []string) []string {
+	mapExcludeFiles := make(map[string]struct{}, len(excludeFiles))
+
+	for _, path := range excludeFiles {
+		mapExcludeFiles[path] = struct{}{}
+	}
+
+	var nonexcludedFiles []string
+
+	for _, path := range files {
+		if _, found := mapExcludeFiles[path]; !found {
+			nonexcludedFiles = append(nonexcludedFiles, path)
+		}
+	}
+
+	return nonexcludedFiles
+}
+
+func (f fileContentProvider) getChildFileContents(dirName string, excludeFiles []string) (map[string][]byte, error) {
 	files, err := filepath.Glob(filepath.Join(f.rootDir, dirName))
 	if err != nil {
 		return nil, errors.Wrapf(err, "invalid glob %q", dirName)
 	}
+
+	if len(excludeFiles) > 0 {
+		excludeFileNames := []string{}
+		for _, excludeFile := range excludeFiles {
+			excludeFileName, err := filepath.Glob(filepath.Join(f.rootDir, excludeFile))
+			if err != nil {
+				return nil, errors.Wrapf(err, "invalid glob %q", excludeFile)
+			}
+			excludeFileNames = append(excludeFileNames, excludeFileName...)
+		}
+		files = excludeFilePaths(files, excludeFileNames)
+	}
+
 	fileArr := map[string][]byte{}
 	for _, filePath := range files {
 		bytes, err := ioutil.ReadFile(filePath)
diff --git a/pkg/analyze/image_pull_secret.go b/pkg/analyze/image_pull_secret.go
index 2c773ae3..72d286c0 100644
--- a/pkg/analyze/image_pull_secret.go
+++ b/pkg/analyze/image_pull_secret.go
@@ -8,8 +8,9 @@ import (
 	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
 )
 
-func analyzeImagePullSecret(analyzer *troubleshootv1beta2.ImagePullSecret, getChildCollectedFileContents func(string) (map[string][]byte, error)) (*AnalyzeResult, error) {
-	imagePullSecrets, err := getChildCollectedFileContents("cluster-resources/image-pull-secrets")
+func analyzeImagePullSecret(analyzer *troubleshootv1beta2.ImagePullSecret, getChildCollectedFileContents getChildCollectedFileContents) (*AnalyzeResult, error) {
+	var excludeFiles = []string{}
+	imagePullSecrets, err := getChildCollectedFileContents("cluster-resources/image-pull-secrets", excludeFiles)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to get file contents for image pull secrets")
 	}
diff --git a/pkg/analyze/job_status.go b/pkg/analyze/job_status.go
index f7892501..86e2bd77 100644
--- a/pkg/analyze/job_status.go
+++ b/pkg/analyze/job_status.go
@@ -12,7 +12,7 @@ import (
 	batchv1 "k8s.io/api/batch/v1"
 )
 
-func analyzeJobStatus(analyzer *troubleshootv1beta2.JobStatus, getFileContents func(string) (map[string][]byte, error)) ([]*AnalyzeResult, error) {
+func analyzeJobStatus(analyzer *troubleshootv1beta2.JobStatus, getFileContents getChildCollectedFileContents) ([]*AnalyzeResult, error) {
 	if analyzer.Name == "" {
 		return analyzeAllJobStatuses(analyzer, getFileContents)
 	} else {
@@ -20,8 +20,9 @@ func analyzeJobStatus(analyzer *troubleshootv1beta2.JobStatus, getFileContents f
 	}
 }
 
-func analyzeOneJobStatus(analyzer *troubleshootv1beta2.JobStatus, getFileContents func(string) (map[string][]byte, error)) ([]*AnalyzeResult, error) {
-	files, err := getFileContents(filepath.Join("cluster-resources", "jobs", fmt.Sprintf("%s.json", analyzer.Namespace)))
+func analyzeOneJobStatus(analyzer *troubleshootv1beta2.JobStatus, getFileContents getChildCollectedFileContents) ([]*AnalyzeResult, error) {
+	excludeFiles := []string{}
+	files, err := getFileContents(filepath.Join("cluster-resources", "jobs", fmt.Sprintf("%s.json", analyzer.Namespace)), excludeFiles)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to read collected jobs from namespace")
 	}
@@ -67,7 +68,7 @@ func analyzeOneJobStatus(analyzer *troubleshootv1beta2.JobStatus, getFileContent
 	return []*AnalyzeResult{result}, nil
 }
 
-func analyzeAllJobStatuses(analyzer *troubleshootv1beta2.JobStatus, getFileContents func(string) (map[string][]byte, error)) ([]*AnalyzeResult, error) {
+func analyzeAllJobStatuses(analyzer *troubleshootv1beta2.JobStatus, getFileContents getChildCollectedFileContents) ([]*AnalyzeResult, error) {
 	fileNames := make([]string, 0)
 	if analyzer.Namespace != "" {
 		fileNames = append(fileNames, filepath.Join("cluster-resources", "jobs", fmt.Sprintf("%s.json", analyzer.Namespace)))
@@ -81,9 +82,10 @@ func analyzeAllJobStatuses(analyzer *troubleshootv1beta2.JobStatus, getFileConte
 		fileNames = append(fileNames, filepath.Join("cluster-resources", "jobs", "*.json"))
 	}
 
+	excludeFiles := []string{}
 	results := []*AnalyzeResult{}
 	for _, fileName := range fileNames {
-		files, err := getFileContents(fileName)
+		files, err := getFileContents(fileName, excludeFiles)
 		if err != nil {
 			return nil, errors.Wrap(err, "failed to read collected jobs from file")
 		}
diff --git a/pkg/analyze/longhorn.go b/pkg/analyze/longhorn.go
index d836a015..bb70a12b 100644
--- a/pkg/analyze/longhorn.go
+++ b/pkg/analyze/longhorn.go
@@ -17,16 +17,17 @@ import (
 	"gopkg.in/yaml.v2"
 )
 
-func longhorn(analyzer *troubleshootv1beta2.LonghornAnalyze, getCollectedFileContents func(string) ([]byte, error), findFiles func(string) (map[string][]byte, error)) ([]*AnalyzeResult, error) {
+func longhorn(analyzer *troubleshootv1beta2.LonghornAnalyze, getFileContents getCollectedFileContents, findFiles getChildCollectedFileContents) ([]*AnalyzeResult, error) {
 	ns := collect.DefaultLonghornNamespace
 	if analyzer.Namespace != "" {
 		ns = analyzer.Namespace
 	}
 
+	excludeFiles := []string{}
 	// get nodes.longhorn.io
 	nodesDir := collect.GetLonghornNodesDirectory(ns)
 	nodesGlob := filepath.Join(nodesDir, "*")
-	nodesYaml, err := findFiles(nodesGlob)
+	nodesYaml, err := findFiles(nodesGlob, excludeFiles)
 	if err != nil {
 		return nil, errors.Wrapf(err, "failed to find longhorn nodes files under %s", nodesDir)
 	}
@@ -44,7 +45,7 @@ func longhorn(analyzer *troubleshootv1beta2.LonghornAnalyze, getCollectedFileCon
 	// get replicas.longhorn.io
 	replicasDir := collect.GetLonghornReplicasDirectory(ns)
 	replicasGlob := filepath.Join(replicasDir, "*")
-	replicasYaml, err := findFiles(replicasGlob)
+	replicasYaml, err := findFiles(replicasGlob, excludeFiles)
 	if err != nil {
 		return nil, errors.Wrapf(err, "failed to find longhorn replicas files under %s", replicasDir)
 	}
@@ -62,7 +63,7 @@ func longhorn(analyzer *troubleshootv1beta2.LonghornAnalyze, getCollectedFileCon
 	// get engines.longhorn.io
 	enginesDir := collect.GetLonghornEnginesDirectory(ns)
 	enginesGlob := filepath.Join(enginesDir, "*")
-	enginesYaml, err := findFiles(enginesGlob)
+	enginesYaml, err := findFiles(enginesGlob, excludeFiles)
 	if err != nil {
 		return nil, errors.Wrapf(err, "failed to find longhorn engines files under %s", enginesDir)
 	}
@@ -80,7 +81,7 @@ func longhorn(analyzer *troubleshootv1beta2.LonghornAnalyze, getCollectedFileCon
 	// get volumes.longhorn.io
 	volumesDir := collect.GetLonghornVolumesDirectory(ns)
 	volumesGlob := filepath.Join(volumesDir, "*.yaml")
-	volumesYaml, err := findFiles(volumesGlob)
+	volumesYaml, err := findFiles(volumesGlob, excludeFiles)
 	if err != nil {
 		return nil, errors.Wrapf(err, "Failed to find longhorn volumes files under %s", volumesDir)
 	}
@@ -112,7 +113,7 @@ func longhorn(analyzer *troubleshootv1beta2.LonghornAnalyze, getCollectedFileCon
 	for _, volume := range volumes {
 		// get replica checksums for each volume if provided
 		checksumsGlob := filepath.Join(volumesDir, volume.Name, "replicachecksums", "*")
-		checksumFiles, err := findFiles(checksumsGlob)
+		checksumFiles, err := findFiles(checksumsGlob, excludeFiles)
 		if err != nil {
 			return nil, errors.Wrapf(err, "Failed to find longhorn replica checksums under %s", checksumsGlob)
 		}
diff --git a/pkg/analyze/replicaset_status.go b/pkg/analyze/replicaset_status.go
index 6ae38db0..fcb583a2 100644
--- a/pkg/analyze/replicaset_status.go
+++ b/pkg/analyze/replicaset_status.go
@@ -13,7 +13,7 @@ import (
 	"k8s.io/apimachinery/pkg/labels"
 )
 
-func analyzeReplicaSetStatus(analyzer *troubleshootv1beta2.ReplicaSetStatus, getFileContents func(string) (map[string][]byte, error)) ([]*AnalyzeResult, error) {
+func analyzeReplicaSetStatus(analyzer *troubleshootv1beta2.ReplicaSetStatus, getFileContents getChildCollectedFileContents) ([]*AnalyzeResult, error) {
 	if analyzer.Name == "" {
 		return analyzeAllReplicaSetStatuses(analyzer, getFileContents)
 	} else {
@@ -21,8 +21,9 @@ func analyzeReplicaSetStatus(analyzer *troubleshootv1beta2.ReplicaSetStatus, get
 	}
 }
 
-func analyzeOneReplicaSetStatus(analyzer *troubleshootv1beta2.ReplicaSetStatus, getFileContents func(string) (map[string][]byte, error)) ([]*AnalyzeResult, error) {
-	files, err := getFileContents(filepath.Join("cluster-resources", "replicasets", fmt.Sprintf("%s.json", analyzer.Namespace)))
+func analyzeOneReplicaSetStatus(analyzer *troubleshootv1beta2.ReplicaSetStatus, getFileContents getChildCollectedFileContents) ([]*AnalyzeResult, error) {
+	excludeFiles := []string{}
+	files, err := getFileContents(filepath.Join("cluster-resources", "replicasets", fmt.Sprintf("%s.json", analyzer.Namespace)), excludeFiles)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to read collected replicasets from namespace")
 	}
@@ -64,7 +65,7 @@ func analyzeOneReplicaSetStatus(analyzer *troubleshootv1beta2.ReplicaSetStatus,
 	return []*AnalyzeResult{result}, nil
 }
 
-func analyzeAllReplicaSetStatuses(analyzer *troubleshootv1beta2.ReplicaSetStatus, getFileContents func(string) (map[string][]byte, error)) ([]*AnalyzeResult, error) {
+func analyzeAllReplicaSetStatuses(analyzer *troubleshootv1beta2.ReplicaSetStatus, getFileContents getChildCollectedFileContents) ([]*AnalyzeResult, error) {
 	fileNames := make([]string, 0)
 	if analyzer.Namespace != "" {
 		fileNames = append(fileNames, filepath.Join("cluster-resources", "replicasets", fmt.Sprintf("%s.json", analyzer.Namespace)))
@@ -78,8 +79,9 @@ func analyzeAllReplicaSetStatuses(analyzer *troubleshootv1beta2.ReplicaSetStatus
 	}
 
 	results := []*AnalyzeResult{}
+	excludeFiles := []string{}
 	for _, fileName := range fileNames {
-		files, err := getFileContents(fileName)
+		files, err := getFileContents(fileName, excludeFiles)
 		if err != nil {
 			return nil, errors.Wrap(err, "failed to read collected replicaset from file")
 		}
diff --git a/pkg/analyze/statefulset_status.go b/pkg/analyze/statefulset_status.go
index d8c5391f..2678aa73 100644
--- a/pkg/analyze/statefulset_status.go
+++ b/pkg/analyze/statefulset_status.go
@@ -10,7 +10,7 @@ import (
 	appsv1 "k8s.io/api/apps/v1"
 )
 
-func analyzeStatefulsetStatus(analyzer *troubleshootv1beta2.StatefulsetStatus, getFileContents func(string) (map[string][]byte, error)) ([]*AnalyzeResult, error) {
+func analyzeStatefulsetStatus(analyzer *troubleshootv1beta2.StatefulsetStatus, getFileContents getChildCollectedFileContents) ([]*AnalyzeResult, error) {
 	if analyzer.Name == "" {
 		return analyzeAllStatefulsetStatuses(analyzer, getFileContents)
 	} else {
@@ -18,8 +18,9 @@ func analyzeStatefulsetStatus(analyzer *troubleshootv1beta2.StatefulsetStatus, g
 	}
 }
 
-func analyzeOneStatefulsetStatus(analyzer *troubleshootv1beta2.StatefulsetStatus, getFileContents func(string) (map[string][]byte, error)) ([]*AnalyzeResult, error) {
-	files, err := getFileContents(filepath.Join("cluster-resources", "statefulsets", fmt.Sprintf("%s.json", analyzer.Namespace)))
+func analyzeOneStatefulsetStatus(analyzer *troubleshootv1beta2.StatefulsetStatus, getFileContents getChildCollectedFileContents) ([]*AnalyzeResult, error) {
+	excludeFiles := []string{}
+	files, err := getFileContents(filepath.Join("cluster-resources", "statefulsets", fmt.Sprintf("%s.json", analyzer.Namespace)), excludeFiles)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to read collected statefulsets from namespace")
 	}
@@ -65,7 +66,7 @@ func analyzeOneStatefulsetStatus(analyzer *troubleshootv1beta2.StatefulsetStatus
 	return []*AnalyzeResult{result}, nil
 }
 
-func analyzeAllStatefulsetStatuses(analyzer *troubleshootv1beta2.StatefulsetStatus, getFileContents func(string) (map[string][]byte, error)) ([]*AnalyzeResult, error) {
+func analyzeAllStatefulsetStatuses(analyzer *troubleshootv1beta2.StatefulsetStatus, getFileContents getChildCollectedFileContents) ([]*AnalyzeResult, error) {
 	fileNames := make([]string, 0)
 	if analyzer.Namespace != "" {
 		fileNames = append(fileNames, filepath.Join("cluster-resources", "statefulsets", fmt.Sprintf("%s.json", analyzer.Namespace)))
@@ -79,9 +80,10 @@ func analyzeAllStatefulsetStatuses(analyzer *troubleshootv1beta2.StatefulsetStat
 		fileNames = append(fileNames, filepath.Join("cluster-resources", "statefulsets", "*.json"))
 	}
 
+	excludeFiles := []string{}
 	results := []*AnalyzeResult{}
 	for _, fileName := range fileNames {
-		files, err := getFileContents(fileName)
+		files, err := getFileContents(fileName, excludeFiles)
 		if err != nil {
 			return nil, errors.Wrap(err, "failed to read collected statefulsets from namespace")
 		}
diff --git a/pkg/analyze/sysctl.go b/pkg/analyze/sysctl.go
index 22688e2f..1e13f18c 100644
--- a/pkg/analyze/sysctl.go
+++ b/pkg/analyze/sysctl.go
@@ -16,8 +16,9 @@ import (
 // The when condition for outcomes in this analyzer is interpreted as "for some node".
 // For example, "when: net.ipv4.ip_forward = 0" is true if at least one node has IP forwarding
 // disabled.
-func analyzeSysctl(analyzer *troubleshootv1beta2.SysctlAnalyze, findFiles func(string) (map[string][]byte, error)) (*AnalyzeResult, error) {
-	files, err := findFiles("sysctl/*")
+func analyzeSysctl(analyzer *troubleshootv1beta2.SysctlAnalyze, findFiles getChildCollectedFileContents) (*AnalyzeResult, error) {
+	excludeFiles := []string{}
+	files, err := findFiles("sysctl/*", excludeFiles)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to find collected sysctl parameters")
 	}
diff --git a/pkg/analyze/text_analyze.go b/pkg/analyze/text_analyze.go
index 2179ac3e..6e14f6fd 100644
--- a/pkg/analyze/text_analyze.go
+++ b/pkg/analyze/text_analyze.go
@@ -13,9 +13,14 @@ import (
 	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
 )
 
-func analyzeTextAnalyze(analyzer *troubleshootv1beta2.TextAnalyze, getCollectedFileContents func(string) (map[string][]byte, error)) ([]*AnalyzeResult, error) {
+func analyzeTextAnalyze(analyzer *troubleshootv1beta2.TextAnalyze, getCollectedFileContents getChildCollectedFileContents) ([]*AnalyzeResult, error) {
 	fullPath := filepath.Join(analyzer.CollectorName, analyzer.FileName)
-	collected, err := getCollectedFileContents(fullPath)
+	excludeFiles := []string{}
+	for _, excludeFile := range analyzer.ExcludeFiles {
+		excludeFiles = append(excludeFiles, filepath.Join(analyzer.CollectorName, excludeFile))
+	}
+
+	collected, err := getCollectedFileContents(fullPath, excludeFiles)
 	if err != nil {
 		return nil, errors.Wrapf(err, "failed to read collected file name: %s", fullPath)
 	}
diff --git a/pkg/analyze/weave.go b/pkg/analyze/weave.go
index a52dfbb9..fd7462e8 100644
--- a/pkg/analyze/weave.go
+++ b/pkg/analyze/weave.go
@@ -39,8 +39,9 @@ type WeaveAttributes struct {
 	Name      string `json:"name"`
 }
 
-func analyzeWeaveReport(analyzer *troubleshootv1beta2.WeaveReportAnalyze, findFiles func(string) (map[string][]byte, error)) ([]*AnalyzeResult, error) {
-	files, err := findFiles(analyzer.ReportFileGlob)
+func analyzeWeaveReport(analyzer *troubleshootv1beta2.WeaveReportAnalyze, findFiles getChildCollectedFileContents) ([]*AnalyzeResult, error) {
+	excludeFiles := []string{}
+	files, err := findFiles(analyzer.ReportFileGlob, excludeFiles)
 	if err != nil {
 		return nil, errors.Wrapf(err, "failed to find weave report files in %q", analyzer.ReportFileGlob)
 	}
diff --git a/pkg/apis/troubleshoot/v1beta2/analyzer_shared.go b/pkg/apis/troubleshoot/v1beta2/analyzer_shared.go
index 8d35e49b..860599ed 100644
--- a/pkg/apis/troubleshoot/v1beta2/analyzer_shared.go
+++ b/pkg/apis/troubleshoot/v1beta2/analyzer_shared.go
@@ -129,6 +129,7 @@ type TextAnalyze struct {
 	RegexGroups     string     `json:"regexGroups,omitempty" yaml:"regexGroups,omitempty"`
 	IgnoreIfNoFiles bool       `json:"ignoreIfNoFiles,omitempty" yaml:"ignoreIfNoFiles,omitempty"`
 	Outcomes        []*Outcome `json:"outcomes" yaml:"outcomes"`
+	ExcludeFiles    []string   `json:"excludeFiles,omitempty" yaml:"excludeFiles,omitempty"`
 }
 
 type YamlCompare struct {
diff --git a/pkg/apis/troubleshoot/v1beta2/collector_shared.go b/pkg/apis/troubleshoot/v1beta2/collector_shared.go
index 31e88be0..7aa532ad 100644
--- a/pkg/apis/troubleshoot/v1beta2/collector_shared.go
+++ b/pkg/apis/troubleshoot/v1beta2/collector_shared.go
@@ -58,7 +58,7 @@ type Logs struct {
 	Selector       []string   `json:"selector" yaml:"selector"`
 	Namespace      string     `json:"namespace,omitempty" yaml:"namespace,omitempty"`
 	ContainerNames []string   `json:"containerNames,omitempty" yaml:"containerNames,omitempty"`
-	Limits         *LogLimits `json:"limits,omitempty" yaml:"omitempty"`
+	Limits         *LogLimits `json:"limits,omitempty" yaml:"limits,omitempty"`
 }
 
 type Data struct {
diff --git a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go
index 7fe8fdd2..17ca9bca 100644
--- a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go
+++ b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go
@@ -4117,6 +4117,11 @@ func (in *TextAnalyze) DeepCopyInto(out *TextAnalyze) {
 			}
 		}
 	}
+	if in.ExcludeFiles != nil {
+		in, out := &in.ExcludeFiles, &out.ExcludeFiles
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TextAnalyze.
diff --git a/pkg/preflight/analyze.go b/pkg/preflight/analyze.go
index c995b25a..246b3fbc 100644
--- a/pkg/preflight/analyze.go
+++ b/pkg/preflight/analyze.go
@@ -57,7 +57,7 @@ func doAnalyze(allCollectedData map[string][]byte, analyzers []*troubleshootv1be
 
 		return contents, nil
 	}
-	getChildCollectedFileContents := func(prefix string) (map[string][]byte, error) {
+	getChildCollectedFileContents := func(prefix string, excludeFiles []string) (map[string][]byte, error) {
 		matching := make(map[string][]byte)
 		for k, v := range allCollectedData {
 			if strings.HasPrefix(k, prefix) {
@@ -71,6 +71,19 @@ func doAnalyze(allCollectedData map[string][]byte, analyzers []*troubleshootv1be
 			}
 		}
 
+		if len(excludeFiles) > 0 {
+			for k := range matching {
+				for _, ex := range excludeFiles {
+					if ok, _ := filepath.Match(ex, k); ok {
+						delete(matching, k)
+					}
+				}
+			}
+		}
+
+		if len(matching) == 0 {
+			return nil, fmt.Errorf("File not found: %s", prefix)
+		}
 		return matching, nil
 	}
 
diff --git a/schemas/analyzer-troubleshoot-v1beta2.json b/schemas/analyzer-troubleshoot-v1beta2.json
index 1cc77c24..4fa5e1e9 100644
--- a/schemas/analyzer-troubleshoot-v1beta2.json
+++ b/schemas/analyzer-troubleshoot-v1beta2.json
@@ -1919,6 +1919,12 @@
                   "exclude": {
                     "oneOf": [{"type": "string"},{"type": "boolean"}]
                   },
+                  "excludeFiles": {
+                    "type": "array",
+                    "items": {
+                      "type": "string"
+                    }
+                  },
                   "fileName": {
                     "type": "string"
                   },
diff --git a/schemas/preflight-troubleshoot-v1beta2.json b/schemas/preflight-troubleshoot-v1beta2.json
index d442ae3e..b5e90186 100644
--- a/schemas/preflight-troubleshoot-v1beta2.json
+++ b/schemas/preflight-troubleshoot-v1beta2.json
@@ -1919,6 +1919,12 @@
                   "exclude": {
                     "oneOf": [{"type": "string"},{"type": "boolean"}]
                   },
+                  "excludeFiles": {
+                    "type": "array",
+                    "items": {
+                      "type": "string"
+                    }
+                  },
                   "fileName": {
                     "type": "string"
                   },
diff --git a/schemas/supportbundle-troubleshoot-v1beta2.json b/schemas/supportbundle-troubleshoot-v1beta2.json
index 57908609..4793f6f8 100644
--- a/schemas/supportbundle-troubleshoot-v1beta2.json
+++ b/schemas/supportbundle-troubleshoot-v1beta2.json
@@ -1965,6 +1965,12 @@
                   "exclude": {
                     "oneOf": [{"type": "string"},{"type": "boolean"}]
                   },
+                  "excludeFiles": {
+                    "type": "array",
+                    "items": {
+                      "type": "string"
+                    }
+                  },
                   "fileName": {
                     "type": "string"
                   },


Test patch:
diff --git a/.github/workflows/build-test-deploy.yaml b/.github/workflows/build-test-deploy.yaml
index b274c999..6c205d7a 100644
--- a/.github/workflows/build-test-deploy.yaml
+++ b/.github/workflows/build-test-deploy.yaml
@@ -8,7 +8,7 @@ on:
     tags:
       - "v*.*.*"
 
-concurrency: 
+concurrency:
   group: ${{ github.workflow }}-${{ github.ref }}
   cancel-in-progress: true
 
@@ -30,6 +30,20 @@ jobs:
 
       - run: make test
 
+  ensure-schemas-are-generated:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/setup-go@v3
+        with:
+          go-version: '1.19'
+      - name: setup env
+        run: |
+          echo "GOPATH=$(go env GOPATH)" >> $GITHUB_ENV
+          echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
+        shell: bash
+      - uses: actions/checkout@v3
+      - run: make check-schemas
+
   compile-preflight:
     runs-on: ubuntu-latest
     steps:
diff --git a/pkg/analyze/deployment_status_test.go b/pkg/analyze/deployment_status_test.go
index 43bcbd63..a6a904a7 100644
--- a/pkg/analyze/deployment_status_test.go
+++ b/pkg/analyze/deployment_status_test.go
@@ -221,7 +221,7 @@ func Test_deploymentStatus(t *testing.T) {
 		t.Run(test.name, func(t *testing.T) {
 			req := require.New(t)
 
-			getFiles := func(n string) (map[string][]byte, error) {
+			getFiles := func(n string, _ []string) (map[string][]byte, error) {
 				if file, ok := test.files[n]; ok {
 					return map[string][]byte{n: file}, nil
 				}
diff --git a/pkg/analyze/job_status_test.go b/pkg/analyze/job_status_test.go
index 38d32adf..d0e29a46 100644
--- a/pkg/analyze/job_status_test.go
+++ b/pkg/analyze/job_status_test.go
@@ -181,7 +181,7 @@ func Test_JobStatus(t *testing.T) {
 		t.Run(test.name, func(t *testing.T) {
 			req := require.New(t)
 
-			getFiles := func(n string) (map[string][]byte, error) {
+			getFiles := func(n string, _ []string) (map[string][]byte, error) {
 				if file, ok := test.files[n]; ok {
 					return map[string][]byte{n: file}, nil
 				}
diff --git a/pkg/analyze/replicaset_status_test.go b/pkg/analyze/replicaset_status_test.go
index c6e767c2..bc48fefc 100644
--- a/pkg/analyze/replicaset_status_test.go
+++ b/pkg/analyze/replicaset_status_test.go
@@ -140,7 +140,7 @@ func Test_analyzeReplicaSetStatus(t *testing.T) {
 		t.Run(test.name, func(t *testing.T) {
 			req := require.New(t)
 
-			getFiles := func(n string) (map[string][]byte, error) {
+			getFiles := func(n string, _ []string) (map[string][]byte, error) {
 				if file, ok := test.files[n]; ok {
 					return map[string][]byte{n: file}, nil
 				}
diff --git a/pkg/analyze/statefulset_status_test.go b/pkg/analyze/statefulset_status_test.go
index 6597ca26..d5822df9 100644
--- a/pkg/analyze/statefulset_status_test.go
+++ b/pkg/analyze/statefulset_status_test.go
@@ -105,7 +105,7 @@ func Test_analyzeStatefulsetStatus(t *testing.T) {
 		t.Run(test.name, func(t *testing.T) {
 			req := require.New(t)
 
-			getFiles := func(n string) (map[string][]byte, error) {
+			getFiles := func(n string, _ []string) (map[string][]byte, error) {
 				if file, ok := test.files[n]; ok {
 					return map[string][]byte{n: file}, nil
 				}
diff --git a/pkg/analyze/sysctl_test.go b/pkg/analyze/sysctl_test.go
index 01cb49cc..4026f3c9 100644
--- a/pkg/analyze/sysctl_test.go
+++ b/pkg/analyze/sysctl_test.go
@@ -274,7 +274,7 @@ func TestAnalyzeSysctl(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			var findFiles = func(glob string) (map[string][]byte, error) {
+			var findFiles = func(glob string, _ []string) (map[string][]byte, error) {
 				return test.files, nil
 			}
 			got, err := analyzeSysctl(test.analyzer, findFiles)
diff --git a/pkg/analyze/text_analyze_test.go b/pkg/analyze/text_analyze_test.go
index 5fc4527e..17822ff9 100644
--- a/pkg/analyze/text_analyze_test.go
+++ b/pkg/analyze/text_analyze_test.go
@@ -582,13 +582,122 @@ func Test_textAnalyze(t *testing.T) {
 				"text-collector-templated-regex-message/cfile-1.txt": []byte(`{"level":"ERROR","timestamp":"2022-05-17T20:37:41Z","caller":"controller/controller.go:317","message":"Reconciler error","context":{"name":"insert-cr-name-here","namespace":"default","error":"myerror"}}`),
 			},
 		},
+		{
+			name: "exclude files case 1 globbing",
+			analyzer: troubleshootv1beta2.TextAnalyze{
+				Outcomes: []*troubleshootv1beta2.Outcome{
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							Message: "fail",
+						},
+					},
+				},
+				CollectorName: "text-collector-1",
+				FileName:      "cfile*.txt",
+				ExcludeFiles:  []string{"*previous.txt"},
+				RegexPattern:  "success",
+			},
+			expectResult: []AnalyzeResult{
+				{
+					IsPass:  false,
+					IsWarn:  false,
+					IsFail:  true,
+					Title:   "text-collector-1",
+					Message: "fail",
+					IconKey: "kubernetes_text_analyze",
+					IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg",
+				},
+			},
+			files: map[string][]byte{
+				"text-collector-1/cfile-1.txt":        []byte("Yes it all succeeded"),
+				"text-collector-1/cfile-previous.txt": []byte("no success here"),
+			},
+		},
+		{
+			name: "exclude files case 2 globbing",
+			analyzer: troubleshootv1beta2.TextAnalyze{
+				Outcomes: []*troubleshootv1beta2.Outcome{
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							Message: "fail",
+						},
+					},
+				},
+				CollectorName: "text-collector-1",
+				FileName:      "cfile*.txt",
+				ExcludeFiles:  []string{"*previous.txt", "cfile-2.txt"},
+				RegexPattern:  "success",
+			},
+			expectResult: []AnalyzeResult{
+				{
+					IsPass:  false,
+					IsWarn:  false,
+					IsFail:  true,
+					Title:   "text-collector-1",
+					Message: "fail",
+					IconKey: "kubernetes_text_analyze",
+					IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg",
+				},
+			},
+			files: map[string][]byte{
+				"text-collector-1/cfile-1.txt":        []byte("Yes it all succeeded"),
+				"text-collector-1/cfile-previous.txt": []byte("no success here"),
+				"text-collector-1/cfile-2.txt":        []byte("no success here"),
+			},
+		},
+		{
+			name: "exclude files case 3 globbing",
+			analyzer: troubleshootv1beta2.TextAnalyze{
+				Outcomes: []*troubleshootv1beta2.Outcome{
+					{
+						Pass: &troubleshootv1beta2.SingleOutcome{
+							Message: "success",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							Message: "fail",
+						},
+					},
+				},
+				CollectorName: "text-collector-1",
+				FileName:      "cfile*.txt",
+				ExcludeFiles:  []string{"*previous.txt"},
+				RegexPattern:  "succeeded",
+			},
+			expectResult: []AnalyzeResult{
+				{
+					IsPass:  true,
+					IsWarn:  false,
+					IsFail:  false,
+					Title:   "text-collector-1",
+					Message: "success",
+					IconKey: "kubernetes_text_analyze",
+					IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg",
+				},
+				{
+					IsPass:  true,
+					IsWarn:  false,
+					IsFail:  false,
+					Title:   "text-collector-1",
+					Message: "success",
+					IconKey: "kubernetes_text_analyze",
+					IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg",
+				},
+			},
+			files: map[string][]byte{
+				"text-collector-1/cfile-1.txt":        []byte("Yes it all succeeded"),
+				"text-collector-1/cfile-previous.txt": []byte("no success here"),
+				"text-collector-1/cfile-2.txt":        []byte("Yes it all succeeded"),
+			},
+		},
 	}
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
 			req := require.New(t)
 
-			getFiles := func(n string) (map[string][]byte, error) {
+			getFiles := func(n string, excludeFiles []string) (map[string][]byte, error) {
 				matching := make(map[string][]byte)
 				for k, v := range test.files {
 					if strings.HasPrefix(k, n) {
@@ -602,6 +711,16 @@ func Test_textAnalyze(t *testing.T) {
 					}
 				}
 
+				if len(excludeFiles) > 0 {
+					for k := range matching {
+						for _, ex := range excludeFiles {
+							if ok, _ := filepath.Match(ex, k); ok {
+								delete(matching, k)
+							}
+						}
+					}
+				}
+
 				if len(matching) == 0 {
 					return nil, fmt.Errorf("File not found: %s", n)
 				}
diff --git a/pkg/analyze/weave_test.go b/pkg/analyze/weave_test.go
index 286df3fb..2de56cfa 100644
--- a/pkg/analyze/weave_test.go
+++ b/pkg/analyze/weave_test.go
@@ -162,7 +162,7 @@ func TestWeaveReport(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			var findFiles = func(glob string) (map[string][]byte, error) {
+			var findFiles = func(glob string, _ []string) (map[string][]byte, error) {
 				return map[string][]byte{
 					"report1": []byte(test.report),
 				}, nil