Repository: replicatedhq/troubleshoot
Pull request #971 (2023-01-18 22:15:06)

Standardize Cluster Resources Collector File Paths
## Description, Motivation and Context

Setting constant values for cluster resources directories and subdirectories, so they can easily be reused

## Checklist

- [x] New and existing tests pass locally with introduced changes.
- [ ] Tests for the changes have been added (for bug fixes / features)
- [x] The commit message(s) are informative and highlight any breaking changes
- [ ] Any documentation required has been added/updated. For changes to https://troubleshoot.sh/ create a PR [here](https://github.com/replicatedhq/troubleshoot.sh/pulls)

## 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. -->


diamonwiggins: A couple questions:

1. It felt like I'd expect to find these constants a bit closer to the actual code and not in the separate `pkg/constants.go`? Curious on other's thoughts. I don't have too strong an opinion on it.
2. Per #960 the only requirement here was cluster resources' root directory, but I ended up adding the others as well to see if that would clean up the code in other places. Worth keeping around?
CpuID: > **It felt like I'd expect to find these constants a bit closer to the actual code and not in the separate pkg/constants.go? Curious on other's thoughts. I don't have too strong an opinion on it.

I tend to agree, for this kind of thing closer to the code itself (for easier readability/trying to grok what it's doing etc). If the constants were being used in a whole bunch of files, having them in `constants.go` might be more reasonable, but that could be an easy future optimization as necessary.

> Per Standardize Cluster Resources Collector #960 the only requirement here was cluster resources' root directory, but I ended up adding the others as well to see if that would clean up the code in other places. Worth keeping around?

deferring to others for opinions 👍 
diamonwiggins: > I am wondering is it worthy to create a cluster_constants.go under /pkg/constants folder to store them? In the meanwhile, since those constants are not like default configuration constants which can be put close to the code or may not in constants.go

makes sense to me
banjoh: > It felt like I'd expect to find these constants a bit closer to the actual code and not in the separate pkg/constants.go? Curious on other's thoughts. I don't have too strong an opinion on it.

The reason for creating the `constants` package is to avoid circular dependencies cause constants end up being sprawled all over the codebase.
diamonwiggins: > > It felt like I'd expect to find these constants a bit closer to the actual code and not in the separate pkg/constants.go? Curious on other's thoughts. I don't have too strong an opinion on it.
> 
> The reason for creating the `constants` package is to avoid circular dependencies cause constants end up being sprawled all over the codebase.

didn't think about the circular dependency bit. good call out

Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c073461a..4f26913c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -15,7 +15,7 @@ There are [community meetings](https://calendar.google.com/calendar/u/0?cid=Y19m
 
 ## Design Principles
 
-When implementing a new feature please review the [design principles](./design/design-principles.md) to help guide the approach.
+When implementing a new feature please review the [design principles](./docs/design/design-principles.md) to help guide the approach.
 
 ## Development Environment
 
@@ -103,4 +103,4 @@ Commit messages should follow the general guidelines:
 - Messages should be descriptive:
   - First line, 50 chars or less, as a heading/title that people can find
   - Then a paragraph explaining things
-- Consider a footer with links to which bugs they fix etc, bearing in mind that Github does some of this magic already
\ No newline at end of file
+- Consider a footer with links to which bugs they fix etc, bearing in mind that Github does some of this magic already
diff --git a/config/crds/troubleshoot.sh_analyzers.yaml b/config/crds/troubleshoot.sh_analyzers.yaml
index 430671c4..45e696dd 100644
--- a/config/crds/troubleshoot.sh_analyzers.yaml
+++ b/config/crds/troubleshoot.sh_analyzers.yaml
@@ -2302,6 +2302,55 @@ spec:
                       required:
                       - outcomes
                       type: object
+                    udpPortStatus:
+                      properties:
+                        annotations:
+                          additionalProperties:
+                            type: string
+                          type: object
+                        checkName:
+                          type: string
+                        collectorName:
+                          type: string
+                        exclude:
+                          type: BoolString
+                        outcomes:
+                          items:
+                            properties:
+                              fail:
+                                properties:
+                                  message:
+                                    type: string
+                                  uri:
+                                    type: string
+                                  when:
+                                    type: string
+                                type: object
+                              pass:
+                                properties:
+                                  message:
+                                    type: string
+                                  uri:
+                                    type: string
+                                  when:
+                                    type: string
+                                type: object
+                              warn:
+                                properties:
+                                  message:
+                                    type: string
+                                  uri:
+                                    type: string
+                                  when:
+                                    type: string
+                                type: object
+                            type: object
+                          type: array
+                        strict:
+                          type: BoolString
+                      required:
+                      - outcomes
+                      type: object
                   type: object
                 type: array
             type: object
diff --git a/config/crds/troubleshoot.sh_collectors.yaml b/config/crds/troubleshoot.sh_collectors.yaml
index 0ce8705a..dd176ec6 100644
--- a/config/crds/troubleshoot.sh_collectors.yaml
+++ b/config/crds/troubleshoot.sh_collectors.yaml
@@ -2424,7 +2424,9 @@ spec:
                                           - name
                                           type: object
                                         type: array
-                                        x-kubernetes-list-type: set
+                                        x-kubernetes-list-map-keys:
+                                        - name
+                                        x-kubernetes-list-type: map
                                       limits:
                                         additionalProperties:
                                           anyOf:
@@ -3887,7 +3889,9 @@ spec:
                                           - name
                                           type: object
                                         type: array
-                                        x-kubernetes-list-type: set
+                                        x-kubernetes-list-map-keys:
+                                        - name
+                                        x-kubernetes-list-type: map
                                       limits:
                                         additionalProperties:
                                           anyOf:
@@ -5385,7 +5389,9 @@ spec:
                                           - name
                                           type: object
                                         type: array
-                                        x-kubernetes-list-type: set
+                                        x-kubernetes-list-map-keys:
+                                        - name
+                                        x-kubernetes-list-type: map
                                       limits:
                                         additionalProperties:
                                           anyOf:
@@ -7307,7 +7313,9 @@ spec:
                                                       - name
                                                       type: object
                                                     type: array
-                                                    x-kubernetes-list-type: set
+                                                    x-kubernetes-list-map-keys:
+                                                    - name
+                                                    x-kubernetes-list-type: map
                                                   limits:
                                                     additionalProperties:
                                                       anyOf:
diff --git a/config/crds/troubleshoot.sh_hostcollectors.yaml b/config/crds/troubleshoot.sh_hostcollectors.yaml
index 4df8b701..913d2151 100644
--- a/config/crds/troubleshoot.sh_hostcollectors.yaml
+++ b/config/crds/troubleshoot.sh_hostcollectors.yaml
@@ -879,6 +879,55 @@ spec:
                       required:
                       - outcomes
                       type: object
+                    udpPortStatus:
+                      properties:
+                        annotations:
+                          additionalProperties:
+                            type: string
+                          type: object
+                        checkName:
+                          type: string
+                        collectorName:
+                          type: string
+                        exclude:
+                          type: BoolString
+                        outcomes:
+                          items:
+                            properties:
+                              fail:
+                                properties:
+                                  message:
+                                    type: string
+                                  uri:
+                                    type: string
+                                  when:
+                                    type: string
+                                type: object
+                              pass:
+                                properties:
+                                  message:
+                                    type: string
+                                  uri:
+                                    type: string
+                                  when:
+                                    type: string
+                                type: object
+                              warn:
+                                properties:
+                                  message:
+                                    type: string
+                                  uri:
+                                    type: string
+                                  when:
+                                    type: string
+                                type: object
+                            type: object
+                          type: array
+                        strict:
+                          type: BoolString
+                      required:
+                      - outcomes
+                      type: object
                   type: object
                 type: array
               collectors:
@@ -1244,6 +1293,19 @@ spec:
                         exclude:
                           type: BoolString
                       type: object
+                    udpPortStatus:
+                      properties:
+                        collectorName:
+                          type: string
+                        exclude:
+                          type: BoolString
+                        interface:
+                          type: string
+                        port:
+                          type: integer
+                      required:
+                      - port
+                      type: object
                   type: object
                 type: array
             type: object
diff --git a/config/crds/troubleshoot.sh_hostpreflights.yaml b/config/crds/troubleshoot.sh_hostpreflights.yaml
index 78ac183c..8dae99b0 100644
--- a/config/crds/troubleshoot.sh_hostpreflights.yaml
+++ b/config/crds/troubleshoot.sh_hostpreflights.yaml
@@ -879,6 +879,55 @@ spec:
                       required:
                       - outcomes
                       type: object
+                    udpPortStatus:
+                      properties:
+                        annotations:
+                          additionalProperties:
+                            type: string
+                          type: object
+                        checkName:
+                          type: string
+                        collectorName:
+                          type: string
+                        exclude:
+                          type: BoolString
+                        outcomes:
+                          items:
+                            properties:
+                              fail:
+                                properties:
+                                  message:
+                                    type: string
+                                  uri:
+                                    type: string
+                                  when:
+                                    type: string
+                                type: object
+                              pass:
+                                properties:
+                                  message:
+                                    type: string
+                                  uri:
+                                    type: string
+                                  when:
+                                    type: string
+                                type: object
+                              warn:
+                                properties:
+                                  message:
+                                    type: string
+                                  uri:
+                                    type: string
+                                  when:
+                                    type: string
+                                type: object
+                            type: object
+                          type: array
+                        strict:
+                          type: BoolString
+                      required:
+                      - outcomes
+                      type: object
                   type: object
                 type: array
               collectors:
@@ -1244,6 +1293,19 @@ spec:
                         exclude:
                           type: BoolString
                       type: object
+                    udpPortStatus:
+                      properties:
+                        collectorName:
+                          type: string
+                        exclude:
+                          type: BoolString
+                        interface:
+                          type: string
+                        port:
+                          type: integer
+                      required:
+                      - port
+                      type: object
                   type: object
                 type: array
               remoteCollectors:
@@ -1519,6 +1581,19 @@ spec:
                         exclude:
                           type: BoolString
                       type: object
+                    udpPortStatus:
+                      properties:
+                        collectorName:
+                          type: string
+                        exclude:
+                          type: BoolString
+                        interface:
+                          type: string
+                        port:
+                          type: integer
+                      required:
+                      - port
+                      type: object
                   type: object
                 type: array
             type: object
diff --git a/config/crds/troubleshoot.sh_preflights.yaml b/config/crds/troubleshoot.sh_preflights.yaml
index 89edcdf9..e81f11d9 100644
--- a/config/crds/troubleshoot.sh_preflights.yaml
+++ b/config/crds/troubleshoot.sh_preflights.yaml
@@ -3816,7 +3816,9 @@ spec:
                                           - name
                                           type: object
                                         type: array
-                                        x-kubernetes-list-type: set
+                                        x-kubernetes-list-map-keys:
+                                        - name
+                                        x-kubernetes-list-type: map
                                       limits:
                                         additionalProperties:
                                           anyOf:
@@ -5279,7 +5281,9 @@ spec:
                                           - name
                                           type: object
                                         type: array
-                                        x-kubernetes-list-type: set
+                                        x-kubernetes-list-map-keys:
+                                        - name
+                                        x-kubernetes-list-type: map
                                       limits:
                                         additionalProperties:
                                           anyOf:
@@ -6777,7 +6781,9 @@ spec:
                                           - name
                                           type: object
                                         type: array
-                                        x-kubernetes-list-type: set
+                                        x-kubernetes-list-map-keys:
+                                        - name
+                                        x-kubernetes-list-type: map
                                       limits:
                                         additionalProperties:
                                           anyOf:
@@ -8699,7 +8705,9 @@ spec:
                                                       - name
                                                       type: object
                                                     type: array
-                                                    x-kubernetes-list-type: set
+                                                    x-kubernetes-list-map-keys:
+                                                    - name
+                                                    x-kubernetes-list-type: map
                                                   limits:
                                                     additionalProperties:
                                                       anyOf:
@@ -10154,6 +10162,19 @@ spec:
                         exclude:
                           type: BoolString
                       type: object
+                    udpPortStatus:
+                      properties:
+                        collectorName:
+                          type: string
+                        exclude:
+                          type: BoolString
+                        interface:
+                          type: string
+                        port:
+                          type: integer
+                      required:
+                      - port
+                      type: object
                   type: object
                 type: array
               uploadResultsTo:
diff --git a/config/crds/troubleshoot.sh_remotecollectors.yaml b/config/crds/troubleshoot.sh_remotecollectors.yaml
index 8218f57c..b7626a60 100644
--- a/config/crds/troubleshoot.sh_remotecollectors.yaml
+++ b/config/crds/troubleshoot.sh_remotecollectors.yaml
@@ -340,6 +340,19 @@ spec:
                         exclude:
                           type: BoolString
                       type: object
+                    udpPortStatus:
+                      properties:
+                        collectorName:
+                          type: string
+                        exclude:
+                          type: BoolString
+                        interface:
+                          type: string
+                        port:
+                          type: integer
+                      required:
+                      - port
+                      type: object
                   type: object
                 type: array
               nodeSelector:
diff --git a/config/crds/troubleshoot.sh_supportbundles.yaml b/config/crds/troubleshoot.sh_supportbundles.yaml
index 839397b3..7f3250d5 100644
--- a/config/crds/troubleshoot.sh_supportbundles.yaml
+++ b/config/crds/troubleshoot.sh_supportbundles.yaml
@@ -3847,7 +3847,9 @@ spec:
                                           - name
                                           type: object
                                         type: array
-                                        x-kubernetes-list-type: set
+                                        x-kubernetes-list-map-keys:
+                                        - name
+                                        x-kubernetes-list-type: map
                                       limits:
                                         additionalProperties:
                                           anyOf:
@@ -5310,7 +5312,9 @@ spec:
                                           - name
                                           type: object
                                         type: array
-                                        x-kubernetes-list-type: set
+                                        x-kubernetes-list-map-keys:
+                                        - name
+                                        x-kubernetes-list-type: map
                                       limits:
                                         additionalProperties:
                                           anyOf:
@@ -6808,7 +6812,9 @@ spec:
                                           - name
                                           type: object
                                         type: array
-                                        x-kubernetes-list-type: set
+                                        x-kubernetes-list-map-keys:
+                                        - name
+                                        x-kubernetes-list-type: map
                                       limits:
                                         additionalProperties:
                                           anyOf:
@@ -8730,7 +8736,9 @@ spec:
                                                       - name
                                                       type: object
                                                     type: array
-                                                    x-kubernetes-list-type: set
+                                                    x-kubernetes-list-map-keys:
+                                                    - name
+                                                    x-kubernetes-list-type: map
                                                   limits:
                                                     additionalProperties:
                                                       anyOf:
@@ -10755,6 +10763,55 @@ spec:
                       required:
                       - outcomes
                       type: object
+                    udpPortStatus:
+                      properties:
+                        annotations:
+                          additionalProperties:
+                            type: string
+                          type: object
+                        checkName:
+                          type: string
+                        collectorName:
+                          type: string
+                        exclude:
+                          type: BoolString
+                        outcomes:
+                          items:
+                            properties:
+                              fail:
+                                properties:
+                                  message:
+                                    type: string
+                                  uri:
+                                    type: string
+                                  when:
+                                    type: string
+                                type: object
+                              pass:
+                                properties:
+                                  message:
+                                    type: string
+                                  uri:
+                                    type: string
+                                  when:
+                                    type: string
+                                type: object
+                              warn:
+                                properties:
+                                  message:
+                                    type: string
+                                  uri:
+                                    type: string
+                                  when:
+                                    type: string
+                                type: object
+                            type: object
+                          type: array
+                        strict:
+                          type: BoolString
+                      required:
+                      - outcomes
+                      type: object
                   type: object
                 type: array
               hostCollectors:
@@ -11120,6 +11177,19 @@ spec:
                         exclude:
                           type: BoolString
                       type: object
+                    udpPortStatus:
+                      properties:
+                        collectorName:
+                          type: string
+                        exclude:
+                          type: BoolString
+                        interface:
+                          type: string
+                        port:
+                          type: integer
+                      required:
+                      - port
+                      type: object
                   type: object
                 type: array
               uri:
diff --git a/examples/collect/host/udp-port.yaml b/examples/collect/host/udp-port.yaml
new file mode 100644
index 00000000..f06a7e6b
--- /dev/null
+++ b/examples/collect/host/udp-port.yaml
@@ -0,0 +1,9 @@
+apiVersion: troubleshoot.sh/v1beta2
+kind: HostCollector
+metadata:
+  name: port
+spec:
+  collectors:
+    - udpPortStatus:
+        collectorName: flannel
+        port: 8472
diff --git a/examples/collect/remote/udp-port.yaml b/examples/collect/remote/udp-port.yaml
new file mode 100644
index 00000000..f06a7e6b
--- /dev/null
+++ b/examples/collect/remote/udp-port.yaml
@@ -0,0 +1,9 @@
+apiVersion: troubleshoot.sh/v1beta2
+kind: HostCollector
+metadata:
+  name: port
+spec:
+  collectors:
+    - udpPortStatus:
+        collectorName: flannel
+        port: 8472
diff --git a/examples/preflight/host/udp-port.yaml b/examples/preflight/host/udp-port.yaml
new file mode 100644
index 00000000..aedb9de7
--- /dev/null
+++ b/examples/preflight/host/udp-port.yaml
@@ -0,0 +1,24 @@
+apiVersion: troubleshoot.sh/v1beta2
+kind: HostPreflight
+metadata:
+  name: port
+spec:
+  collectors:
+    - udpPortStatus:
+        collectorName: flannel
+        port: 8472
+  analyzers:
+    - udpPortStatus:
+        collectorName: flannel
+        outcomes:
+          - fail:
+              when: "address-in-use"
+              message: Another process was already listening on port 8472.
+          - fail:
+              when: "error"
+              message: Unexpected port status
+          - pass:
+              when: "connected"
+              message: Port 8472 is open
+          - warn:
+              message: Unexpected port status
diff --git a/examples/preflight/remote/udp-port.yaml b/examples/preflight/remote/udp-port.yaml
new file mode 100644
index 00000000..aedb9de7
--- /dev/null
+++ b/examples/preflight/remote/udp-port.yaml
@@ -0,0 +1,24 @@
+apiVersion: troubleshoot.sh/v1beta2
+kind: HostPreflight
+metadata:
+  name: port
+spec:
+  collectors:
+    - udpPortStatus:
+        collectorName: flannel
+        port: 8472
+  analyzers:
+    - udpPortStatus:
+        collectorName: flannel
+        outcomes:
+          - fail:
+              when: "address-in-use"
+              message: Another process was already listening on port 8472.
+          - fail:
+              when: "error"
+              message: Unexpected port status
+          - pass:
+              when: "connected"
+              message: Port 8472 is open
+          - warn:
+              message: Unexpected port status
diff --git a/go.mod b/go.mod
index dfeb2fb7..d9249f05 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,7 @@ require (
 	github.com/blang/semver/v4 v4.0.0
 	github.com/containers/image/v5 v5.23.1
 	github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2
-	github.com/fatih/color v1.13.0
+	github.com/fatih/color v1.14.1
 	github.com/go-logr/logr v1.2.3
 	github.com/go-redis/redis/v7 v7.4.1
 	github.com/go-sql-driver/mysql v1.7.0
@@ -36,12 +36,12 @@ require (
 	github.com/tj/go-spin v1.1.0
 	golang.org/x/sync v0.1.0
 	gopkg.in/yaml.v2 v2.4.0
-	k8s.io/api v0.26.0
+	k8s.io/api v0.26.1
 	k8s.io/apiextensions-apiserver v0.26.0
-	k8s.io/apimachinery v0.26.0
+	k8s.io/apimachinery v0.26.1
 	k8s.io/apiserver v0.26.0
 	k8s.io/cli-runtime v0.26.0
-	k8s.io/client-go v0.26.0
+	k8s.io/client-go v0.26.1
 	k8s.io/klog/v2 v2.80.1
 	oras.land/oras-go v1.2.2
 	sigs.k8s.io/controller-runtime v0.14.1
@@ -132,7 +132,7 @@ require (
 	github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
 	github.com/magiconair/properties v1.8.6 // indirect
 	github.com/mailru/easyjson v0.7.6 // indirect
-	github.com/mattn/go-colorable v0.1.12 // indirect
+	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-runewidth v0.0.13 // indirect
 	github.com/mattn/go-shellwords v1.0.12 // indirect
 	github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
diff --git a/go.sum b/go.sum
index 81f40d61..cb42d2ff 100644
--- a/go.sum
+++ b/go.sum
@@ -394,8 +394,8 @@ github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQL
 github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
 github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
-github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
-github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
+github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
+github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
 github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
 github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
@@ -725,12 +725,10 @@ github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYt
 github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
 github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
-github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
-github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
-github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
 github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
-github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
-github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
 github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
@@ -1281,7 +1279,6 @@ golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1338,7 +1335,6 @@ golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1710,15 +1706,15 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
 k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo=
 k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ=
 k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8=
-k8s.io/api v0.26.0 h1:IpPlZnxBpV1xl7TGk/X6lFtpgjgntCg8PJ+qrPHAC7I=
-k8s.io/api v0.26.0/go.mod h1:k6HDTaIFC8yn1i6pSClSqIwLABIcLV9l5Q4EcngKnQg=
+k8s.io/api v0.26.1 h1:f+SWYiPd/GsiWwVRz+NbFyCgvv75Pk9NK6dlkZgpCRQ=
+k8s.io/api v0.26.1/go.mod h1:xd/GBNgR0f707+ATNyPmQ1oyKSgndzXij81FzWGsejg=
 k8s.io/apiextensions-apiserver v0.26.0 h1:Gy93Xo1eg2ZIkNX/8vy5xviVSxwQulsnUdQ00nEdpDo=
 k8s.io/apiextensions-apiserver v0.26.0/go.mod h1:7ez0LTiyW5nq3vADtK6C3kMESxadD51Bh6uz3JOlqWQ=
 k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
 k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
 k8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc=
-k8s.io/apimachinery v0.26.0 h1:1feANjElT7MvPqp0JT6F3Ss6TWDwmcjLypwoPpEf7zg=
-k8s.io/apimachinery v0.26.0/go.mod h1:tnPmbONNJ7ByJNz9+n9kMjNP8ON+1qoAIIC70lztu74=
+k8s.io/apimachinery v0.26.1 h1:8EZ/eGJL+hY/MYCNwhmDzVqq2lPl3N3Bo8rvweJwXUQ=
+k8s.io/apimachinery v0.26.1/go.mod h1:tnPmbONNJ7ByJNz9+n9kMjNP8ON+1qoAIIC70lztu74=
 k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU=
 k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM=
 k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q=
@@ -1729,8 +1725,8 @@ k8s.io/cli-runtime v0.26.0/go.mod h1:o+4KmwHzO/UK0wepE1qpRk6l3o60/txUZ1fEXWGIKTY
 k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y=
 k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k=
 k8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0=
-k8s.io/client-go v0.26.0 h1:lT1D3OfO+wIi9UFolCrifbjUUgu7CpLca0AD8ghRLI8=
-k8s.io/client-go v0.26.0/go.mod h1:I2Sh57A79EQsDmn7F7ASpmru1cceh3ocVT9KlX2jEZg=
+k8s.io/client-go v0.26.1 h1:87CXzYJnAMGaa/IDDfRdhTzxk/wzGZ+/HUQpqgVSZXU=
+k8s.io/client-go v0.26.1/go.mod h1:IWNSglg+rQ3OcvDkhY6+QLeasV4OYHDjdqeWkDQZwGE=
 k8s.io/code-generator v0.19.7/go.mod h1:lwEq3YnLYb/7uVXLorOJfxg+cUu2oihFhHZ0n9NIla0=
 k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk=
 k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI=
diff --git a/pkg/analyze/cluster_pod_statuses.go b/pkg/analyze/cluster_pod_statuses.go
index 3b5a9c0c..e643ba74 100644
--- a/pkg/analyze/cluster_pod_statuses.go
+++ b/pkg/analyze/cluster_pod_statuses.go
@@ -10,13 +10,14 @@ import (
 
 	"github.com/pkg/errors"
 	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+	"github.com/replicatedhq/troubleshoot/pkg/constants"
 	"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
 	corev1 "k8s.io/api/core/v1"
 )
 
 func clusterPodStatuses(analyzer *troubleshootv1beta2.ClusterPodStatuses, getChildCollectedFileContents getChildCollectedFileContents) ([]*AnalyzeResult, error) {
 	excludeFiles := []string{}
-	collected, err := getChildCollectedFileContents(filepath.Join("cluster-resources", "pods", "*.json"), excludeFiles)
+	collected, err := getChildCollectedFileContents(filepath.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_PODS, "*.json"), excludeFiles)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to read collected pods")
 	}
diff --git a/pkg/analyze/container_runtime.go b/pkg/analyze/container_runtime.go
index 48654bb2..5c03d5ab 100644
--- a/pkg/analyze/container_runtime.go
+++ b/pkg/analyze/container_runtime.go
@@ -2,16 +2,18 @@ package analyzer
 
 import (
 	"encoding/json"
+	"fmt"
 	"net/url"
 	"strings"
 
 	"github.com/pkg/errors"
 	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+	"github.com/replicatedhq/troubleshoot/pkg/constants"
 	corev1 "k8s.io/api/core/v1"
 )
 
 func analyzeContainerRuntime(analyzer *troubleshootv1beta2.ContainerRuntime, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
-	collected, err := getCollectedFileContents("cluster-resources/nodes.json")
+	collected, err := getCollectedFileContents(fmt.Sprintf("%s/%s.json", constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_NODES))
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to get contents of nodes.json")
 	}
diff --git a/pkg/analyze/crd.go b/pkg/analyze/crd.go
index 75761f3a..c5b751d3 100644
--- a/pkg/analyze/crd.go
+++ b/pkg/analyze/crd.go
@@ -5,11 +5,12 @@ import (
 	"fmt"
 
 	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+	"github.com/replicatedhq/troubleshoot/pkg/constants"
 	apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
 )
 
 func analyzeCustomResourceDefinition(analyzer *troubleshootv1beta2.CustomResourceDefinition, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
-	crdData, err := getCollectedFileContents("cluster-resources/custom-resource-definitions.json")
+	crdData, err := getCollectedFileContents(fmt.Sprintf("%s/%s.json", constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_CUSTOM_RESOURCE_DEFINITIONS))
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/analyze/deployment_status.go b/pkg/analyze/deployment_status.go
index 1c61aa7b..ca405e39 100644
--- a/pkg/analyze/deployment_status.go
+++ b/pkg/analyze/deployment_status.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/pkg/errors"
 	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+	"github.com/replicatedhq/troubleshoot/pkg/constants"
 	appsv1 "k8s.io/api/apps/v1"
 )
 
@@ -20,7 +21,7 @@ func analyzeDeploymentStatus(analyzer *troubleshootv1beta2.DeploymentStatus, get
 
 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)
+	files, err := getFileContents(filepath.Join(constants.CLUSTER_RESOURCES_DIR, constants.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")
 	}
@@ -66,15 +67,15 @@ func analyzeAllDeploymentStatuses(analyzer *troubleshootv1beta2.DeploymentStatus
 	excludeFiles := []string{}
 	fileNames := make([]string, 0)
 	if analyzer.Namespace != "" {
-		fileNames = append(fileNames, filepath.Join("cluster-resources", "deployments", fmt.Sprintf("%s.json", analyzer.Namespace)))
+		fileNames = append(fileNames, filepath.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_DEPLOYMENTS, fmt.Sprintf("%s.json", analyzer.Namespace)))
 	}
 	for _, ns := range analyzer.Namespaces {
-		fileNames = append(fileNames, filepath.Join("cluster-resources", "deployments", fmt.Sprintf("%s.json", ns)))
+		fileNames = append(fileNames, filepath.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_DEPLOYMENTS, fmt.Sprintf("%s.json", ns)))
 	}
 
 	// no namespace specified, so we need to analyze all deployments
 	if len(fileNames) == 0 {
-		fileNames = append(fileNames, filepath.Join("cluster-resources", "deployments", "*.json"))
+		fileNames = append(fileNames, filepath.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_DEPLOYMENTS, "*.json"))
 	}
 
 	results := []*AnalyzeResult{}
diff --git a/pkg/analyze/distribution.go b/pkg/analyze/distribution.go
index 2312daf5..4959b3c0 100644
--- a/pkg/analyze/distribution.go
+++ b/pkg/analyze/distribution.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/pkg/errors"
 	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+	"github.com/replicatedhq/troubleshoot/pkg/constants"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
@@ -143,9 +144,9 @@ func ParseNodesForProviders(nodes []corev1.Node) (providers, string) {
 
 func analyzeDistribution(analyzer *troubleshootv1beta2.Distribution, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
 	var unknownDistribution string
-	collected, err := getCollectedFileContents("cluster-resources/nodes.json")
+	collected, err := getCollectedFileContents(fmt.Sprintf("%s/%s.json", constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_NODES))
 	if err != nil {
-		return nil, errors.Wrap(err, "failed to get contents of nodes.json")
+		return nil, errors.Wrap(err, fmt.Sprintf("failed to get contents of %s.json", constants.CLUSTER_RESOURCES_NODES))
 	}
 
 	var nodes corev1.NodeList
@@ -155,7 +156,7 @@ func analyzeDistribution(analyzer *troubleshootv1beta2.Distribution, getCollecte
 
 	foundProviders, _ := ParseNodesForProviders(nodes.Items)
 
-	apiResourcesBytes, err := getCollectedFileContents("cluster-resources/resources.json")
+	apiResourcesBytes, err := getCollectedFileContents(fmt.Sprintf("%s/%s.json", constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_RESOURCES))
 	// if the file is not found, that is not a fatal error
 	// troubleshoot 0.9.15 and earlier did not collect that file
 	if err == nil {
diff --git a/pkg/analyze/host_analyzer.go b/pkg/analyze/host_analyzer.go
index 6b04b153..0f2f05c0 100644
--- a/pkg/analyze/host_analyzer.go
+++ b/pkg/analyze/host_analyzer.go
@@ -22,6 +22,8 @@ func GetHostAnalyzer(analyzer *troubleshootv1beta2.HostAnalyze) (HostAnalyzer, b
 		return &AnalyzeHostDiskUsage{analyzer.DiskUsage}, true
 	case analyzer.TCPPortStatus != nil:
 		return &AnalyzeHostTCPPortStatus{analyzer.TCPPortStatus}, true
+	case analyzer.UDPPortStatus != nil:
+		return &AnalyzeHostUDPPortStatus{analyzer.UDPPortStatus}, true
 	case analyzer.HTTP != nil:
 		return &AnalyzeHostHTTP{analyzer.HTTP}, true
 	case analyzer.Time != nil:
diff --git a/pkg/analyze/host_udpportstatus.go b/pkg/analyze/host_udpportstatus.go
new file mode 100644
index 00000000..e81baf8a
--- /dev/null
+++ b/pkg/analyze/host_udpportstatus.go
@@ -0,0 +1,100 @@
+package analyzer
+
+import (
+	"encoding/json"
+	"fmt"
+	"path"
+
+	"github.com/pkg/errors"
+	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+	"github.com/replicatedhq/troubleshoot/pkg/collect"
+)
+
+// AnalyzeHostUDPPortStatus is an analyzer that will return only one matching result, or a warning if nothing matches. The first
+// match that is encountered is the one that is returned.
+type AnalyzeHostUDPPortStatus struct {
+	hostAnalyzer *troubleshootv1beta2.UDPPortStatusAnalyze
+}
+
+func (a *AnalyzeHostUDPPortStatus) Title() string {
+	return hostAnalyzerTitleOrDefault(a.hostAnalyzer.AnalyzeMeta, "UDP Port Status")
+}
+
+func (a *AnalyzeHostUDPPortStatus) IsExcluded() (bool, error) {
+	return isExcluded(a.hostAnalyzer.Exclude)
+}
+
+func (a *AnalyzeHostUDPPortStatus) Analyze(getCollectedFileContents func(string) ([]byte, error)) ([]*AnalyzeResult, error) {
+	hostAnalyzer := a.hostAnalyzer
+
+	fullPath := path.Join("host-collectors/udpPortStatus", "udpPortStatus.json")
+	if hostAnalyzer.CollectorName != "" {
+		fullPath = path.Join("host-collectors/udpPortStatus", fmt.Sprintf("%s.json", hostAnalyzer.CollectorName))
+	}
+
+	collected, err := getCollectedFileContents(fullPath)
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to read collected file name: %s", fullPath)
+	}
+	actual := collect.NetworkStatusResult{}
+	if err := json.Unmarshal(collected, &actual); err != nil {
+		return nil, errors.Wrap(err, "failed to unmarshal collected")
+	}
+
+	result := &AnalyzeResult{Title: a.Title()}
+
+	for _, outcome := range hostAnalyzer.Outcomes {
+
+		if outcome.Fail != nil {
+			if outcome.Fail.When == "" {
+				result.IsFail = true
+				result.Message = outcome.Fail.Message
+				result.URI = outcome.Fail.URI
+
+				return []*AnalyzeResult{result}, nil
+			}
+
+			if string(actual.Status) == outcome.Fail.When {
+				result.IsFail = true
+				result.Message = outcome.Fail.Message
+				result.URI = outcome.Fail.URI
+
+				return []*AnalyzeResult{result}, nil
+			}
+		} else if outcome.Warn != nil {
+			if outcome.Warn.When == "" {
+				result.IsWarn = true
+				result.Message = outcome.Warn.Message
+				result.URI = outcome.Warn.URI
+
+				return []*AnalyzeResult{result}, nil
+			}
+
+			if string(actual.Status) == outcome.Warn.When {
+				result.IsWarn = true
+				result.Message = outcome.Warn.Message
+				result.URI = outcome.Warn.URI
+
+				return []*AnalyzeResult{result}, nil
+			}
+		} else if outcome.Pass != nil {
+			if outcome.Pass.When == "" {
+				result.IsPass = true
+				result.Message = outcome.Pass.Message
+				result.URI = outcome.Pass.URI
+
+				return []*AnalyzeResult{result}, nil
+			}
+
+			if string(actual.Status) == outcome.Pass.When {
+				result.IsPass = true
+				result.Message = outcome.Pass.Message
+				result.URI = outcome.Pass.URI
+
+				return []*AnalyzeResult{result}, nil
+			}
+		}
+	}
+
+	return []*AnalyzeResult{result}, nil
+}
diff --git a/pkg/analyze/image_pull_secret.go b/pkg/analyze/image_pull_secret.go
index 72d286c0..29139394 100644
--- a/pkg/analyze/image_pull_secret.go
+++ b/pkg/analyze/image_pull_secret.go
@@ -6,11 +6,12 @@ import (
 
 	"github.com/pkg/errors"
 	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+	"github.com/replicatedhq/troubleshoot/pkg/constants"
 )
 
 func analyzeImagePullSecret(analyzer *troubleshootv1beta2.ImagePullSecret, getChildCollectedFileContents getChildCollectedFileContents) (*AnalyzeResult, error) {
 	var excludeFiles = []string{}
-	imagePullSecrets, err := getChildCollectedFileContents("cluster-resources/image-pull-secrets", excludeFiles)
+	imagePullSecrets, err := getChildCollectedFileContents(fmt.Sprintf("%s/%s", constants.CLUSTER_RESOURCES_DIR, constants.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/ingress.go b/pkg/analyze/ingress.go
index 8d02f6ad..ea7244ee 100644
--- a/pkg/analyze/ingress.go
+++ b/pkg/analyze/ingress.go
@@ -6,11 +6,12 @@ import (
 	"path/filepath"
 
 	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+	"github.com/replicatedhq/troubleshoot/pkg/constants"
 	extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
 )
 
 func analyzeIngress(analyzer *troubleshootv1beta2.Ingress, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
-	ingressData, err := getCollectedFileContents(filepath.Join("cluster-resources", "ingress", fmt.Sprintf("%s.json", analyzer.Namespace)))
+	ingressData, err := getCollectedFileContents(filepath.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_INGRESS, fmt.Sprintf("%s.json", analyzer.Namespace)))
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/analyze/job_status.go b/pkg/analyze/job_status.go
index 86e2bd77..fdbfc096 100644
--- a/pkg/analyze/job_status.go
+++ b/pkg/analyze/job_status.go
@@ -9,6 +9,7 @@ import (
 
 	"github.com/pkg/errors"
 	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+	"github.com/replicatedhq/troubleshoot/pkg/constants"
 	batchv1 "k8s.io/api/batch/v1"
 )
 
@@ -22,7 +23,7 @@ func analyzeJobStatus(analyzer *troubleshootv1beta2.JobStatus, getFileContents g
 
 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)
+	files, err := getFileContents(filepath.Join(constants.CLUSTER_RESOURCES_DIR, constants.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")
 	}
@@ -71,15 +72,15 @@ func analyzeOneJobStatus(analyzer *troubleshootv1beta2.JobStatus, getFileContent
 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)))
+		fileNames = append(fileNames, filepath.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_JOBS, fmt.Sprintf("%s.json", analyzer.Namespace)))
 	}
 	for _, ns := range analyzer.Namespaces {
-		fileNames = append(fileNames, filepath.Join("cluster-resources", "jobs", fmt.Sprintf("%s.json", ns)))
+		fileNames = append(fileNames, filepath.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_JOBS, fmt.Sprintf("%s.json", ns)))
 	}
 
 	// no namespace specified, so we need to analyze all jobs
 	if len(analyzer.Namespaces) == 0 {
-		fileNames = append(fileNames, filepath.Join("cluster-resources", "jobs", "*.json"))
+		fileNames = append(fileNames, filepath.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_JOBS, "*.json"))
 	}
 
 	excludeFiles := []string{}
diff --git a/pkg/analyze/kube_resource.go b/pkg/analyze/kube_resource.go
index c4a8f96b..f9d82f5d 100644
--- a/pkg/analyze/kube_resource.go
+++ b/pkg/analyze/kube_resource.go
@@ -8,27 +8,28 @@ import (
 
 	"github.com/pkg/errors"
 	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+	"github.com/replicatedhq/troubleshoot/pkg/constants"
 	iutils "github.com/replicatedhq/troubleshoot/pkg/interfaceutils"
 	"gopkg.in/yaml.v2"
 )
 
 var Filemap = map[string]string{
-	"Deployment":           "deployments",
-	"StatefulSet":          "statefulsets",
-	"NetworkPolicy":        "network-policy",
-	"Pod":                  "pods",
-	"Ingress":              "ingress",
-	"Service":              "services",
-	"ResourceQuota":        "resource-quotas",
-	"Job":                  "jobs",
-	"PersistentVoumeClaim": "pvcs",
-	"pvc":                  "pvcs",
-	"ReplicaSet":           "replicasets",
-	"Namespace":            "namespaces.json",
-	"PersistentVolume":     "pvs.json",
-	"pv":                   "pvs.json",
-	"Node":                 "nodes.json",
-	"StorageClass":         "storage-classes.json",
+	"Deployment":           constants.CLUSTER_RESOURCES_DEPLOYMENTS,
+	"StatefulSet":          constants.CLUSTER_RESOURCES_STATEFULSETS,
+	"NetworkPolicy":        constants.CLUSTER_RESOURCES_NETWORK_POLICY,
+	"Pod":                  constants.CLUSTER_RESOURCES_PODS,
+	"Ingress":              constants.CLUSTER_RESOURCES_INGRESS,
+	"Service":              constants.CLUSTER_RESOURCES_SERVICES,
+	"ResourceQuota":        constants.CLUSTER_RESOURCES_RESOURCE_QUOTA,
+	"Job":                  constants.CLUSTER_RESOURCES_JOBS,
+	"PersistentVoumeClaim": constants.CLUSTER_RESOURCES_PVCS,
+	"pvc":                  constants.CLUSTER_RESOURCES_PVCS,
+	"ReplicaSet":           constants.CLUSTER_RESOURCES_REPLICASETS,
+	"Namespace":            fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_NAMESPACES),
+	"PersistentVolume":     fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_PVS),
+	"pv":                   fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_PVS),
+	"Node":                 fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_NODES),
+	"StorageClass":         fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_STORAGE_CLASS),
 }
 
 // FindResource locates and returns a kubernetes resource as an interface{} from a support bundle based on some basic selectors
