Skip to content

Commit

Permalink
feat(webmetric): Support POST/PUT content with web metrics. Fixes #371 (
Browse files Browse the repository at this point in the history
#1573)

feat(webmetric): Support POST/PUT content with web metrics. Fixes #371 (#1573)

Signed-off-by: Noam Gal <[email protected]>
  • Loading branch information
ATGardner authored Nov 1, 2021
1 parent b6f0182 commit fe87bdd
Show file tree
Hide file tree
Showing 9 changed files with 206 additions and 14 deletions.
25 changes: 24 additions & 1 deletion docs/analysis/web.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Web Metrics

A HTTP request can be performed against some external service to obtain the measurement. This example
An HTTP request can be performed against some external service to obtain the measurement. This example
makes a HTTP GET request to some URL. The webhook response must return JSON content. The result of
the optional `jsonPath` expression will be assigned to the `result` variable that can be referenced
in the `successCondition` and `failureCondition` expressions. If omitted, will use the entire body
Expand Down Expand Up @@ -49,3 +49,26 @@ NOTE: if the result is a string, two convenience functions `asInt` and `asFloat`
to convert a result value to a numeric type so that mathematical comparison operators can be used
(e.g. >, <, >=, <=).

### Optional web methods
It is possible to use a POST or PUT requests, by specifying the `method` and `body` fields

```yaml
metrics:
- name: webmetric
successCondition: result == true
provider:
web:
method: POST # valid values are GET|POST|PUT, defaults to GET
url: "http://my-server.com/api/v1/measurement?service={{ args.service-name }}"
timeoutSeconds: 20 # defaults to 10 seconds
headers:
- key: Authorization
value: "Bearer {{ args.api-token }}"
- key: Content-Type # if body is a json, it is recommended to set the Content-Type
value: "application/json"
body: "{\"key\": \"string value\"}"
jsonPath: "{$.data.ok}"
```
!!! tip
In order to send in JSON, you have to encode it yourself, and send the correct Content-Type as well.
Setting a `body` field for a `GET` request will result in an error.
4 changes: 4 additions & 0 deletions manifests/crds/analysis-run-crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2447,6 +2447,8 @@ spec:
type: object
web:
properties:
body:
type: string
headers:
items:
properties:
Expand All @@ -2463,6 +2465,8 @@ spec:
type: boolean
jsonPath:
type: string
method:
type: string
timeoutSeconds:
format: int64
type: integer
Expand Down
4 changes: 4 additions & 0 deletions manifests/crds/analysis-template-crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2442,6 +2442,8 @@ spec:
type: object
web:
properties:
body:
type: string
headers:
items:
properties:
Expand All @@ -2458,6 +2460,8 @@ spec:
type: boolean
jsonPath:
type: string
method:
type: string
timeoutSeconds:
format: int64
type: integer
Expand Down
4 changes: 4 additions & 0 deletions manifests/crds/cluster-analysis-template-crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2442,6 +2442,8 @@ spec:
type: object
web:
properties:
body:
type: string
headers:
items:
properties:
Expand All @@ -2458,6 +2460,8 @@ spec:
type: boolean
jsonPath:
type: string
method:
type: string
timeoutSeconds:
format: int64
type: integer
Expand Down
12 changes: 12 additions & 0 deletions manifests/install.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2448,6 +2448,8 @@ spec:
type: object
web:
properties:
body:
type: string
headers:
items:
properties:
Expand All @@ -2464,6 +2466,8 @@ spec:
type: boolean
jsonPath:
type: string
method:
type: string
timeoutSeconds:
format: int64
type: integer
Expand Down Expand Up @@ -5006,6 +5010,8 @@ spec:
type: object
web:
properties:
body:
type: string
headers:
items:
properties:
Expand All @@ -5022,6 +5028,8 @@ spec:
type: boolean
jsonPath:
type: string
method:
type: string
timeoutSeconds:
format: int64
type: integer
Expand Down Expand Up @@ -7491,6 +7499,8 @@ spec:
type: object
web:
properties:
body:
type: string
headers:
items:
properties:
Expand All @@ -7507,6 +7517,8 @@ spec:
type: boolean
jsonPath:
type: string
method:
type: string
timeoutSeconds:
format: int64
type: integer
Expand Down
12 changes: 12 additions & 0 deletions manifests/namespace-install.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2448,6 +2448,8 @@ spec:
type: object
web:
properties:
body:
type: string
headers:
items:
properties:
Expand All @@ -2464,6 +2466,8 @@ spec:
type: boolean
jsonPath:
type: string
method:
type: string
timeoutSeconds:
format: int64
type: integer
Expand Down Expand Up @@ -5006,6 +5010,8 @@ spec:
type: object
web:
properties:
body:
type: string
headers:
items:
properties:
Expand All @@ -5022,6 +5028,8 @@ spec:
type: boolean
jsonPath:
type: string
method:
type: string
timeoutSeconds:
format: int64
type: integer
Expand Down Expand Up @@ -7491,6 +7499,8 @@ spec:
type: object
web:
properties:
body:
type: string
headers:
items:
properties:
Expand All @@ -7507,6 +7517,8 @@ spec:
type: boolean
jsonPath:
type: string
method:
type: string
timeoutSeconds:
format: int64
type: integer
Expand Down
26 changes: 19 additions & 7 deletions metricproviders/webmetric/webmetric.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"reflect"
"strings"
"time"

