Repository: replicatedhq/troubleshoot
Pull request #981 (2023-01-24 21:15:21)

feat(hostpreflights): udp port status
## Description, Motivation and Context

Adds a collector to check if a UDP port is in use.

Usage example is:

```
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
```

https://github.com/replicatedhq/troubleshoot.sh/pull/466

<!--- If it relates to an open issue, please link to the issue here.
e.g.
Fixes: #414
-->

## Checklist

- [x] New and existing tests pass locally with introduced changes.
- [x] Tests for the changes have been added (for bug fixes / features)
- [x] The commit message(s) are informative and highlight any breaking changes
- [x] 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. -->


emosbaugh: [sc-61600]

Patch:
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_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 c5ad6d7c..e81f11d9 100644
--- a/config/crds/troubleshoot.sh_preflights.yaml
+++ b/config/crds/troubleshoot.sh_preflights.yaml
@@ -10162,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 a8f05e46..7f3250d5 100644
--- a/config/crds/troubleshoot.sh_supportbundles.yaml
+++ b/config/crds/troubleshoot.sh_supportbundles.yaml
@@ -10763,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:
@@ -11128,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/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/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/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/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/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/preflight-troubleshoot-v1beta2.json b/schemas/preflight-troubleshoot-v1beta2.json
index d5c174c8..d8a702e8 100644
--- a/schemas/preflight-troubleshoot-v1beta2.json
+++ b/schemas/preflight-troubleshoot-v1beta2.json
@@ -9330,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 8e5c1d0a..43407b92 100644
--- a/schemas/supportbundle-troubleshoot-v1beta2.json
+++ b/schemas/supportbundle-troubleshoot-v1beta2.json
@@ -10313,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"}]
+                  }
+                }
               }
             }
           }
@@ -10821,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)
+		})
+	}
+}