@@ -43,12 +44,12 @@ func FindResource(kind string, clusterScoped bool, namespace string, name string
 		return nil, errors.New("failed to find resource")
 	}
 
-	datapath = filepath.Join("cluster-resources", resourceLocation)
+	datapath = filepath.Join(constants.CLUSTER_RESOURCES_DIR, resourceLocation)
 	if !clusterScoped {
 		if namespace == "" {
 			namespace = "default"
 		}
-		datapath = filepath.Join("cluster-resources", resourceLocation, fmt.Sprintf("%s.json", namespace))
+		datapath = filepath.Join(constants.CLUSTER_RESOURCES_DIR, resourceLocation, fmt.Sprintf("%s.json", namespace))
 	}
 
 	file, err := getFileContents(datapath)
diff --git a/pkg/analyze/node_resources.go b/pkg/analyze/node_resources.go
index a37815d9..ad85d868 100644
--- a/pkg/analyze/node_resources.go
+++ b/pkg/analyze/node_resources.go
@@ -9,12 +9,13 @@ import (
 
 	"github.com/pkg/errors"
 	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+	"github.com/replicatedhq/troubleshoot/pkg/constants"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/resource"
 )
 
 func analyzeNodeResources(analyzer *troubleshootv1beta2.NodeResources, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
-	collected, err := getCollectedFileContents("cluster-resources/nodes.json")
+	collected, err := getCollectedFileContents(fmt.Sprintf("%s/%s.json", constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_NODES))
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to get contents of nodes.json")
 	}
