Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 7 commits into from
Nov 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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