mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-04-15 07:16:34 +00:00
Merge pull request #314 from areed/host-preflight-urls
Host HTTP request analyzer
This commit is contained in:
22
examples/preflight/host-http.yaml
Normal file
22
examples/preflight/host-http.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
apiVersion: troubleshoot.sh/v1beta2
|
||||
kind: HostPreflight
|
||||
metadata:
|
||||
name: http
|
||||
spec:
|
||||
collectors:
|
||||
- http:
|
||||
collectorName: registry
|
||||
get:
|
||||
url: https://registry.replicated.com
|
||||
analyzers:
|
||||
- http:
|
||||
collectorName: registry
|
||||
outcomes:
|
||||
- fail:
|
||||
when: "error"
|
||||
message: Error connecting to registry
|
||||
- pass:
|
||||
when: "statusCode == 404"
|
||||
message: Connected to registry
|
||||
- fail:
|
||||
message: "Unexpected response"
|
||||
@@ -76,6 +76,13 @@ func HostAnalyze(hostAnalyzer *troubleshootv1beta2.HostAnalyze, getFile getColle
|
||||
}
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
if hostAnalyzer.HTTP != nil {
|
||||
result, err := analyzeHostHTTP(hostAnalyzer.HTTP, getFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []*AnalyzeResult{result}, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("invalid analyzer")
|
||||
}
|
||||
|
||||
140
pkg/analyze/host_http.go
Normal file
140
pkg/analyze/host_http.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
)
|
||||
|
||||
type httpResult struct {
|
||||
Error *collect.HTTPError
|
||||
Response *collect.HTTPResponse
|
||||
}
|
||||
|
||||
func analyzeHostHTTP(hostAnalyzer *troubleshootv1beta2.HTTPAnalyze, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
|
||||
name := filepath.Join("http", "result.json")
|
||||
if hostAnalyzer.CollectorName != "" {
|
||||
name = filepath.Join("http", hostAnalyzer.CollectorName+".json")
|
||||
}
|
||||
contents, err := getCollectedFileContents(name)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get collected file")
|
||||
}
|
||||
|
||||
httpInfo := &httpResult{}
|
||||
if err := json.Unmarshal(contents, httpInfo); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal http result")
|
||||
}
|
||||
|
||||
result := AnalyzeResult{}
|
||||
|
||||
title := hostAnalyzer.CheckName
|
||||
if title == "" {
|
||||
title = "HTTP Request"
|
||||
}
|
||||
result.Title = 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 &result, nil
|
||||
}
|
||||
|
||||
isMatch, err := compareHostHTTPConditionalToActual(outcome.Fail.When, httpInfo)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to compare %s", outcome.Fail.When)
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsFail = true
|
||||
result.Message = outcome.Fail.Message
|
||||
result.URI = outcome.Fail.URI
|
||||
|
||||
return &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 &result, nil
|
||||
}
|
||||
|
||||
isMatch, err := compareHostHTTPConditionalToActual(outcome.Warn.When, httpInfo)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to compare %s", outcome.Warn.When)
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsWarn = true
|
||||
result.Message = outcome.Warn.Message
|
||||
result.URI = outcome.Warn.URI
|
||||
|
||||
return &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 &result, nil
|
||||
}
|
||||
|
||||
isMatch, err := compareHostHTTPConditionalToActual(outcome.Pass.When, httpInfo)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to compare %s", outcome.Pass.When)
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsPass = true
|
||||
result.Message = outcome.Pass.Message
|
||||
result.URI = outcome.Pass.URI
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func compareHostHTTPConditionalToActual(conditional string, result *httpResult) (res bool, err error) {
|
||||
if conditional == "error" {
|
||||
return result.Error != nil, nil
|
||||
}
|
||||
|
||||
parts := strings.Split(conditional, " ")
|
||||
if len(parts) != 3 {
|
||||
return false, fmt.Errorf("Failed to parse conditional: got %d parts", len(parts))
|
||||
}
|
||||
|
||||
if parts[0] != "statusCode" {
|
||||
return false, errors.New(`Conditional must begin with keyword "statusCode"`)
|
||||
}
|
||||
|
||||
if parts[1] != "=" && parts[1] != "==" && parts[1] != "===" {
|
||||
return false, errors.New(`Only supported operator is "=="`)
|
||||
}
|
||||
|
||||
i, err := strconv.Atoi(parts[2])
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if result.Response == nil {
|
||||
return false, err
|
||||
}
|
||||
return result.Response.Status == i, nil
|
||||
}
|
||||
104
pkg/analyze/host_http_test.go
Normal file
104
pkg/analyze/host_http_test.go
Normal file
@@ -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/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAnalyzeHostHTTP(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
httpResult *httpResult
|
||||
hostAnalyzer *troubleshootv1beta2.HTTPAnalyze
|
||||
result *AnalyzeResult
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "error",
|
||||
httpResult: &httpResult{
|
||||
Error: &collect.HTTPError{
|
||||
Message: "i/o timeout",
|
||||
},
|
||||
},
|
||||
hostAnalyzer: &troubleshootv1beta2.HTTPAnalyze{
|
||||
CollectorName: "registry",
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "error",
|
||||
Message: "Failed to reach replicated.registry.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
result: &AnalyzeResult{
|
||||
Title: "HTTP Request",
|
||||
IsFail: true,
|
||||
Message: "Failed to reach replicated.registry.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "200",
|
||||
httpResult: &httpResult{
|
||||
Response: &collect.HTTPResponse{
|
||||
Status: 200,
|
||||
},
|
||||
},
|
||||
hostAnalyzer: &troubleshootv1beta2.HTTPAnalyze{
|
||||
CollectorName: "registry",
|
||||
Outcomes: []*troubleshootv1beta2.Outcome{
|
||||
{
|
||||
Fail: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "error",
|
||||
Message: "Failed to reach replicated.registry.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
Warn: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "statusCode == 204",
|
||||
Message: "No content",
|
||||
},
|
||||
},
|
||||
{
|
||||
Pass: &troubleshootv1beta2.SingleOutcome{
|
||||
When: "statusCode == 200",
|
||||
Message: "Successfully reached registry",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
result: &AnalyzeResult{
|
||||
Title: "HTTP Request",
|
||||
IsPass: true,
|
||||
Message: "Successfully reached registry",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
req := require.New(t)
|
||||
b, err := json.Marshal(test.httpResult)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
getCollectedFileContents := func(filename string) ([]byte, error) {
|
||||
return b, nil
|
||||
}
|
||||
|
||||
result, err := analyzeHostHTTP(test.hostAnalyzer, getCollectedFileContents)
|
||||
if test.expectErr {
|
||||
req.Error(err)
|
||||
} else {
|
||||
req.NoError(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, test.result, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,12 @@ type DiskUsageAnalyze struct {
|
||||
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
|
||||
}
|
||||
|
||||
type HTTPAnalyze struct {
|
||||
AnalyzeMeta `json:",inline" yaml:",inline"`
|
||||
CollectorName string `json:"collectorName,omitempty" yaml:"collectorName,omitempty"`
|
||||
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
|
||||
}
|
||||
|
||||
type HostAnalyze struct {
|
||||
CPU *CPUAnalyze `json:"cpu,omitempty" yaml:"cpu,omitempty"`
|
||||
//
|
||||
@@ -38,4 +44,6 @@ type HostAnalyze struct {
|
||||
Memory *MemoryAnalyze `json:"memory,omitempty" yaml:"memory,omitempty"`
|
||||
|
||||
TCPPortStatus *TCPPortStatusAnalyze `json:"tcpPortStatus,omitempty" yaml:"tcpPortStatus,omitempty"`
|
||||
|
||||
HTTP *HTTPAnalyze `json:"http" yaml:"http"`
|
||||
}
|
||||
|
||||
@@ -52,6 +52,13 @@ type DiskUsage struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
type HostHTTP struct {
|
||||
HostCollectorMeta `json:",inline" yaml:",inline"`
|
||||
Get *Get `json:"get,omitempty" yaml:"get,omitempty"`
|
||||
Post *Post `json:"post,omitempty" yaml:"post,omitempty"`
|
||||
Put *Put `json:"put,omitempty" yaml:"put,omitempty"`
|
||||
}
|
||||
|
||||
type HostCollect struct {
|
||||
CPU *CPU `json:"cpu,omitempty" yaml:"cpu,omitempty"`
|
||||
Memory *Memory `json:"memory,omitempty" yaml:"memory,omitempty"`
|
||||
@@ -61,6 +68,7 @@ type HostCollect struct {
|
||||
Kubernetes *Kubernetes `json:"kubernetes,omitempty" yaml:"kubernetes,omitempty"`
|
||||
IPV4Interfaces *IPV4Interfaces `json:"ipv4Interfaces,omitempty" yaml:"ipv4Interfaces,omitempty"`
|
||||
DiskUsage *DiskUsage `json:"diskUsage,omitempty" yaml:"diskUsage,omitempty"`
|
||||
HTTP *HostHTTP `json:"http,omitempty" yaml:"http,omitempty"`
|
||||
}
|
||||
|
||||
func (c *HostCollect) GetName() string {
|
||||
|
||||
@@ -1027,6 +1027,33 @@ func (in *HTTP) DeepCopy() *HTTP {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *HTTPAnalyze) DeepCopyInto(out *HTTPAnalyze) {
|
||||
*out = *in
|
||||
out.AnalyzeMeta = in.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 HTTPAnalyze.
|
||||
func (in *HTTPAnalyze) DeepCopy() *HTTPAnalyze {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(HTTPAnalyze)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *HTTPLoadBalancer) DeepCopyInto(out *HTTPLoadBalancer) {
|
||||
*out = *in
|
||||
@@ -1071,6 +1098,11 @@ func (in *HostAnalyze) DeepCopyInto(out *HostAnalyze) {
|
||||
*out = new(TCPPortStatusAnalyze)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.HTTP != nil {
|
||||
in, out := &in.HTTP, &out.HTTP
|
||||
*out = new(HTTPAnalyze)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostAnalyze.
|
||||
@@ -1126,6 +1158,11 @@ func (in *HostCollect) DeepCopyInto(out *HostCollect) {
|
||||
*out = new(DiskUsage)
|
||||
**out = **in
|
||||
}
|
||||
if in.HTTP != nil {
|
||||
in, out := &in.HTTP, &out.HTTP
|
||||
*out = new(HostHTTP)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostCollect.
|
||||
@@ -1154,6 +1191,37 @@ func (in *HostCollectorMeta) DeepCopy() *HostCollectorMeta {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *HostHTTP) DeepCopyInto(out *HostHTTP) {
|
||||
*out = *in
|
||||
out.HostCollectorMeta = in.HostCollectorMeta
|
||||
if in.Get != nil {
|
||||
in, out := &in.Get, &out.Get
|
||||
*out = new(Get)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Post != nil {
|
||||
in, out := &in.Post, &out.Post
|
||||
*out = new(Post)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Put != nil {
|
||||
in, out := &in.Put, &out.Put
|
||||
*out = new(Put)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostHTTP.
|
||||
func (in *HostHTTP) DeepCopy() *HostHTTP {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(HostHTTP)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *HostPreflight) DeepCopyInto(out *HostPreflight) {
|
||||
*out = *in
|
||||
|
||||
@@ -28,6 +28,8 @@ func (c *HostCollector) RunCollectorSync() (result map[string][]byte, err error)
|
||||
result, err = HostDiskUsage(c)
|
||||
} else if c.Collect.TCPPortStatus != nil {
|
||||
result, err = HostTCPPortStatus(c)
|
||||
} else if c.Collect.HTTP != nil {
|
||||
result, err = HostHTTP(c)
|
||||
} else {
|
||||
err = errors.New("no spec found to run")
|
||||
return
|
||||
|
||||
39
pkg/collect/host_http.go
Normal file
39
pkg/collect/host_http.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package collect
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func HostHTTP(c *HostCollector) (map[string][]byte, error) {
|
||||
httpCollector := c.Collect.HTTP
|
||||
var response *http.Response
|
||||
var err error
|
||||
|
||||
if httpCollector.Get != nil {
|
||||
response, err = doGet(httpCollector.Get)
|
||||
} else if httpCollector.Post != nil {
|
||||
response, err = doPost(httpCollector.Post)
|
||||
} else if httpCollector.Put != nil {
|
||||
response, err = doPut(httpCollector.Put)
|
||||
} else {
|
||||
return nil, errors.New("no supported http request type")
|
||||
}
|
||||
|
||||
output, err := responseToOutput(response, err, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fileName := "result.json"
|
||||
if httpCollector.CollectorName != "" {
|
||||
fileName = httpCollector.CollectorName + ".json"
|
||||
}
|
||||
httpOutput := map[string][]byte{
|
||||
filepath.Join("http", fileName): output,
|
||||
}
|
||||
|
||||
return httpOutput, nil
|
||||
}
|
||||
@@ -12,13 +12,13 @@ import (
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
)
|
||||
|
||||
type httpResponse struct {
|
||||
type HTTPResponse struct {
|
||||
Status int `json:"status"`
|
||||
Body string `json:"body"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
}
|
||||
|
||||
type httpError struct {
|
||||
type HTTPError struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ func doPut(put *troubleshootv1beta2.Put) (*http.Response, error) {
|
||||
func responseToOutput(response *http.Response, err error, doRedact bool) ([]byte, error) {
|
||||
output := make(map[string]interface{})
|
||||
if err != nil {
|
||||
output["error"] = httpError{
|
||||
output["error"] = HTTPError{
|
||||
Message: err.Error(),
|
||||
}
|
||||
} else {
|
||||
@@ -120,7 +120,7 @@ func responseToOutput(response *http.Response, err error, doRedact bool) ([]byte
|
||||
headers[k] = strings.Join(v, ",")
|
||||
}
|
||||
|
||||
output["response"] = httpResponse{
|
||||
output["response"] = HTTPResponse{
|
||||
Status: response.StatusCode,
|
||||
Body: string(body),
|
||||
Headers: headers,
|
||||
|
||||
Reference in New Issue
Block a user