diff --git a/pkg/analyze/replicaset_status.go b/pkg/analyze/replicaset_status.go
index fcb583a2..ca5e7d2f 100644
--- a/pkg/analyze/replicaset_status.go
+++ b/pkg/analyze/replicaset_status.go
@@ -9,6 +9,7 @@ import (
 
 	"github.com/pkg/errors"
 	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+	"github.com/replicatedhq/troubleshoot/pkg/constants"
 	appsv1 "k8s.io/api/apps/v1"
 	"k8s.io/apimachinery/pkg/labels"
 )
@@ -23,7 +24,7 @@ func analyzeReplicaSetStatus(analyzer *troubleshootv1beta2.ReplicaSetStatus, get
 
 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)
+	files, err := getFileContents(filepath.Join(constants.CLUSTER_RESOURCES_DIR, constants.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")
 	}
@@ -68,14 +69,14 @@ func analyzeOneReplicaSetStatus(analyzer *troubleshootv1beta2.ReplicaSetStatus,
 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)))
+		fileNames = append(fileNames, filepath.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_REPLICASETS, fmt.Sprintf("%s.json", analyzer.Namespace)))
 	}
 	for _, ns := range analyzer.Namespaces {
-		fileNames = append(fileNames, filepath.Join("cluster-resources", "replicasets", fmt.Sprintf("%s.json", ns)))
+		fileNames = append(fileNames, filepath.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_REPLICASETS, fmt.Sprintf("%s.json", ns)))
 	}
 
 	if len(fileNames) == 0 {
-		fileNames = append(fileNames, filepath.Join("cluster-resources", "replicasets", "*.json"))
+		fileNames = append(fileNames, filepath.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_REPLICASETS, "*.json"))
 	}
 
 	results := []*AnalyzeResult{}
diff --git a/pkg/analyze/statefulset_status.go b/pkg/analyze/statefulset_status.go
index 2678aa73..d2836db2 100644
--- a/pkg/analyze/statefulset_status.go
+++ b/pkg/analyze/statefulset_status.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/pkg/errors"
 	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+	"github.com/replicatedhq/troubleshoot/pkg/constants"
 	appsv1 "k8s.io/api/apps/v1"
 )
 