metricutil "github.com/argoproj/argo-rollouts/utils/metric"
Expand Down Expand Up @@ -46,18 +47,29 @@ func (p *Provider) Run(run *v1alpha1.AnalysisRun, metric v1alpha1.Metric) v1alph
StartedAt: &startTime,
}

// Create request
request := &http.Request{
Method: "GET", // TODO maybe make this configurable....also implies we will need body templates
method := v1alpha1.WebMetricMethodGet
if metric.Provider.Web.Method != "" {
method = metric.Provider.Web.Method
}

url, err := url.Parse(metric.Provider.Web.URL)
url := metric.Provider.Web.URL

var body io.Reader

if metric.Provider.Web.Body != "" {
if method == v1alpha1.WebMetricMethodGet {
return metricutil.MarkMeasurementError(measurement, fmt.Errorf("Body can only be used with POST or PUT WebMetric Method types"))
}

body = strings.NewReader(metric.Provider.Web.Body)
}

// Create request
request, err := http.NewRequest(string(method), url, body)
if err != nil {
return metricutil.MarkMeasurementError(measurement, err)
}

request.URL = url

request.Header = make(http.Header)

for _, header := range metric.Provider.Web.Headers {
Expand Down
109 changes: 108 additions & 1 deletion metricproviders/webmetric/webmetric_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package webmetric

import (
"bytes"
"io"
"net/http"
"net/http/httptest"
Expand All @@ -17,6 +18,8 @@ func TestRunSuite(t *testing.T) {
webServerStatus int
webServerResponse string
metric v1alpha1.Metric
expectedMethod string
expectedBody string
expectedValue string
expectedPhase v1alpha1.AnalysisPhase
expectedErrorMessage string
Expand Down Expand Up @@ -433,7 +436,6 @@ func TestRunSuite(t *testing.T) {
expectedPhase: v1alpha1.AnalysisPhaseError,
expectedErrorMessage: "Could not find JSONPath in body",
},

// When_200Response_And_NilBody_Then_Succeed
{
webServerStatus: 200,
Expand Down Expand Up @@ -477,13 +479,118 @@ func TestRunSuite(t *testing.T) {
expectedPhase: v1alpha1.AnalysisPhaseError,
expectedErrorMessage: "",
},
// When_methodEmpty_Then_server_gets_GET
{
webServerStatus: 200,
webServerResponse: `{"a": 1, "b": true, "c": [1, 2, 3, 4], "d": null}`,
metric: v1alpha1.Metric{
Name: "foo",
SuccessCondition: "result.a > 0 && result.b && all(result.c, {# < 5}) && result.d == nil",
Provider: v1alpha1.MetricProvider{
Web: &v1alpha1.WebMetric{
// URL: server.URL,
Headers: []v1alpha1.WebMetricHeader{{Key: "key", Value: "value"}},
},
},
},
expectedMethod: "GET",
expectedValue: `{"a":1,"b":true,"c":[1,2,3,4],"d":null}`,
expectedPhase: v1alpha1.AnalysisPhaseSuccessful,
},
// When_methodGET_Then_server_gets_GET
{
webServerStatus: 200,
webServerResponse: `{"a": 1, "b": true, "c": [1, 2, 3, 4], "d": null}`,
metric: v1alpha1.Metric{
Name: "foo",
SuccessCondition: "result.a > 0 && result.b && all(result.c, {# < 5}) && result.d == nil",
Provider: v1alpha1.MetricProvider{
Web: &v1alpha1.WebMetric{
Method: v1alpha1.WebMetricMethodGet,
// URL: server.URL,
Headers: []v1alpha1.WebMetricHeader{{Key: "key", Value: "value"}},
},
},
},
expectedMethod: "GET",
expectedValue: `{"a":1,"b":true,"c":[1,2,3,4],"d":null}`,
expectedPhase: v1alpha1.AnalysisPhaseSuccessful,
},
// When_methodPOST_Then_server_gets_body
{
webServerStatus: 200,
webServerResponse: `{"a": 1, "b": true, "c": [1, 2, 3, 4], "d": null}`,
metric: v1alpha1.Metric{
Name: "foo",
SuccessCondition: "result.a > 0 && result.b && all(result.c, {# < 5}) && result.d == nil",
Provider: v1alpha1.MetricProvider{
Web: &v1alpha1.WebMetric{
Method: v1alpha1.WebMetricMethodPost,
// URL: server.URL,
Headers: []v1alpha1.WebMetricHeader{{Key: "key", Value: "value"}},
Body: "some body",
},
},
},
expectedMethod: "POST",
expectedBody: "some body",
expectedValue: `{"a":1,"b":true,"c":[1,2,3,4],"d":null}`,
expectedPhase: v1alpha1.AnalysisPhaseSuccessful,
},
// When_methodPUT_Then_server_gets_body
{
webServerStatus: 200,
webServerResponse: `{"a": 1, "b": true, "c": [1, 2, 3, 4], "d": null}`,
metric: v1alpha1.Metric{
Name: "foo",
SuccessCondition: "result.a > 0 && result.b && all(result.c, {# < 5}) && result.d == nil",
Provider: v1alpha1.MetricProvider{
Web: &v1alpha1.WebMetric{
Method: v1alpha1.WebMetricMethodPut,
// URL: server.URL,
Headers: []v1alpha1.WebMetricHeader{{Key: "key", Value: "value"}},
Body: "some body",
},
},
},
expectedMethod: "PUT",
expectedBody: "some body",
expectedValue: `{"a":1,"b":true,"c":[1,2,3,4],"d":null}`,
expectedPhase: v1alpha1.AnalysisPhaseSuccessful,
},
// When_sendingBodyWithGet_Then_Failure
{
metric: v1alpha1.Metric{
Name: "foo",
SuccessCondition: "result.a > 0 && result.b && all(result.c, {# < 5}) && result.d == nil",
Provider: v1alpha1.MetricProvider{
Web: &v1alpha1.WebMetric{
// URL: server.URL,
Headers: []v1alpha1.WebMetricHeader{{Key: "key", Value: "value"}},
Body: "some body",
},
},
},
expectedValue: "Body can only be used with POST or PUT WebMetric Method types",
expectedPhase: v1alpha1.AnalysisPhaseError,
},
}

// Run

for _, test := range tests {
// Server setup with response
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if test.expectedMethod != "" {
assert.Equal(t, test.expectedMethod, req.Method)
}

if test.expectedBody != "" {
buf := new(bytes.Buffer)
buf.ReadFrom(req.Body)
assert.Equal(t, test.expectedBody, buf.String())
}

if test.webServerStatus < 200 || test.webServerStatus >= 300 {
http.Error(rw, http.StatusText(test.webServerStatus), test.webServerStatus)
} else {
Expand Down
Loading

0 comments on commit fe87bdd

Please sign in to comment.