@@ -20,7 +21,7 @@ func analyzeStatefulsetStatus(analyzer *troubleshootv1beta2.StatefulsetStatus, g
 
 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)
+	files, err := getFileContents(filepath.Join(constants.CLUSTER_RESOURCES_DIR, constants.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")
 	}
@@ -69,15 +70,15 @@ func analyzeOneStatefulsetStatus(analyzer *troubleshootv1beta2.StatefulsetStatus
 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)))
+		fileNames = append(fileNames, filepath.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_STATEFULSETS, fmt.Sprintf("%s.json", analyzer.Namespace)))
 	}
 	for _, ns := range analyzer.Namespaces {
-		fileNames = append(fileNames, filepath.Join("cluster-resources", "statefulsets", fmt.Sprintf("%s.json", ns)))
+		fileNames = append(fileNames, filepath.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_STATEFULSETS, fmt.Sprintf("%s.json", ns)))
 	}
 
 	// no namespace specified, so we need to analyze all statefulsets
 	if len(fileNames) == 0 {
-		fileNames = append(fileNames, filepath.Join("cluster-resources", "statefulsets", "*.json"))
+		fileNames = append(fileNames, filepath.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_STATEFULSETS, "*.json"))
 	}
 
 	excludeFiles := []string{}
diff --git a/pkg/analyze/storage_class.go b/pkg/analyze/storage_class.go
index 82ba42b1..450c6a67 100644
--- a/pkg/analyze/storage_class.go
+++ b/pkg/analyze/storage_class.go
@@ -5,11 +5,12 @@ import (
 	"fmt"
 
 	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+	"github.com/replicatedhq/troubleshoot/pkg/constants"
 	storagev1beta1 "k8s.io/api/storage/v1beta1"
 )
 
 func analyzeStorageClass(analyzer *troubleshootv1beta2.StorageClass, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
-	storageClassesData, err := getCollectedFileContents("cluster-resources/storage-classes.json")
+	storageClassesData, err := getCollectedFileContents(fmt.Sprintf("%s/%s.json", constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_STORAGE_CLASS))
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/apis/troubleshoot/v1beta2/hostanalyzer_shared.go b/pkg/apis/troubleshoot/v1beta2/hostanalyzer_shared.go
index 87bd1464..a97ce918 100644
--- a/pkg/apis/troubleshoot/v1beta2/hostanalyzer_shared.go
+++ b/pkg/apis/troubleshoot/v1beta2/hostanalyzer_shared.go
@@ -30,6 +30,12 @@ type TCPPortStatusAnalyze struct {
 	Outcomes      []*Outcome `json:"outcomes" yaml:"outcomes"`
 }
 
+type UDPPortStatusAnalyze struct {
+	AnalyzeMeta   `json:",inline" yaml:",inline"`
+	CollectorName string     `json:"collectorName,omitempty" yaml:"collectorName,omitempty"`
+	Outcomes      []*Outcome `json:"outcomes" yaml:"outcomes"`
+}
+
 type DiskUsageAnalyze struct {
 	AnalyzeMeta   `json:",inline" yaml:",inline"`
 	CollectorName string     `json:"collectorName,omitempty" yaml:"collectorName,omitempty"`
@@ -115,6 +121,8 @@ type HostAnalyze struct {
 
 	TCPPortStatus *TCPPortStatusAnalyze `json:"tcpPortStatus,omitempty" yaml:"tcpPortStatus,omitempty"`
 
+	UDPPortStatus *UDPPortStatusAnalyze `json:"udpPortStatus,omitempty" yaml:"udpPortStatus,omitempty"`
+
 	HTTP *HTTPAnalyze `json:"http,omitempty" yaml:"http,omitempty"`
 
 	Time *TimeAnalyze `json:"time,omitempty" yaml:"time,omitempty"`
diff --git a/pkg/apis/troubleshoot/v1beta2/hostcollector_shared.go b/pkg/apis/troubleshoot/v1beta2/hostcollector_shared.go
index fb40dd30..d3ee8cce 100644
--- a/pkg/apis/troubleshoot/v1beta2/hostcollector_shared.go
+++ b/pkg/apis/troubleshoot/v1beta2/hostcollector_shared.go
@@ -39,6 +39,12 @@ type TCPPortStatus struct {
 	Port              int    `json:"port"`
 }
 
+type UDPPortStatus struct {
+	HostCollectorMeta `json:",inline" yaml:",inline"`
+	Interface         string `json:"interface,omitempty"`
+	Port              int    `json:"port"`
+}
+
 type Kubernetes struct {
 	HostCollectorMeta `json:",inline" yaml:",inline"`
 }
@@ -162,6 +168,7 @@ type HostCollect struct {
 	TCPLoadBalancer       *TCPLoadBalancer       `json:"tcpLoadBalancer,omitempty" yaml:"tcpLoadBalancer,omitempty"`
 	HTTPLoadBalancer      *HTTPLoadBalancer      `json:"httpLoadBalancer,omitempty" yaml:"httpLoadBalancer,omitempty"`
 	TCPPortStatus         *TCPPortStatus         `json:"tcpPortStatus,omitempty" yaml:"tcpPortStatus,omitempty"`
+	UDPPortStatus         *UDPPortStatus         `json:"udpPortStatus,omitempty" yaml:"udpPortStatus,omitempty"`
 	Kubernetes            *Kubernetes            `json:"kubernetes,omitempty" yaml:"kubernetes,omitempty"`
 	IPV4Interfaces        *IPV4Interfaces        `json:"ipv4Interfaces,omitempty" yaml:"ipv4Interfaces,omitempty"`
 	DiskUsage             *DiskUsage             `json:"diskUsage,omitempty" yaml:"diskUsage,omitempty"`
diff --git a/pkg/apis/troubleshoot/v1beta2/remote_collector_shared.go b/pkg/apis/troubleshoot/v1beta2/remote_collector_shared.go
index 0ada95fb..6fd8f4cb 100644
--- a/pkg/apis/troubleshoot/v1beta2/remote_collector_shared.go
+++ b/pkg/apis/troubleshoot/v1beta2/remote_collector_shared.go
@@ -42,6 +42,12 @@ type RemoteTCPPortStatus struct {
 	Port                int    `json:"port"`
 }
 
+type RemoteUDPPortStatus struct {
+	RemoteCollectorMeta `json:",inline" yaml:",inline"`
+	Interface           string `json:"interface,omitempty"`
+	Port                int    `json:"port"`
+}
+
 type RemoteKubernetes struct {
 	RemoteCollectorMeta `json:",inline" yaml:",inline"`
 }
@@ -141,6 +147,7 @@ type RemoteCollect struct {
 	TCPLoadBalancer       *RemoteTCPLoadBalancer       `json:"tcpLoadBalancer,omitempty" yaml:"tcpLoadBalancer,omitempty"`
 	HTTPLoadBalancer      *RemoteHTTPLoadBalancer      `json:"httpLoadBalancer,omitempty" yaml:"httpLoadBalancer,omitempty"`
 	TCPPortStatus         *RemoteTCPPortStatus         `json:"tcpPortStatus,omitempty" yaml:"tcpPortStatus,omitempty"`
+	UDPPortStatus         *RemoteUDPPortStatus         `json:"udpPortStatus,omitempty" yaml:"udpPortStatus,omitempty"`
 	IPV4Interfaces        *RemoteIPV4Interfaces        `json:"ipv4Interfaces,omitempty" yaml:"ipv4Interfaces,omitempty"`
 	DiskUsage             *RemoteDiskUsage             `json:"diskUsage,omitempty" yaml:"diskUsage,omitempty"`
 	HTTP                  *RemoteHTTP                  `json:"http,omitempty" yaml:"http,omitempty"`
diff --git a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go
index 2f76773a..29b154d1 100644
--- a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go
+++ b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go
@@ -1509,6 +1509,11 @@ func (in *HostAnalyze) DeepCopyInto(out *HostAnalyze) {
 		*out = new(TCPPortStatusAnalyze)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.UDPPortStatus != nil {
+		in, out := &in.UDPPortStatus, &out.UDPPortStatus
+		*out = new(UDPPortStatusAnalyze)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.HTTP != nil {
 		in, out := &in.HTTP, &out.HTTP
 		*out = new(HTTPAnalyze)
@@ -1620,6 +1625,11 @@ func (in *HostCollect) DeepCopyInto(out *HostCollect) {
 		*out = new(TCPPortStatus)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.UDPPortStatus != nil {
+		in, out := &in.UDPPortStatus, &out.UDPPortStatus
+		*out = new(UDPPortStatus)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.Kubernetes != nil {
 		in, out := &in.Kubernetes, &out.Kubernetes
 		*out = new(Kubernetes)
@@ -3101,6 +3111,11 @@ func (in *RemoteCollect) DeepCopyInto(out *RemoteCollect) {
 		*out = new(RemoteTCPPortStatus)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.UDPPortStatus != nil {
+		in, out := &in.UDPPortStatus, &out.UDPPortStatus
+		*out = new(RemoteUDPPortStatus)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.IPV4Interfaces != nil {
 		in, out := &in.IPV4Interfaces, &out.IPV4Interfaces
 		*out = new(RemoteIPV4Interfaces)
@@ -3530,6 +3545,22 @@ func (in *RemoteTime) DeepCopy() *RemoteTime {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *RemoteUDPPortStatus) DeepCopyInto(out *RemoteUDPPortStatus) {
+	*out = *in
+	in.RemoteCollectorMeta.DeepCopyInto(&out.RemoteCollectorMeta)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RemoteUDPPortStatus.
+func (in *RemoteUDPPortStatus) DeepCopy() *RemoteUDPPortStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(RemoteUDPPortStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Removals) DeepCopyInto(out *Removals) {
 	*out = *in
@@ -4233,6 +4264,49 @@ func (in *TimeAnalyze) DeepCopy() *TimeAnalyze {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *UDPPortStatus) DeepCopyInto(out *UDPPortStatus) {
+	*out = *in
+	in.HostCollectorMeta.DeepCopyInto(&out.HostCollectorMeta)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UDPPortStatus.
+func (in *UDPPortStatus) DeepCopy() *UDPPortStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(UDPPortStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *UDPPortStatusAnalyze) DeepCopyInto(out *UDPPortStatusAnalyze) {
+	*out = *in
+	in.AnalyzeMeta.DeepCopyInto(&out.AnalyzeMeta)
+	if in.Outcomes != nil {
+		in, out := &in.Outcomes, &out.Outcomes
+		*out = make([]*Outcome, len(*in))
+		for i := range *in {
+			if (*in)[i] != nil {
+				in, out := &(*in)[i], &(*out)[i]
+				*out = new(Outcome)
+				(*in).DeepCopyInto(*out)
+			}
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UDPPortStatusAnalyze.
+func (in *UDPPortStatusAnalyze) DeepCopy() *UDPPortStatusAnalyze {
+	if in == nil {
+		return nil
+	}
+	out := new(UDPPortStatusAnalyze)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *WeaveReportAnalyze) DeepCopyInto(out *WeaveReportAnalyze) {
 	*out = *in
diff --git a/pkg/client/troubleshootclientset/clientset.go b/pkg/client/troubleshootclientset/clientset.go
index cc5a2916..7af52d4d 100644
--- a/pkg/client/troubleshootclientset/clientset.go
+++ b/pkg/client/troubleshootclientset/clientset.go
@@ -19,6 +19,7 @@ package troubleshootclientset
 
 import (
 	"fmt"
+	"net/http"
 
 	troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta1"
 	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2"
@@ -33,8 +34,7 @@ type Interface interface {
 	TroubleshootV1beta2() troubleshootv1beta2.TroubleshootV1beta2Interface
 }
 
-// Clientset contains the clients for groups. Each group has exactly one
-// version included in a Clientset.
+// Clientset contains the clients for groups.
 type Clientset struct {
 	*discovery.DiscoveryClient
 	troubleshootV1beta1 *troubleshootv1beta1.TroubleshootV1beta1Client
@@ -62,26 +62,49 @@ func (c *Clientset) Discovery() discovery.DiscoveryInterface {
 // NewForConfig creates a new Clientset for the given config.
 // If config's RateLimiter is not set and QPS and Burst are acceptable,
 // NewForConfig will generate a rate-limiter in configShallowCopy.
+// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient),
+// where httpClient was generated with rest.HTTPClientFor(c).
 func NewForConfig(c *rest.Config) (*Clientset, error) {
 	configShallowCopy := *c
+
+	if configShallowCopy.UserAgent == "" {
+		configShallowCopy.UserAgent = rest.DefaultKubernetesUserAgent()
+	}
+
+	// share the transport between all clients
+	httpClient, err := rest.HTTPClientFor(&configShallowCopy)
+	if err != nil {
+		return nil, err
+	}
+
+	return NewForConfigAndClient(&configShallowCopy, httpClient)
+}
+
+// NewForConfigAndClient creates a new Clientset for the given config and http client.
+// Note the http client provided takes precedence over the configured transport values.
+// If config's RateLimiter is not set and QPS and Burst are acceptable,
+// NewForConfigAndClient will generate a rate-limiter in configShallowCopy.
+func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset, error) {
+	configShallowCopy := *c
 	if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 {
 		if configShallowCopy.Burst <= 0 {
 			return nil, fmt.Errorf("burst is required to be greater than 0 when RateLimiter is not set and QPS is set to greater than 0")
 		}
 		configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst)
 	}
+
 	var cs Clientset
 	var err error
-	cs.troubleshootV1beta1, err = troubleshootv1beta1.NewForConfig(&configShallowCopy)
+	cs.troubleshootV1beta1, err = troubleshootv1beta1.NewForConfigAndClient(&configShallowCopy, httpClient)
 	if err != nil {
 		return nil, err
 	}
-	cs.troubleshootV1beta2, err = troubleshootv1beta2.NewForConfig(&configShallowCopy)
+	cs.troubleshootV1beta2, err = troubleshootv1beta2.NewForConfigAndClient(&configShallowCopy, httpClient)
 	if err != nil {
 		return nil, err
 	}
 
-	cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(&configShallowCopy)
+	cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfigAndClient(&configShallowCopy, httpClient)
 	if err != nil {
 		return nil, err
 	}
@@ -91,12 +114,11 @@ func NewForConfig(c *rest.Config) (*Clientset, error) {
 // NewForConfigOrDie creates a new Clientset for the given config and
 // panics if there is an error in the config.
 func NewForConfigOrDie(c *rest.Config) *Clientset {
-	var cs Clientset
-	cs.troubleshootV1beta1 = troubleshootv1beta1.NewForConfigOrDie(c)
-	cs.troubleshootV1beta2 = troubleshootv1beta2.NewForConfigOrDie(c)
-
-	cs.DiscoveryClient = discovery.NewDiscoveryClientForConfigOrDie(c)
-	return &cs
+	cs, err := NewForConfig(c)
+	if err != nil {
+		panic(err)
+	}
+	return cs
 }
 
 // New creates a new Clientset for the given RESTClient.
diff --git a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta1/fake/fake_analyzer.go b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta1/fake/fake_analyzer.go
index ec566758..2661cb46 100644
--- a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta1/fake/fake_analyzer.go
+++ b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta1/fake/fake_analyzer.go
@@ -116,7 +116,7 @@ func (c *FakeAnalyzers) UpdateStatus(ctx context.Context, analyzer *v1beta1.Anal
 // Delete takes name of the analyzer and deletes it. Returns an error if one occurs.
 func (c *FakeAnalyzers) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
 	_, err := c.Fake.
-		Invokes(testing.NewDeleteAction(analyzersResource, c.ns, name), &v1beta1.Analyzer{})
+		Invokes(testing.NewDeleteActionWithOptions(analyzersResource, c.ns, name, opts), &v1beta1.Analyzer{})
 
 	return err
 }
diff --git a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta1/fake/fake_collector.go b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta1/fake/fake_collector.go
index 8ea3d5ec..2a43591e 100644
--- a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta1/fake/fake_collector.go
+++ b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta1/fake/fake_collector.go
@@ -116,7 +116,7 @@ func (c *FakeCollectors) UpdateStatus(ctx context.Context, collector *v1beta1.Co
 // Delete takes name of the collector and deletes it. Returns an error if one occurs.
 func (c *FakeCollectors) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
 	_, err := c.Fake.
-		Invokes(testing.NewDeleteAction(collectorsResource, c.ns, name), &v1beta1.Collector{})
+		Invokes(testing.NewDeleteActionWithOptions(collectorsResource, c.ns, name, opts), &v1beta1.Collector{})
 
 	return err
 }
diff --git a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta1/fake/fake_preflight.go b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta1/fake/fake_preflight.go
index 9e272470..35ddc41a 100644
--- a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta1/fake/fake_preflight.go
+++ b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta1/fake/fake_preflight.go
@@ -116,7 +116,7 @@ func (c *FakePreflights) UpdateStatus(ctx context.Context, preflight *v1beta1.Pr
 // Delete takes name of the preflight and deletes it. Returns an error if one occurs.
 func (c *FakePreflights) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
 	_, err := c.Fake.
-		Invokes(testing.NewDeleteAction(preflightsResource, c.ns, name), &v1beta1.Preflight{})
+		Invokes(testing.NewDeleteActionWithOptions(preflightsResource, c.ns, name, opts), &v1beta1.Preflight{})
 
 	return err
 }
diff --git a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta1/fake/fake_redactor.go b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta1/fake/fake_redactor.go
index 3109c798..261d5dd4 100644
--- a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta1/fake/fake_redactor.go
+++ b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta1/fake/fake_redactor.go
@@ -116,7 +116,7 @@ func (c *FakeRedactors) UpdateStatus(ctx context.Context, redactor *v1beta1.Reda
 // Delete takes name of the redactor and deletes it. Returns an error if one occurs.
 func (c *FakeRedactors) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
 	_, err := c.Fake.
-		Invokes(testing.NewDeleteAction(redactorsResource, c.ns, name), &v1beta1.Redactor{})
+		Invokes(testing.NewDeleteActionWithOptions(redactorsResource, c.ns, name, opts), &v1beta1.Redactor{})
 
 	return err
 }
diff --git a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta1/fake/fake_supportbundle.go b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta1/fake/fake_supportbundle.go
index 6b4b8e49..46d6b3a2 100644
--- a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta1/fake/fake_supportbundle.go
+++ b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta1/fake/fake_supportbundle.go
@@ -116,7 +116,7 @@ func (c *FakeSupportBundles) UpdateStatus(ctx context.Context, supportBundle *v1
 // Delete takes name of the supportBundle and deletes it. Returns an error if one occurs.
 func (c *FakeSupportBundles) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
 	_, err := c.Fake.
-		Invokes(testing.NewDeleteAction(supportbundlesResource, c.ns, name), &v1beta1.SupportBundle{})
+		Invokes(testing.NewDeleteActionWithOptions(supportbundlesResource, c.ns, name, opts), &v1beta1.SupportBundle{})
 
 	return err
 }
diff --git a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta1/troubleshoot_client.go b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta1/troubleshoot_client.go
index 8b5b3299..628b5ebc 100644
--- a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta1/troubleshoot_client.go
+++ b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta1/troubleshoot_client.go
@@ -18,6 +18,8 @@ limitations under the License.
 package v1beta1
 
 import (
+	"net/http"
+
 	v1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1"
 	"github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme"
 	rest "k8s.io/client-go/rest"
@@ -58,12 +60,28 @@ func (c *TroubleshootV1beta1Client) SupportBundles(namespace string) SupportBund
 }
 
 // NewForConfig creates a new TroubleshootV1beta1Client for the given config.
+// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient),
+// where httpClient was generated with rest.HTTPClientFor(c).
 func NewForConfig(c *rest.Config) (*TroubleshootV1beta1Client, error) {
 	config := *c
 	if err := setConfigDefaults(&config); err != nil {
 		return nil, err
 	}
-	client, err := rest.RESTClientFor(&config)
+	httpClient, err := rest.HTTPClientFor(&config)
+	if err != nil {
+		return nil, err
+	}
+	return NewForConfigAndClient(&config, httpClient)
+}
+
+// NewForConfigAndClient creates a new TroubleshootV1beta1Client for the given config and http client.
+// Note the http client provided takes precedence over the configured transport values.
+func NewForConfigAndClient(c *rest.Config, h *http.Client) (*TroubleshootV1beta1Client, error) {
+	config := *c
+	if err := setConfigDefaults(&config); err != nil {
+		return nil, err
+	}
+	client, err := rest.RESTClientForConfigAndClient(&config, h)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_analyzer.go b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_analyzer.go
index b17f4584..c15c4745 100644
--- a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_analyzer.go
+++ b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_analyzer.go
@@ -116,7 +116,7 @@ func (c *FakeAnalyzers) UpdateStatus(ctx context.Context, analyzer *v1beta2.Anal
 // Delete takes name of the analyzer and deletes it. Returns an error if one occurs.
 func (c *FakeAnalyzers) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
 	_, err := c.Fake.
-		Invokes(testing.NewDeleteAction(analyzersResource, c.ns, name), &v1beta2.Analyzer{})
+		Invokes(testing.NewDeleteActionWithOptions(analyzersResource, c.ns, name, opts), &v1beta2.Analyzer{})
 
 	return err
 }
diff --git a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_collector.go b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_collector.go
index d352dd09..1f9c0053 100644
--- a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_collector.go
+++ b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_collector.go
@@ -116,7 +116,7 @@ func (c *FakeCollectors) UpdateStatus(ctx context.Context, collector *v1beta2.Co
 // Delete takes name of the collector and deletes it. Returns an error if one occurs.
 func (c *FakeCollectors) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
 	_, err := c.Fake.
-		Invokes(testing.NewDeleteAction(collectorsResource, c.ns, name), &v1beta2.Collector{})
+		Invokes(testing.NewDeleteActionWithOptions(collectorsResource, c.ns, name, opts), &v1beta2.Collector{})
 
 	return err
 }
diff --git a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_hostcollector.go b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_hostcollector.go
index 837c7576..cd9d1b6a 100644
--- a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_hostcollector.go
+++ b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_hostcollector.go
@@ -116,7 +116,7 @@ func (c *FakeHostCollectors) UpdateStatus(ctx context.Context, hostCollector *v1
 // Delete takes name of the hostCollector and deletes it. Returns an error if one occurs.
 func (c *FakeHostCollectors) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
 	_, err := c.Fake.
-		Invokes(testing.NewDeleteAction(hostcollectorsResource, c.ns, name), &v1beta2.HostCollector{})
+		Invokes(testing.NewDeleteActionWithOptions(hostcollectorsResource, c.ns, name, opts), &v1beta2.HostCollector{})
 
 	return err
 }
diff --git a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_hostpreflight.go b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_hostpreflight.go
index 761fd6d8..57e5b907 100644
--- a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_hostpreflight.go
+++ b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_hostpreflight.go
@@ -116,7 +116,7 @@ func (c *FakeHostPreflights) UpdateStatus(ctx context.Context, hostPreflight *v1
 // Delete takes name of the hostPreflight and deletes it. Returns an error if one occurs.
 func (c *FakeHostPreflights) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
 	_, err := c.Fake.
-		Invokes(testing.NewDeleteAction(hostpreflightsResource, c.ns, name), &v1beta2.HostPreflight{})
+		Invokes(testing.NewDeleteActionWithOptions(hostpreflightsResource, c.ns, name, opts), &v1beta2.HostPreflight{})
 
 	return err
 }
diff --git a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_preflight.go b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_preflight.go
index 67ee4666..a4014d75 100644
--- a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_preflight.go
+++ b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_preflight.go
@@ -116,7 +116,7 @@ func (c *FakePreflights) UpdateStatus(ctx context.Context, preflight *v1beta2.Pr
 // Delete takes name of the preflight and deletes it. Returns an error if one occurs.
 func (c *FakePreflights) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
 	_, err := c.Fake.
-		Invokes(testing.NewDeleteAction(preflightsResource, c.ns, name), &v1beta2.Preflight{})
+		Invokes(testing.NewDeleteActionWithOptions(preflightsResource, c.ns, name, opts), &v1beta2.Preflight{})
 
 	return err
 }
diff --git a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_redactor.go b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_redactor.go
index 43349660..3bfbf5fa 100644
--- a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_redactor.go
+++ b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_redactor.go
@@ -116,7 +116,7 @@ func (c *FakeRedactors) UpdateStatus(ctx context.Context, redactor *v1beta2.Reda
 // Delete takes name of the redactor and deletes it. Returns an error if one occurs.
 func (c *FakeRedactors) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
 	_, err := c.Fake.
-		Invokes(testing.NewDeleteAction(redactorsResource, c.ns, name), &v1beta2.Redactor{})
+		Invokes(testing.NewDeleteActionWithOptions(redactorsResource, c.ns, name, opts), &v1beta2.Redactor{})
 
 	return err
 }
diff --git a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_remotecollector.go b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_remotecollector.go
index ae0ddb17..a9bcadf8 100644
--- a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_remotecollector.go
+++ b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_remotecollector.go
@@ -116,7 +116,7 @@ func (c *FakeRemoteCollectors) UpdateStatus(ctx context.Context, remoteCollector
 // Delete takes name of the remoteCollector and deletes it. Returns an error if one occurs.
 func (c *FakeRemoteCollectors) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
 	_, err := c.Fake.
-		Invokes(testing.NewDeleteAction(remotecollectorsResource, c.ns, name), &v1beta2.RemoteCollector{})
+		Invokes(testing.NewDeleteActionWithOptions(remotecollectorsResource, c.ns, name, opts), &v1beta2.RemoteCollector{})
 
 	return err
 }
diff --git a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_supportbundle.go b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_supportbundle.go
index e5cb8ad8..e71c0cfa 100644
--- a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_supportbundle.go
+++ b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_supportbundle.go
@@ -116,7 +116,7 @@ func (c *FakeSupportBundles) UpdateStatus(ctx context.Context, supportBundle *v1
 // Delete takes name of the supportBundle and deletes it. Returns an error if one occurs.
 func (c *FakeSupportBundles) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
 	_, err := c.Fake.
-		Invokes(testing.NewDeleteAction(supportbundlesResource, c.ns, name), &v1beta2.SupportBundle{})
+		Invokes(testing.NewDeleteActionWithOptions(supportbundlesResource, c.ns, name, opts), &v1beta2.SupportBundle{})
 
 	return err
 }
diff --git a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/troubleshoot_client.go b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/troubleshoot_client.go
index 6c3a0fea..ae137af9 100644
--- a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/troubleshoot_client.go
+++ b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/troubleshoot_client.go
@@ -18,6 +18,8 @@ limitations under the License.
 package v1beta2
 
 import (
+	"net/http"
+
 	v1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
 	"github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme"
 	rest "k8s.io/client-go/rest"
@@ -73,12 +75,28 @@ func (c *TroubleshootV1beta2Client) SupportBundles(namespace string) SupportBund
 }
 
 // NewForConfig creates a new TroubleshootV1beta2Client for the given config.
+// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient),
+// where httpClient was generated with rest.HTTPClientFor(c).
 func NewForConfig(c *rest.Config) (*TroubleshootV1beta2Client, error) {
 	config := *c
 	if err := setConfigDefaults(&config); err != nil {
 		return nil, err
 	}
-	client, err := rest.RESTClientFor(&config)
+	httpClient, err := rest.HTTPClientFor(&config)
+	if err != nil {
+		return nil, err
+	}
+	return NewForConfigAndClient(&config, httpClient)
+}
+
+// NewForConfigAndClient creates a new TroubleshootV1beta2Client for the given config and http client.
+// Note the http client provided takes precedence over the configured transport values.
+func NewForConfigAndClient(c *rest.Config, h *http.Client) (*TroubleshootV1beta2Client, error) {
+	config := *c
+	if err := setConfigDefaults(&config); err != nil {
+		return nil, err
+	}
+	client, err := rest.RESTClientForConfigAndClient(&config, h)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/collect/cluster_resources.go b/pkg/collect/cluster_resources.go
index bed27877..1e7c17a3 100644
--- a/pkg/collect/cluster_resources.go
+++ b/pkg/collect/cluster_resources.go
@@ -12,6 +12,7 @@ import (
 	"strings"
 
 	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+	"github.com/replicatedhq/troubleshoot/pkg/constants"
 	"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
 	"gopkg.in/yaml.v2"
 	authorizationv1 "k8s.io/api/authorization/v1"
@@ -118,17 +119,17 @@ func (c *CollectClusterResources) Collect(progressChan chan<- interface{}) (Coll
 	if len(c.Collector.Namespaces) > 0 {
 		namespaces, namespaceErrors := getNamespaces(ctx, client, c.Collector.Namespaces)
 		namespaceNames = c.Collector.Namespaces
-		output.SaveResult(c.BundlePath, "cluster-resources/namespaces.json", bytes.NewBuffer(namespaces))
-		output.SaveResult(c.BundlePath, "cluster-resources/namespaces-errors.json", marshalErrors(namespaceErrors))
+		output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_NAMESPACES)), bytes.NewBuffer(namespaces))
+		output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_NAMESPACES)), marshalErrors(namespaceErrors))
 	} else if c.Namespace != "" {
 		namespace, namespaceErrors := getNamespace(ctx, client, c.Namespace)
-		output.SaveResult(c.BundlePath, "cluster-resources/namespaces.json", bytes.NewBuffer(namespace))
-		output.SaveResult(c.BundlePath, "cluster-resources/namespaces-errors.json", marshalErrors(namespaceErrors))
+		output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_NAMESPACES)), bytes.NewBuffer(namespace))
+		output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_NAMESPACES)), marshalErrors(namespaceErrors))
 		namespaceNames = append(namespaceNames, c.Namespace)
 	} else {
 		namespaces, namespaceList, namespaceErrors := getAllNamespaces(ctx, client)
-		output.SaveResult(c.BundlePath, "cluster-resources/namespaces.json", bytes.NewBuffer(namespaces))
-		output.SaveResult(c.BundlePath, "cluster-resources/namespaces-errors.json", marshalErrors(namespaceErrors))
+		output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_NAMESPACES)), bytes.NewBuffer(namespaces))
+		output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_NAMESPACES)), marshalErrors(namespaceErrors))
 		if namespaceList != nil {
 			for _, namespace := range namespaceList.Items {
 				namespaceNames = append(namespaceNames, namespace.Name)
@@ -142,9 +143,9 @@ func (c *CollectClusterResources) Collect(progressChan chan<- interface{}) (Coll
 	// auth cani
 	authCanI := authCanI(reviewStatuses, namespaceNames)
 	for k, v := range authCanI {
-		output.SaveResult(c.BundlePath, path.Join("cluster-resources/auth-cani-list", k), bytes.NewBuffer(v))
+		output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_AUTH_CANI, k), bytes.NewBuffer(v))
 	}
-	output.SaveResult(c.BundlePath, "cluster-resources/auth-cani-list-errors.json", marshalErrors(reviewStatusErrors))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_AUTH_CANI)), marshalErrors(reviewStatusErrors))
 
 	if nsListedFromCluster && !c.Collector.IgnoreRBAC {
 		filteredNamespaces := []string{}
@@ -160,9 +161,9 @@ func (c *CollectClusterResources) Collect(progressChan chan<- interface{}) (Coll
 	// pods
 	pods, podErrors, unhealthyPods := pods(ctx, client, namespaceNames)
 	for k, v := range pods {
-		output.SaveResult(c.BundlePath, path.Join("cluster-resources/pods", k), bytes.NewBuffer(v))
+		output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_PODS, k), bytes.NewBuffer(v))
 	}
-	output.SaveResult(c.BundlePath, "cluster-resources/pods-errors.json", marshalErrors(podErrors))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_PODS)), marshalErrors(podErrors))
 
 	for _, pod := range unhealthyPods {
 		allContainers := append(pod.Spec.InitContainers, pod.Spec.Containers...)
@@ -176,7 +177,7 @@ func (c *CollectClusterResources) Collect(progressChan chan<- interface{}) (Coll
 			}
 			podLogs, err := savePodLogs(ctx, c.BundlePath, client, &pod, "", container.Name, limits, false, false)
 			if err != nil {
-				errPath := filepath.Join("cluster-resources", "pods", "logs", pod.Namespace, pod.Name, fmt.Sprintf("%s-logs-errors.log", container.Name))
+				errPath := filepath.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_PODS_LOGS, pod.Namespace, pod.Name, fmt.Sprintf("%s-logs-errors.log", container.Name))
 				output.SaveResult(c.BundlePath, errPath, bytes.NewBuffer([]byte(err.Error())))
 			}
 			// Add logs collector results to the rest of the output
@@ -188,156 +189,156 @@ func (c *CollectClusterResources) Collect(progressChan chan<- interface{}) (Coll
 
 	PodDisruptionBudgets, pdbError := getPodDisruptionBudgets(ctx, client, namespaceNames)
 	for k, v := range PodDisruptionBudgets {
-		output.SaveResult(c.BundlePath, path.Join("cluster-resources/pod-disruption-budgets", k), bytes.NewBuffer(v))
+		output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_POD_DISRUPTION_BUDGETS, k), bytes.NewBuffer(v))
 	}
-	output.SaveResult(c.BundlePath, "cluster-resources/pod-disruption-budgets-info.json", marshalErrors(pdbError))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_POD_DISRUPTION_BUDGETS)), marshalErrors(pdbError))
 
 	// services
 	services, servicesErrors := services(ctx, client, namespaceNames)
 	for k, v := range services {
-		output.SaveResult(c.BundlePath, path.Join("cluster-resources/services", k), bytes.NewBuffer(v))
+		output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_SERVICES, k), bytes.NewBuffer(v))
 	}
-	output.SaveResult(c.BundlePath, "cluster-resources/services-errors.json", marshalErrors(servicesErrors))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_SERVICES)), marshalErrors(servicesErrors))
 
 	// deployments
 	deployments, deploymentsErrors := deployments(ctx, client, namespaceNames)
 	for k, v := range deployments {
-		output.SaveResult(c.BundlePath, path.Join("cluster-resources/deployments", k), bytes.NewBuffer(v))
+		output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_DEPLOYMENTS, k), bytes.NewBuffer(v))
 	}
-	output.SaveResult(c.BundlePath, "cluster-resources/deployments-errors.json", marshalErrors(deploymentsErrors))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_DEPLOYMENTS)), marshalErrors(deploymentsErrors))
 
 	// statefulsets
 	statefulsets, statefulsetsErrors := statefulsets(ctx, client, namespaceNames)
 	for k, v := range statefulsets {
-		output.SaveResult(c.BundlePath, path.Join("cluster-resources/statefulsets", k), bytes.NewBuffer(v))
+		output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_STATEFULSETS, k), bytes.NewBuffer(v))
 	}
-	output.SaveResult(c.BundlePath, "cluster-resources/statefulsets-errors.json", marshalErrors(statefulsetsErrors))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_STATEFULSETS)), marshalErrors(statefulsetsErrors))
 
 	// replicasets
 	replicasets, replicasetsErrors := replicasets(ctx, client, namespaceNames)
 	for k, v := range replicasets {
-		output.SaveResult(c.BundlePath, path.Join("cluster-resources/replicasets", k), bytes.NewBuffer(v))
+		output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_STATEFULSETS), k), bytes.NewBuffer(v))
 	}
-	output.SaveResult(c.BundlePath, "cluster-resources/replicasets-errors.json", marshalErrors(replicasetsErrors))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_REPLICASETS)), marshalErrors(replicasetsErrors))
 
 	// jobs
 	jobs, jobsErrors := jobs(ctx, client, namespaceNames)
 	for k, v := range jobs {
-		output.SaveResult(c.BundlePath, path.Join("cluster-resources/jobs", k), bytes.NewBuffer(v))
+		output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_JOBS, k), bytes.NewBuffer(v))
 	}
-	output.SaveResult(c.BundlePath, "cluster-resources/jobs-errors.json", marshalErrors(jobsErrors))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_JOBS)), marshalErrors(jobsErrors))
 
 	// cronJobs
 	cronJobs, cronJobsErrors := cronJobs(ctx, client, namespaceNames)
 	for k, v := range cronJobs {
-		output.SaveResult(c.BundlePath, path.Join("cluster-resources/cronjobs", k), bytes.NewBuffer(v))
+		output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_CRONJOBS, k), bytes.NewBuffer(v))
 	}
-	output.SaveResult(c.BundlePath, "cluster-resources/cronjobs-errors.json", marshalErrors(cronJobsErrors))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_CRONJOBS)), marshalErrors(cronJobsErrors))
 
 	// ingress
 	ingress, ingressErrors := ingress(ctx, client, namespaceNames)
 	for k, v := range ingress {
-		output.SaveResult(c.BundlePath, path.Join("cluster-resources/ingress", k), bytes.NewBuffer(v))
+		output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_INGRESS, k), bytes.NewBuffer(v))
 	}
-	output.SaveResult(c.BundlePath, "cluster-resources/ingress-errors.json", marshalErrors(ingressErrors))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_INGRESS)), marshalErrors(ingressErrors))
 
 	// network policy
 	networkPolicy, networkPolicyErrors := networkPolicy(ctx, client, namespaceNames)
 	for k, v := range networkPolicy {
-		output.SaveResult(c.BundlePath, path.Join("cluster-resources/network-policy", k), bytes.NewBuffer(v))
+		output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_NETWORK_POLICY, k), bytes.NewBuffer(v))
 	}
-	output.SaveResult(c.BundlePath, "cluster-resources/network-policy-errors.json", marshalErrors(networkPolicyErrors))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_NETWORK_POLICY)), marshalErrors(networkPolicyErrors))
 
 	// resource quotas
 	resourceQuota, resourceQuotaErrors := resourceQuota(ctx, client, namespaceNames)
 	for k, v := range resourceQuota {
-		output.SaveResult(c.BundlePath, path.Join("cluster-resources/resource-quotas", k), bytes.NewBuffer(v))
+		output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_RESOURCE_QUOTA, k), bytes.NewBuffer(v))
 	}
-	output.SaveResult(c.BundlePath, "cluster-resources/resource-quota-errors.json", marshalErrors(resourceQuotaErrors))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_RESOURCE_QUOTA)), marshalErrors(resourceQuotaErrors))
 
 	// storage classes
 	storageClasses, storageErrors := storageClasses(ctx, client)
-	output.SaveResult(c.BundlePath, "cluster-resources/storage-classes.json", bytes.NewBuffer(storageClasses))
-	output.SaveResult(c.BundlePath, "cluster-resources/storage-errors.json", marshalErrors(storageErrors))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_STORAGE_CLASS)), bytes.NewBuffer(storageClasses))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_STORAGE_CLASS)), marshalErrors(storageErrors))
 
 	// crds
 	customResourceDefinitions, crdErrors := crds(ctx, client, c.ClientConfig)
-	output.SaveResult(c.BundlePath, "cluster-resources/custom-resource-definitions.json", bytes.NewBuffer(customResourceDefinitions))
-	output.SaveResult(c.BundlePath, "cluster-resources/custom-resource-definitions-errors.json", marshalErrors(crdErrors))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_CUSTOM_RESOURCE_DEFINITIONS)), bytes.NewBuffer(customResourceDefinitions))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_CUSTOM_RESOURCE_DEFINITIONS)), marshalErrors(crdErrors))
 
 	// crs
 	customResources, crErrors := crs(ctx, dynamicClient, client, c.ClientConfig, namespaceNames)
 	for k, v := range customResources {
-		output.SaveResult(c.BundlePath, fmt.Sprintf("cluster-resources/custom-resources/%v", k), bytes.NewBuffer(v))
+		output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_CUSTOM_RESOURCE_DEFINITIONS, k), bytes.NewBuffer(v))
 	}
-	output.SaveResult(c.BundlePath, "cluster-resources/custom-resources/custom-resources-errors.json", marshalErrors(crErrors))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_CUSTOM_RESOURCE_DEFINITIONS, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_CUSTOM_RESOURCE_DEFINITIONS)), marshalErrors(crErrors))
 
 	// imagepullsecrets
 	imagePullSecrets, pullSecretsErrors := imagePullSecrets(ctx, client, namespaceNames)
 	for k, v := range imagePullSecrets {
-		output.SaveResult(c.BundlePath, path.Join("cluster-resources/image-pull-secrets", k), bytes.NewBuffer(v))
+		output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_IMAGE_PULL_SECRETS, k), bytes.NewBuffer(v))
 	}
-	output.SaveResult(c.BundlePath, "cluster-resources/image-pull-secrets-errors.json", marshalErrors(pullSecretsErrors))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_IMAGE_PULL_SECRETS)), marshalErrors(pullSecretsErrors))
 
 	// nodes
 	nodes, nodeErrors := nodes(ctx, client)
-	output.SaveResult(c.BundlePath, "cluster-resources/nodes.json", bytes.NewBuffer(nodes))
-	output.SaveResult(c.BundlePath, "cluster-resources/nodes-errors.json", marshalErrors(nodeErrors))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_NODES)), bytes.NewBuffer(nodes))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_NODES)), marshalErrors(nodeErrors))
 
 	groups, resources, groupsResourcesErrors := apiResources(ctx, client)
-	output.SaveResult(c.BundlePath, "cluster-resources/groups.json", bytes.NewBuffer(groups))
-	output.SaveResult(c.BundlePath, "cluster-resources/resources.json", bytes.NewBuffer(resources))
-	output.SaveResult(c.BundlePath, "cluster-resources/groups-resources-errors.json", marshalErrors(groupsResourcesErrors))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_GROUPS)), bytes.NewBuffer(groups))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_RESOURCES)), bytes.NewBuffer(resources))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-%s-errors.json", constants.CLUSTER_RESOURCES_GROUPS, constants.CLUSTER_RESOURCES_RESOURCES)), marshalErrors(groupsResourcesErrors))
 
 	// limit ranges
 	limitRanges, limitRangesErrors := limitRanges(ctx, client, namespaceNames)
 	for k, v := range limitRanges {
-		output.SaveResult(c.BundlePath, path.Join("cluster-resources/limitranges", k), bytes.NewBuffer(v))
+		output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_LIMITRANGES, k), bytes.NewBuffer(v))
 	}
-	output.SaveResult(c.BundlePath, "cluster-resources/limitranges-errors.json", marshalErrors(limitRangesErrors))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_LIMITRANGES)), marshalErrors(limitRangesErrors))
 
 	//Events
 	events, eventsErrors := events(ctx, client, namespaceNames)
 	for k, v := range events {
-		output.SaveResult(c.BundlePath, path.Join("cluster-resources/events", k), bytes.NewBuffer(v))
+		output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_EVENTS, k), bytes.NewBuffer(v))
 	}
-	output.SaveResult(c.BundlePath, "cluster-resources/events-errors.json", marshalErrors(eventsErrors))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_EVENTS)), marshalErrors(eventsErrors))
 
 	//Persistent Volumes
 	pvs, pvsErrors := pvs(ctx, client)
-	output.SaveResult(c.BundlePath, "cluster-resources/pvs.json", bytes.NewBuffer(pvs))
-	output.SaveResult(c.BundlePath, "cluster-resources/pvs-errors.json", marshalErrors(pvsErrors))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_PVS), bytes.NewBuffer(pvs))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_PVS)), marshalErrors(pvsErrors))
 
 	//Persistent Volume Claims
 	pvcs, pvcsErrors := pvcs(ctx, client, namespaceNames)
 	for k, v := range pvcs {
-		output.SaveResult(c.BundlePath, path.Join("cluster-resources/pvcs", k), bytes.NewBuffer(v))
+		output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_PVCS, k), bytes.NewBuffer(v))
 	}
-	output.SaveResult(c.BundlePath, "cluster-resources/pvcs-errors.json", marshalErrors(pvcsErrors))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_PVCS)), marshalErrors(pvcsErrors))
 
 	//Roles
 	roles, rolesErrors := roles(ctx, client, namespaceNames)
 	for k, v := range roles {
-		output.SaveResult(c.BundlePath, path.Join("cluster-resources/roles", k), bytes.NewBuffer(v))
+		output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_ROLES, k), bytes.NewBuffer(v))
 	}
-	output.SaveResult(c.BundlePath, "cluster-resources/roles-errors.json", marshalErrors(rolesErrors))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_ROLES)), marshalErrors(rolesErrors))
 
 	//Role Bindings
 	roleBindings, roleBindingsErrors := roleBindings(ctx, client, namespaceNames)
 	for k, v := range roleBindings {
-		output.SaveResult(c.BundlePath, path.Join("cluster-resources/rolebindings", k), bytes.NewBuffer(v))
+		output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_ROLE_BINDINGS, k), bytes.NewBuffer(v))
 	}
-	output.SaveResult(c.BundlePath, "cluster-resources/rolebindings-errors.json", marshalErrors(roleBindingsErrors))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_ROLE_BINDINGS)), marshalErrors(roleBindingsErrors))
 
 	//Cluster Roles
 	clusterRoles, clusterRolesErrors := clusterRoles(ctx, client)
-	output.SaveResult(c.BundlePath, "cluster-resources/clusterroles.json", bytes.NewBuffer(clusterRoles))
-	output.SaveResult(c.BundlePath, "cluster-resources/clusterroles-errors.json", marshalErrors(clusterRolesErrors))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_CLUSTER_ROLES), bytes.NewBuffer(clusterRoles))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_CLUSTER_ROLES)), marshalErrors(clusterRolesErrors))
 
 	//Cluster Role Bindings
 	clusterRoleBindings, clusterRoleBindingsErrors := clusterRoleBindings(ctx, client)
-	output.SaveResult(c.BundlePath, "cluster-resources/clusterRoleBindings.json", bytes.NewBuffer(clusterRoleBindings))
-	output.SaveResult(c.BundlePath, "cluster-resources/clusterRoleBindings-errors.json", marshalErrors(clusterRoleBindingsErrors))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_CLUSTER_ROLE_BINDINGS), bytes.NewBuffer(clusterRoleBindings))
+	output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_CLUSTER_ROLE_BINDINGS)), marshalErrors(clusterRoleBindingsErrors))
 
 	return output, nil
 }
diff --git a/pkg/collect/host_collector.go b/pkg/collect/host_collector.go
index d7ba5749..9e538922 100644
--- a/pkg/collect/host_collector.go
+++ b/pkg/collect/host_collector.go
@@ -24,6 +24,8 @@ func GetHostCollector(collector *troubleshootv1beta2.HostCollect, bundlePath str
 		return &CollectHostDiskUsage{collector.DiskUsage, bundlePath}, true
 	case collector.TCPPortStatus != nil:
 		return &CollectHostTCPPortStatus{collector.TCPPortStatus, bundlePath}, true
+	case collector.UDPPortStatus != nil:
+		return &CollectHostUDPPortStatus{collector.UDPPortStatus, bundlePath}, true
 	case collector.HTTP != nil:
 		return &CollectHostHTTP{collector.HTTP, bundlePath}, true
 	case collector.Time != nil:
diff --git a/pkg/collect/host_udpportstatus.go b/pkg/collect/host_udpportstatus.go
new file mode 100644
index 00000000..f190fe41
--- /dev/null
+++ b/pkg/collect/host_udpportstatus.go
@@ -0,0 +1,78 @@
+package collect
+
+import (
+	"bytes"
+	"encoding/json"
+	"net"
+	"path/filepath"
+	"strings"
+
+	"github.com/pkg/errors"
+	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+)
+
+type CollectHostUDPPortStatus struct {
+	hostCollector *troubleshootv1beta2.UDPPortStatus
+	BundlePath    string
+}
+
+func (c *CollectHostUDPPortStatus) Title() string {
+	return hostCollectorTitleOrDefault(c.hostCollector.HostCollectorMeta, "UDP Port Status")
+}
+
+func (c *CollectHostUDPPortStatus) IsExcluded() (bool, error) {
+	return isExcluded(c.hostCollector.Exclude)
+}
+
+func (c *CollectHostUDPPortStatus) Collect(progressChan chan<- interface{}) (map[string][]byte, error) {
+	listenAddress := net.UDPAddr{
+		IP:   net.ParseIP("0.0.0.0"),
+		Port: c.hostCollector.Port,
+	}
+
+	if c.hostCollector.Interface != "" {
+		iface, err := net.InterfaceByName(c.hostCollector.Interface)
+		if err != nil {
+			return nil, errors.Wrapf(err, "lookup interface %s", c.hostCollector.Interface)
+		}
+		ip, err := getIPv4FromInterface(iface)
+		if err != nil {
+			return nil, errors.Wrapf(err, "get ipv4 address for interface %s", c.hostCollector.Interface)
+		}
+		listenAddress.IP = ip
+	}
+
+	var networkStatus NetworkStatus
+	lstn, err := net.ListenUDP("udp", &listenAddress)
+	if err != nil {
+		if strings.Contains(err.Error(), "address already in use") {
+			networkStatus = NetworkStatusAddressInUse
+		} else {
+			networkStatus = NetworkStatusErrorOther
+		}
+	} else {
+		networkStatus = NetworkStatusConnected
+		lstn.Close()
+	}
+
+	result := NetworkStatusResult{
+		Status: networkStatus,
+	}
+	b, err := json.Marshal(result)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to marshal result")
+	}
+
+	collectorName := c.hostCollector.CollectorName
+	if collectorName == "" {
+		collectorName = "udpPortStatus"
+	}
+	name := filepath.Join("host-collectors/udpPortStatus", collectorName+".json")
+
+	output := NewResult()
+	output.SaveResult(c.BundlePath, name, bytes.NewBuffer(b))
+
+	return map[string][]byte{
+		name: b,
+	}, nil
+}
diff --git a/pkg/collect/logs.go b/pkg/collect/logs.go
index 2b5d60fd..666f42ec 100644
--- a/pkg/collect/logs.go
+++ b/pkg/collect/logs.go
@@ -173,7 +173,7 @@ func savePodLogs(
 	// TODO: Abstract away hard coded directory structure paths
 	// Maybe create a FS provider or something similar
 	filePathPrefix := filepath.Join(
-		"cluster-resources", "pods", "logs", pod.Namespace, pod.Name, pod.Spec.Containers[0].Name,
+		constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_PODS_LOGS, pod.Namespace, pod.Name, pod.Spec.Containers[0].Name,
 	)
 
 	// TODO: If collectorName is empty, the path is stored with a leading slash
@@ -185,7 +185,7 @@ func savePodLogs(
 	if container != "" {
 		linkRelPathPrefix = fmt.Sprintf("%s/%s/%s", collectorName, pod.Name, container)
 		filePathPrefix = filepath.Join(
-			"cluster-resources", "pods", "logs", pod.Namespace, pod.Name, container,
+			constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_PODS_LOGS, pod.Namespace, pod.Name, container,
 		)
 	}
 
diff --git a/pkg/collect/remote_collector.go b/pkg/collect/remote_collector.go
index e2481acf..6db6ab96 100644
--- a/pkg/collect/remote_collector.go
+++ b/pkg/collect/remote_collector.go
@@ -205,6 +205,15 @@ func (c *RemoteCollector) toHostCollector() (*troubleshootv1beta2.HostCollect, e
 			Interface: c.Collect.TCPPortStatus.Interface,
 			Port:      c.Collect.TCPPortStatus.Port,
 		}
+	case c.Collect.UDPPortStatus != nil:
+		hostCollect.UDPPortStatus = &troubleshootv1beta2.UDPPortStatus{
+			HostCollectorMeta: troubleshootv1beta2.HostCollectorMeta{
+				CollectorName: c.Collect.UDPPortStatus.CollectorName,
+				Exclude:       c.Collect.UDPPortStatus.Exclude,
+			},
+			Interface: c.Collect.UDPPortStatus.Interface,
+			Port:      c.Collect.UDPPortStatus.Port,
+		}
 	case c.Collect.HTTP != nil:
 		hostCollect.HTTP = &troubleshootv1beta2.HostHTTP{
 			HostCollectorMeta: troubleshootv1beta2.HostCollectorMeta{
diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go
index 05194e60..0238224d 100644
--- a/pkg/constants/constants.go
+++ b/pkg/constants/constants.go
@@ -13,4 +13,36 @@ const (
 	VersionFilename = "version.yaml"
 	// DEFAULT_LOGS_COLLECTOR_TIMEOUT is the default timeout for logs collector.
 	DEFAULT_LOGS_COLLECTOR_TIMEOUT = 60 * time.Second
+
+	// Cluster Resources Collector Directories
+	CLUSTER_RESOURCES_DIR                         = "cluster-resources"
+	CLUSTER_RESOURCES_NAMESPACES                  = "namespace"
+	CLUSTER_RESOURCES_AUTH_CANI                   = "auth-cani-list"
+	CLUSTER_RESOURCES_PODS                        = "pods"
+	CLUSTER_RESOURCES_PODS_LOGS                   = "pods/logs"
+	CLUSTER_RESOURCES_POD_DISRUPTION_BUDGETS      = "pod-disruption-budgets"
+	CLUSTER_RESOURCES_SERVICES                    = "services"
+	CLUSTER_RESOURCES_DEPLOYMENTS                 = "deployments"
+	CLUSTER_RESOURCES_REPLICASETS                 = "replicasets"
+	CLUSTER_RESOURCES_STATEFULSETS                = "statefulsets"
+	CLUSTER_RESOURCES_JOBS                        = "jobs"
+	CLUSTER_RESOURCES_CRONJOBS                    = "cronjobs"
+	CLUSTER_RESOURCES_INGRESS                     = "ingress"
+	CLUSTER_RESOURCES_NETWORK_POLICY              = "network-policy"
+	CLUSTER_RESOURCES_RESOURCE_QUOTA              = "resource-quota"
+	CLUSTER_RESOURCES_STORAGE_CLASS               = "storage-classes"
+	CLUSTER_RESOURCES_CUSTOM_RESOURCE_DEFINITIONS = "custom-resource-definitions"
+	CLUSTER_RESOURCES_CUSTOM_RESOURCES            = "custom-resources"
+	CLUSTER_RESOURCES_IMAGE_PULL_SECRETS          = "image-pull-secrets"
+	CLUSTER_RESOURCES_NODES                       = "nodes"
+	CLUSTER_RESOURCES_GROUPS                      = "groups"
+	CLUSTER_RESOURCES_RESOURCES                   = "resources"
+	CLUSTER_RESOURCES_LIMITRANGES                 = "limitranges"
+	CLUSTER_RESOURCES_EVENTS                      = "events"
+	CLUSTER_RESOURCES_PVS                         = "pvs"
+	CLUSTER_RESOURCES_PVCS                        = "pvcs"
+	CLUSTER_RESOURCES_ROLES                       = "roles"
+	CLUSTER_RESOURCES_ROLE_BINDINGS               = "rolebindings"
+	CLUSTER_RESOURCES_CLUSTER_ROLES               = "clusterroles"
+	CLUSTER_RESOURCES_CLUSTER_ROLE_BINDINGS       = "clusterRoleBindings"
 )
diff --git a/pkg/redact/redact.go b/pkg/redact/redact.go
index 29312e35..920a3756 100644
--- a/pkg/redact/redact.go
+++ b/pkg/redact/redact.go
@@ -10,6 +10,7 @@ import (
 	"github.com/gobwas/glob"
 	"github.com/pkg/errors"
 	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+	"github.com/replicatedhq/troubleshoot/pkg/constants"
 )
 
 const (
@@ -333,7 +334,7 @@ func getRedactors(path string) ([]Redactor, error) {
 
 	uniqueCRs := map[string]bool{}
 	for _, cr := range customResources {
-		fileglob := fmt.Sprintf("cluster-resources/custom-resources/%s/*", cr.resource)
+		fileglob := fmt.Sprintf("%s/%s/%s/*", constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_CUSTOM_RESOURCES, cr.resource)
 		redactors = append(redactors, NewYamlRedactor(cr.yamlPath, fileglob, ""))
 
 		// redact kubectl last applied annotation once for each resource since it contains copies of
diff --git a/pkg/supportbundle/parse.go b/pkg/supportbundle/parse.go
index c5134ae9..e8843351 100644
--- a/pkg/supportbundle/parse.go
+++ b/pkg/supportbundle/parse.go
@@ -11,6 +11,7 @@ import (
 
 	"github.com/mholt/archiver/v3"
 	"github.com/pkg/errors"
+	"github.com/replicatedhq/troubleshoot/pkg/constants"
 	types "github.com/replicatedhq/troubleshoot/pkg/supportbundle/types"
 	corev1 "k8s.io/api/core/v1"
 )
@@ -20,15 +21,15 @@ var (
 )
 
 func getPodsFilePath(namespace string) string {
-	return filepath.Join("cluster-resources", "pods", fmt.Sprintf("%s.json", namespace))
+	return filepath.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_PODS, fmt.Sprintf("%s.json", namespace))
 }
 
 func getContainerLogsFilePath(namespace string, podName string, containerName string) string {
-	return filepath.Join("cluster-resources", "pods", "logs", namespace, podName, fmt.Sprintf("%s.log", containerName))
+	return filepath.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_PODS_LOGS, namespace, podName, fmt.Sprintf("%s.log", containerName))
 }
 
 func getEventsFilePath(namespace string) string {
-	return filepath.Join("cluster-resources", "events", fmt.Sprintf("%s.json", namespace))
+	return filepath.Join(constants.CLUSTER_RESOURCES_DIR, "events", fmt.Sprintf("%s.json", namespace))
 }
 
 func GetPodDetails(bundleArchive string, podNamespace string, podName string) (*types.PodDetails, error) {
diff --git a/schemas/analyzer-troubleshoot-v1beta2.json b/schemas/analyzer-troubleshoot-v1beta2.json
index 26c2fab3..87fa17ef 100644
--- a/schemas/analyzer-troubleshoot-v1beta2.json
+++ b/schemas/analyzer-troubleshoot-v1beta2.json
@@ -3512,6 +3512,82 @@
                     "oneOf": [{"type": "string"},{"type": "boolean"}]
                   }
                 }
+              },
+              "udpPortStatus": {
+                "type": "object",
+                "required": [
+                  "outcomes"
+                ],
+                "properties": {
+                  "annotations": {
+                    "type": "object",
+                    "additionalProperties": {
+                      "type": "string"
+                    }
+                  },
+                  "checkName": {
+                    "type": "string"
+                  },
+                  "collectorName": {
+                    "type": "string"
+                  },
+                  "exclude": {
+                    "oneOf": [{"type": "string"},{"type": "boolean"}]
+                  },
+                  "outcomes": {
+                    "type": "array",
+                    "items": {
+                      "type": "object",
+                      "properties": {
+                        "fail": {
+                          "type": "object",
+                          "properties": {
+                            "message": {
+                              "type": "string"
+                            },
+                            "uri": {
+                              "type": "string"
+                            },
+                            "when": {
+                              "type": "string"
+                            }
+                          }
+                        },
+                        "pass": {
+                          "type": "object",
+                          "properties": {
+                            "message": {
+                              "type": "string"
+                            },
+                            "uri": {
+                              "type": "string"
+                            },
+                            "when": {
+                              "type": "string"
+                            }
+                          }
+                        },
+                        "warn": {
+                          "type": "object",
+                          "properties": {
+                            "message": {
+                              "type": "string"
+                            },
+                            "uri": {
+                              "type": "string"
+                            },
+                            "when": {
+                              "type": "string"
+                            }
+                          }
+                        }
+                      }
+                    }
+                  },
+                  "strict": {
+                    "oneOf": [{"type": "string"},{"type": "boolean"}]
+                  }
+                }
               }
             }
           }
diff --git a/schemas/collector-troubleshoot-v1beta2.json b/schemas/collector-troubleshoot-v1beta2.json
index 09a44e0e..7bf2ae9e 100644
--- a/schemas/collector-troubleshoot-v1beta2.json
+++ b/schemas/collector-troubleshoot-v1beta2.json
@@ -2225,7 +2225,10 @@
                                       }
                                     }
                                   },
-                                  "x-kubernetes-list-type": "set"
+                                  "x-kubernetes-list-map-keys": [
+                                    "name"
+                                  ],
+                                  "x-kubernetes-list-type": "map"
                                 },
                                 "limits": {
                                   "description": "Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/",
@@ -3412,7 +3415,10 @@
                                       }
                                     }
                                   },
-                                  "x-kubernetes-list-type": "set"
+                                  "x-kubernetes-list-map-keys": [
+                                    "name"
+                                  ],
+                                  "x-kubernetes-list-type": "map"
                                 },
                                 "limits": {
                                   "description": "Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/",
@@ -4613,7 +4619,10 @@
                                       }
                                     }
                                   },
-                                  "x-kubernetes-list-type": "set"
+                                  "x-kubernetes-list-map-keys": [
+                                    "name"
+                                  ],
+                                  "x-kubernetes-list-type": "map"
                                 },
                                 "limits": {
                                   "description": "Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/",
@@ -5859,7 +5868,10 @@
                                                   }
                                                 }
                                               },
-                                              "x-kubernetes-list-type": "set"
+                                              "x-kubernetes-list-map-keys": [
+                                                "name"
+                                              ],
+                                              "x-kubernetes-list-type": "map"
                                             },
                                             "limits": {
                                               "description": "Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/",
diff --git a/schemas/preflight-troubleshoot-v1beta2.json b/schemas/preflight-troubleshoot-v1beta2.json
index ea94ecd7..d8a702e8 100644
--- a/schemas/preflight-troubleshoot-v1beta2.json
+++ b/schemas/preflight-troubleshoot-v1beta2.json
@@ -4369,7 +4369,10 @@
                                       }
                                     }
                                   },
-                                  "x-kubernetes-list-type": "set"
+                                  "x-kubernetes-list-map-keys": [
+                                    "name"
+                                  ],
+                                  "x-kubernetes-list-type": "map"
                                 },
                                 "limits": {
                                   "description": "Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/",
@@ -5556,7 +5559,10 @@
                                       }
                                     }
                                   },
-                                  "x-kubernetes-list-type": "set"
+                                  "x-kubernetes-list-map-keys": [
+                                    "name"
+                                  ],
+                                  "x-kubernetes-list-type": "map"
                                 },
                                 "limits": {
                                   "description": "Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/",
@@ -6757,7 +6763,10 @@
                                       }
                                     }
                                   },
-                                  "x-kubernetes-list-type": "set"
+                                  "x-kubernetes-list-map-keys": [
+                                    "name"
+                                  ],
+                                  "x-kubernetes-list-type": "map"
                                 },
                                 "limits": {
                                   "description": "Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/",
@@ -8003,7 +8012,10 @@
                                                   }
                                                 }
                                               },
-                                              "x-kubernetes-list-type": "set"
+                                              "x-kubernetes-list-map-keys": [
+                                                "name"
+                                              ],
+                                              "x-kubernetes-list-type": "map"
                                             },
                                             "limits": {
                                               "description": "Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/",
@@ -9318,6 +9330,26 @@
                     "oneOf": [{"type": "string"},{"type": "boolean"}]
                   }
                 }
+              },
+              "udpPortStatus": {
+                "type": "object",
+                "required": [
+                  "port"
+                ],
+                "properties": {
+                  "collectorName": {
+                    "type": "string"
+                  },
+                  "exclude": {
+                    "oneOf": [{"type": "string"},{"type": "boolean"}]
+                  },
+                  "interface": {
+                    "type": "string"
+                  },
+                  "port": {
+                    "type": "integer"
+                  }
+                }
               }
             }
           }
diff --git a/schemas/supportbundle-troubleshoot-v1beta2.json b/schemas/supportbundle-troubleshoot-v1beta2.json
index 4ea71088..43407b92 100644
--- a/schemas/supportbundle-troubleshoot-v1beta2.json
+++ b/schemas/supportbundle-troubleshoot-v1beta2.json
@@ -4415,7 +4415,10 @@
                                       }
                                     }
                                   },
-                                  "x-kubernetes-list-type": "set"
+                                  "x-kubernetes-list-map-keys": [
+                                    "name"
+                                  ],
+                                  "x-kubernetes-list-type": "map"
                                 },
                                 "limits": {
                                   "description": "Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/",
@@ -5602,7 +5605,10 @@
                                       }
                                     }
                                   },
-                                  "x-kubernetes-list-type": "set"
+                                  "x-kubernetes-list-map-keys": [
+                                    "name"
+                                  ],
+                                  "x-kubernetes-list-type": "map"
                                 },
                                 "limits": {
                                   "description": "Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/",
@@ -6803,7 +6809,10 @@
                                       }
                                     }
                                   },
-                                  "x-kubernetes-list-type": "set"
+                                  "x-kubernetes-list-map-keys": [
+                                    "name"
+                                  ],
+                                  "x-kubernetes-list-type": "map"
                                 },
                                 "limits": {
                                   "description": "Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/",
@@ -8049,7 +8058,10 @@
                                                   }
                                                 }
                                               },
-                                              "x-kubernetes-list-type": "set"
+                                              "x-kubernetes-list-map-keys": [
+                                                "name"
+                                              ],
+                                              "x-kubernetes-list-type": "map"
                                             },
                                             "limits": {
                                               "description": "Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/",
@@ -10301,6 +10313,82 @@
                     "oneOf": [{"type": "string"},{"type": "boolean"}]
                   }
                 }
+              },
+              "udpPortStatus": {
+                "type": "object",
+                "required": [
+                  "outcomes"
+                ],
+                "properties": {
+                  "annotations": {
+                    "type": "object",
+                    "additionalProperties": {
+                      "type": "string"
+                    }
+                  },
+                  "checkName": {
+                    "type": "string"
+                  },
+                  "collectorName": {
+                    "type": "string"
+                  },
+                  "exclude": {
+                    "oneOf": [{"type": "string"},{"type": "boolean"}]
+                  },
+                  "outcomes": {
+                    "type": "array",
+                    "items": {
+                      "type": "object",
+                      "properties": {
+                        "fail": {
+                          "type": "object",
+                          "properties": {
+                            "message": {
+                              "type": "string"
+                            },
+                            "uri": {
+                              "type": "string"
+                            },
+                            "when": {
+                              "type": "string"
+                            }
+                          }
+                        },
+                        "pass": {
+                          "type": "object",
+                          "properties": {
+                            "message": {
+                              "type": "string"
+                            },
+                            "uri": {
+                              "type": "string"
+                            },
+                            "when": {
+                              "type": "string"
+                            }
+                          }
+                        },
+                        "warn": {
+                          "type": "object",
+                          "properties": {
+                            "message": {
+                              "type": "string"
+                            },
+                            "uri": {
+                              "type": "string"
+                            },
+                            "when": {
+                              "type": "string"
+                            }
+                          }
+                        }
+                      }
+                    }
+                  },
+                  "strict": {
+                    "oneOf": [{"type": "string"},{"type": "boolean"}]
+                  }
+                }
               }
             }
           }
@@ -10809,6 +10897,26 @@
                     "oneOf": [{"type": "string"},{"type": "boolean"}]
                   }
                 }
+              },
+              "udpPortStatus": {
+                "type": "object",
+                "required": [
+                  "port"
+                ],
+                "properties": {
+                  "collectorName": {
+                    "type": "string"
+                  },
+                  "exclude": {
+                    "oneOf": [{"type": "string"},{"type": "boolean"}]
+                  },
+                  "interface": {
+                    "type": "string"
+                  },
+                  "port": {
+                    "type": "integer"
+                  }
+                }
               }
             }
           }


Test patch:
diff --git a/pkg/analyze/host_udpportstatus_test.go b/pkg/analyze/host_udpportstatus_test.go
new file mode 100644
index 00000000..bfc09794
--- /dev/null
+++ b/pkg/analyze/host_udpportstatus_test.go
@@ -0,0 +1,104 @@
+package analyzer
+
+import (
+	"encoding/json"
+	"testing"
+
+	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+	"github.com/replicatedhq/troubleshoot/pkg/collect"
+	"github.com/stretchr/testify/require"
+)
+
+func TestAnalyzeUDPPortStatus(t *testing.T) {
+	tt := []struct {
+		name      string
+		collected collect.NetworkStatusResult
+		analyzer  troubleshootv1beta2.UDPPortStatusAnalyze
+		results   []*AnalyzeResult
+		wantErr   bool
+	}{
+		{
+			name: "pass expect single result",
+			collected: collect.NetworkStatusResult{
+				Status: "connected",
+			},
+			analyzer: troubleshootv1beta2.UDPPortStatusAnalyze{
+				AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
+					CheckName: "Kubernetes API UDP Port Status",
+				},
+				Outcomes: []*troubleshootv1beta2.Outcome{
+					{
+						Pass: &troubleshootv1beta2.SingleOutcome{
+							When:    "connected",
+							Message: "Port 6443 is open",
+						},
+					},
+					{
+						Warn: &troubleshootv1beta2.SingleOutcome{
+							Message: "Unexpected port status",
+						},
+					},
+				},
+			},
+			results: []*AnalyzeResult{
+				{
+					Title:   "Kubernetes API UDP Port Status",
+					IsPass:  true,
+					Message: "Port 6443 is open",
+				},
+			},
+		},
+
+		{
+			name: "get warn if no match",
+			collected: collect.NetworkStatusResult{
+				Status: "connected",
+			},
+			analyzer: troubleshootv1beta2.UDPPortStatusAnalyze{
+				AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{
+					CheckName: "Kubernetes API UDP Port Status",
+				},
+				Outcomes: []*troubleshootv1beta2.Outcome{
+					{
+						Fail: &troubleshootv1beta2.SingleOutcome{
+							When:    "foo",
+							Message: "Port 6443 is open",
+						},
+					},
+					{
+						Warn: &troubleshootv1beta2.SingleOutcome{
+							Message: "Unexpected port status",
+						},
+					},
+				},
+			},
+			results: []*AnalyzeResult{
+				{
+					Title:   "Kubernetes API UDP Port Status",
+					IsWarn:  true,
+					Message: "Unexpected port status",
+				},
+			},
+		},
+	}
+
+	for _, tc := range tt {
+		t.Run(tc.name, func(t *testing.T) {
+			fn := func(_ string) ([]byte, error) {
+				return json.Marshal(&tc.collected)
+			}
+			analyzer := AnalyzeHostUDPPortStatus{
+				hostAnalyzer: &tc.analyzer,
+			}
+			results, err := analyzer.Analyze(fn)
+			if tc.wantErr {
+				require.NotNil(t, err)
+				return
+			}
+
+			require.Nil(t, err)
+			require.Equal(t, tc.results, results)
+		})
+	}
+
+}
diff --git a/pkg/collect/host_udpportstatus_test.go b/pkg/collect/host_udpportstatus_test.go
new file mode 100644
index 00000000..569cbfe0
--- /dev/null
+++ b/pkg/collect/host_udpportstatus_test.go
@@ -0,0 +1,94 @@
+package collect
+
+import (
+	"net"
+	"os"
+	"strconv"
+	"testing"
+
+	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestCollectHostUDPPortStatus_Collect(t *testing.T) {
+	getPort := func() (int, *net.UDPConn, error) {
+		listenAddress := net.UDPAddr{
+			IP: net.ParseIP("0.0.0.0"),
+		}
+		conn, err := net.ListenUDP("udp", &listenAddress)
+		if err != nil {
+			return 0, nil, err
+		}
+
+		_, p, err := net.SplitHostPort(conn.LocalAddr().String())
+		if err != nil {
+			return 0, nil, err
+		}
+		port, err := strconv.Atoi(p)
+		return port, conn, err
+	}
+
+	type fields struct {
+		hostCollector *troubleshootv1beta2.UDPPortStatus
+	}
+	tests := []struct {
+		name    string
+		getPort func(t *testing.T) (port int, closeFn func() error)
+		fields  fields
+		want    map[string][]byte
+	}{
+		{
+			name: "connected",
+			getPort: func(t *testing.T) (int, func() error) {
+				port, conn, err := getPort()
+				require.NoError(t, err)
+				conn.Close()
+				return port, nil
+			},
+			want: map[string][]byte{
+				"host-collectors/udpPortStatus/udpPortStatus.json": []byte(`{"status":"connected","message":""}`),
+			},
+		},
+		{
+			name: "address-in-use",
+			getPort: func(t *testing.T) (int, func() error) {
+				port, conn, err := getPort()
+				require.NoError(t, err)
+				return port, conn.Close
+			},
+			want: map[string][]byte{
+				"host-collectors/udpPortStatus/udpPortStatus.json": []byte(`{"status":"address-in-use","message":""}`),
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			port, closeFn := tt.getPort(t)
+			if closeFn != nil {
+				defer closeFn()
+			}
+
+			tmpDir, err := os.MkdirTemp("", "bundle")
+			require.NoError(t, err)
+
+			c := &CollectHostUDPPortStatus{
+				hostCollector: &troubleshootv1beta2.UDPPortStatus{
+					Port: port,
+				},
+				BundlePath: tmpDir,
+			}
+
+			progresChan := make(chan interface{})
+			defer close(progresChan)
+			go func() {
+				for range progresChan {
+				}
+			}()
+			got, err := c.Collect(progresChan)
+			require.NoError(t, err)
+
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}