From 6c3fcfe5e4efe77d4d819fa3631032126c6c72b2 Mon Sep 17 00:00:00 2001 From: stephen-harris <54176138+stephen-harris@users.noreply.github.com> Date: Sat, 10 Oct 2020 00:02:13 +0100 Subject: [PATCH] feat(analysis): Add Datadog metric provider. Fixes #702 (#705) --- docs/features/analysis.md | 39 +++ manifests/crds/analysis-run-crd.yaml | 9 + manifests/crds/analysis-template-crd.yaml | 9 + .../crds/cluster-analysis-template-crd.yaml | 9 + manifests/install.yaml | 33 +++ manifests/namespace-install.yaml | 33 +++ metricproviders/datadog/datadog.go | 211 +++++++++++++++ metricproviders/datadog/datadog_test.go | 243 ++++++++++++++++++ metricproviders/metricproviders.go | 5 + pkg/apis/rollouts/v1alpha1/analysis_types.go | 7 + .../rollouts/v1alpha1/openapi_generated.go | 33 ++- .../v1alpha1/zz_generated.deepcopy.go | 21 ++ utils/analysis/factory.go | 3 + utils/analysis/factory_test.go | 1 + 14 files changed, 655 insertions(+), 1 deletion(-) create mode 100644 metricproviders/datadog/datadog.go create mode 100644 metricproviders/datadog/datadog_test.go diff --git a/docs/features/analysis.md b/docs/features/analysis.md index 7dc60134ac..0957565414 100644 --- a/docs/features/analysis.md +++ b/docs/features/analysis.md @@ -881,3 +881,42 @@ was greater than `0.90` NOTE: if the result is a string, two convenience functions `asInt` and `asFloat` are provided to convert a result value to a numeric type so that mathematical comparison operators can be used (e.g. >, <, >=, <=). + +## Datadog Metrics + +A [Datadog](https://www.datadoghq.com/) query can be used to obtain measurements for analysis. + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: AnalysisTemplate +metadata: + name: loq-error-rate +spec: + args: + - name: service-name + metrics: + - name: error-rate + interval: 5m + successCondition: result <= 0.01 + failureLimit: 3 + provider: + datadog: + interval: 5m + query: | + sum:requests.error.count{service:{{args.service-name}}} / + sum:requests.request.count{service:{{args.service-name}}} +``` + +Datadog api and app tokens can be configured in a kubernetes secret in argo-rollouts namespace. + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: datadog +type: Opaque +data: + address: https://api.datadoghq.com + api-key: + app-key: +``` diff --git a/manifests/crds/analysis-run-crd.yaml b/manifests/crds/analysis-run-crd.yaml index 1a32d700e6..741f15b66a 100644 --- a/manifests/crds/analysis-run-crd.yaml +++ b/manifests/crds/analysis-run-crd.yaml @@ -80,6 +80,15 @@ spec: type: string provider: properties: + datadog: + properties: + interval: + type: string + query: + type: string + required: + - query + type: object job: properties: metadata: diff --git a/manifests/crds/analysis-template-crd.yaml b/manifests/crds/analysis-template-crd.yaml index 3327265d86..bbfeec8f36 100644 --- a/manifests/crds/analysis-template-crd.yaml +++ b/manifests/crds/analysis-template-crd.yaml @@ -74,6 +74,15 @@ spec: type: string provider: properties: + datadog: + properties: + interval: + type: string + query: + type: string + required: + - query + type: object job: properties: metadata: diff --git a/manifests/crds/cluster-analysis-template-crd.yaml b/manifests/crds/cluster-analysis-template-crd.yaml index 855b75d170..f4b03e99f4 100644 --- a/manifests/crds/cluster-analysis-template-crd.yaml +++ b/manifests/crds/cluster-analysis-template-crd.yaml @@ -74,6 +74,15 @@ spec: type: string provider: properties: + datadog: + properties: + interval: + type: string + query: + type: string + required: + - query + type: object job: properties: metadata: diff --git a/manifests/install.yaml b/manifests/install.yaml index a7bff4dfc0..3f16f18050 100644 --- a/manifests/install.yaml +++ b/manifests/install.yaml @@ -81,6 +81,17 @@ spec: type: string provider: properties: + datadog: + properties: + address: + type: string + interval: + type: string + query: + type: string + required: + - query + type: object job: properties: metadata: @@ -2888,6 +2899,17 @@ spec: type: string provider: properties: + datadog: + properties: + address: + type: string + interval: + type: string + query: + type: string + required: + - query + type: object job: properties: metadata: @@ -5623,6 +5645,17 @@ spec: type: string provider: properties: + datadog: + properties: + address: + type: string + interval: + type: string + query: + type: string + required: + - query + type: object job: properties: metadata: diff --git a/manifests/namespace-install.yaml b/manifests/namespace-install.yaml index 3a9ec57e0b..6465936647 100644 --- a/manifests/namespace-install.yaml +++ b/manifests/namespace-install.yaml @@ -81,6 +81,17 @@ spec: type: string provider: properties: + datadog: + properties: + address: + type: string + interval: + type: string + query: + type: string + required: + - query + type: object job: properties: metadata: @@ -2888,6 +2899,17 @@ spec: type: string provider: properties: + datadog: + properties: + address: + type: string + interval: + type: string + query: + type: string + required: + - query + type: object job: properties: metadata: @@ -5623,6 +5645,17 @@ spec: type: string provider: properties: + datadog: + properties: + address: + type: string + interval: + type: string + query: + type: string + required: + - query + type: object job: properties: metadata: diff --git a/metricproviders/datadog/datadog.go b/metricproviders/datadog/datadog.go new file mode 100644 index 0000000000..607c061600 --- /dev/null +++ b/metricproviders/datadog/datadog.go @@ -0,0 +1,211 @@ +package datadog + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + "strconv" + "strings" + "time" + + "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" + "github.com/argoproj/argo-rollouts/utils/evaluate" + metricutil "github.com/argoproj/argo-rollouts/utils/metric" + log "github.com/sirupsen/logrus" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +var unixNow = func() int64 { return time.Now().Unix() } + +const ( + //ProviderType indicates the provider is datadog + ProviderType = "Datadog" + DatadogTokensSecretName = "datadog" +) + +// Provider contains all the required components to run a Datadog query +// Implements the Provider Interface +type Provider struct { + logCtx log.Entry + config datadogConfig +} + +type datadogResponse struct { + Series []struct { + Pointlist [][]float64 `json:"pointlist"` + } +} + +type datadogConfig struct { + Address string `yaml:"address,omitempty"` + ApiKey string `yaml:"api-key,omitempty"` + AppKey string `yaml:"app-key,omitempty"` +} + +// Type incidates provider is a Datadog provider +func (p *Provider) Type() string { + return ProviderType +} + +func (p *Provider) Run(run *v1alpha1.AnalysisRun, metric v1alpha1.Metric) v1alpha1.Measurement { + startTime := metav1.Now() + + // Measurement to pass back + measurement := v1alpha1.Measurement{ + StartedAt: &startTime, + } + + endpoint := "https://api.datadoghq.com/api/v1/query" + if p.config.Address != "" { + endpoint = p.config.Address + "/api/v1/query" + } + + url, _ := url.Parse(endpoint) + + now := unixNow() + var interval int64 = 300 + if metric.Provider.Datadog.Interval != "" { + expDuration, err := metric.Provider.Datadog.Interval.Duration() + if err != nil { + return metricutil.MarkMeasurementError(measurement, err) + } + // Convert to seconds as DataDog expects unix timestamp + interval = int64(expDuration.Seconds()) + } + + q := url.Query() + q.Set("query", metric.Provider.Datadog.Query) + q.Set("from", strconv.FormatInt(now-interval, 10)) + q.Set("to", strconv.FormatInt(now, 10)) + url.RawQuery = q.Encode() + + request := &http.Request{Method: "GET"} + request.URL = url + request.Header = make(http.Header) + request.Header.Set("Content-Type", "application/json") + request.Header.Set("DD-API-KEY", p.config.ApiKey) + request.Header.Set("DD-APPLICATION-KEY", p.config.AppKey) + + // Send Request + httpClient := &http.Client{ + Timeout: time.Duration(10) * time.Second, + } + response, err := httpClient.Do(request) + + if err != nil { + return metricutil.MarkMeasurementError(measurement, err) + } + + value, status, err := p.parseResponse(metric, response) + if err != nil { + return metricutil.MarkMeasurementError(measurement, err) + } + + measurement.Value = value + measurement.Phase = status + finishedTime := metav1.Now() + measurement.FinishedAt = &finishedTime + + return measurement +} + +func (p *Provider) parseResponse(metric v1alpha1.Metric, response *http.Response) (string, v1alpha1.AnalysisPhase, error) { + + bodyBytes, err := ioutil.ReadAll(response.Body) + + if err != nil { + return "", v1alpha1.AnalysisPhaseError, fmt.Errorf("Received no bytes in response: %v", err) + } + + if response.StatusCode == http.StatusForbidden || response.StatusCode == http.StatusUnauthorized { + return "", v1alpha1.AnalysisPhaseError, fmt.Errorf("received authentication error response code: %v %s", response.StatusCode, string(bodyBytes)) + } else if response.StatusCode != http.StatusOK { + return "", v1alpha1.AnalysisPhaseError, fmt.Errorf("received non 2xx response code: %v %s", response.StatusCode, string(bodyBytes)) + } + + var res datadogResponse + err = json.Unmarshal(bodyBytes, &res) + if err != nil { + return "", v1alpha1.AnalysisPhaseError, fmt.Errorf("Could not parse JSON body: %v", err) + } + + if len(res.Series) < 1 || len(res.Series[0].Pointlist) < 1 { + return "", v1alpha1.AnalysisPhaseError, fmt.Errorf("Datadog returned no value: %s", string(bodyBytes)) + } + + series := res.Series[0] + datapoint := series.Pointlist[len(series.Pointlist)-1] + if len(datapoint) < 1 { + return "", v1alpha1.AnalysisPhaseError, fmt.Errorf("Datadog returned no value: %s", string(bodyBytes)) + } + + status := evaluate.EvaluateResult(datapoint[1], metric, p.logCtx) + return strconv.FormatFloat(datapoint[1], 'f', -1, 64), status, nil +} + +// Resume should not be used the Datadog provider since all the work should occur in the Run method +func (p *Provider) Resume(run *v1alpha1.AnalysisRun, metric v1alpha1.Metric, measurement v1alpha1.Measurement) v1alpha1.Measurement { + p.logCtx.Warn("Datadog provider should not execute the Resume method") + return measurement +} + +// Terminate should not be used the Datadog provider since all the work should occur in the Run method +func (p *Provider) Terminate(run *v1alpha1.AnalysisRun, metric v1alpha1.Metric, measurement v1alpha1.Measurement) v1alpha1.Measurement { + p.logCtx.Warn("Datadog provider should not execute the Terminate method") + return measurement +} + +// GarbageCollect is a no-op for the Datadog provider +func (p *Provider) GarbageCollect(run *v1alpha1.AnalysisRun, metric v1alpha1.Metric, limit int) error { + return nil +} + +func NewDatadogProvider(logCtx log.Entry, kubeclientset kubernetes.Interface) (*Provider, error) { + ns := Namespace() + secret, err := kubeclientset.CoreV1().Secrets(ns).Get(context.TODO(), DatadogTokensSecretName, metav1.GetOptions{}) + if err != nil { + return nil, err + } + + apiKey := string(secret.Data["api-key"]) + appKey := string(secret.Data["app-key"]) + address := "" + if _, hasAddress := secret.Data["address"]; hasAddress { + address = string(secret.Data["address"]) + } + + if apiKey != "" && appKey != "" { + return &Provider{ + logCtx: logCtx, + config: datadogConfig{ + Address: address, + ApiKey: apiKey, + AppKey: appKey, + }, + }, nil + } else { + return nil, errors.New("API or App token not found") + } + +} + +func Namespace() string { + // This way assumes you've set the POD_NAMESPACE environment variable using the downward API. + // This check has to be done first for backwards compatibility with the way InClusterConfig was originally set up + if ns, ok := os.LookupEnv("POD_NAMESPACE"); ok { + return ns + } + // Fall back to the namespace associated with the service account token, if available + if data, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); err == nil { + if ns := strings.TrimSpace(string(data)); len(ns) > 0 { + return ns + } + } + return "argo-rollouts" +} diff --git a/metricproviders/datadog/datadog_test.go b/metricproviders/datadog/datadog_test.go new file mode 100644 index 0000000000..ddd5a7d887 --- /dev/null +++ b/metricproviders/datadog/datadog_test.go @@ -0,0 +1,243 @@ +package datadog + +import ( + "io" + "net/http" + "net/http/httptest" + "strconv" + "testing" + + "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + k8sfake "k8s.io/client-go/kubernetes/fake" + kubetesting "k8s.io/client-go/testing" +) + +func TestRunSuite(t *testing.T) { + + const expectedApiKey = "0123456789abcdef0123456789abcdef" + const expectedAppKey = "0123456789abcdef0123456789abcdef01234567" + + unixNow = func() int64 { return 1599076435 } + + // Test Cases + var tests = []struct { + webServerStatus int + webServerResponse string + metric v1alpha1.Metric + expectedIntervalSeconds int64 + expectedValue string + expectedPhase v1alpha1.AnalysisPhase + expectedErrorMessage string + }{ + // When last value of time series matches condition then succeed. + { + webServerStatus: 200, + webServerResponse: `{"status":"ok","series":[{"pointlist":[[1598867910000,0.0020008318672513122],[1598867925000,0.0003332881882246533]]}]}`, + metric: v1alpha1.Metric{ + Name: "foo", + SuccessCondition: "result < 0.001", + FailureCondition: "result >= 0.001", + Provider: v1alpha1.MetricProvider{ + Datadog: &v1alpha1.DatadogMetric{ + Query: "avg:kubernetes.cpu.user.total{*}", + Interval: "10m", + }, + }, + }, + expectedIntervalSeconds: 600, + expectedValue: "0.0003332881882246533", + expectedPhase: v1alpha1.AnalysisPhaseSuccessful, + }, + // When last value of time series does not match condition then fail. + { + webServerStatus: 200, + webServerResponse: `{"status":"ok","series":[{"pointlist":[[1598867910000,0.0020008318672513122],[1598867925000,0.006121378742186943]]}]}`, + metric: v1alpha1.Metric{ + Name: "foo", + SuccessCondition: "result < 0.001", + FailureCondition: "result >= 0.001", + Provider: v1alpha1.MetricProvider{ + Datadog: &v1alpha1.DatadogMetric{ + Query: "avg:kubernetes.cpu.user.total{*}", + }, + }, + }, + expectedIntervalSeconds: 300, + expectedValue: "0.006121378742186943", + expectedPhase: v1alpha1.AnalysisPhaseFailed, + }, + // Error if the request is invalid + { + webServerStatus: 400, + webServerResponse: `{"status":"error","error":"error messsage"}`, + metric: v1alpha1.Metric{ + Name: "foo", + SuccessCondition: "result < 0.001", + FailureCondition: "result >= 0.001", + Provider: v1alpha1.MetricProvider{ + Datadog: &v1alpha1.DatadogMetric{ + Query: "avg:kubernetes.cpu.user.total{*}", + }, + }, + }, + expectedIntervalSeconds: 300, + expectedPhase: v1alpha1.AnalysisPhaseError, + expectedErrorMessage: "received non 2xx response code: 400 {\"status\":\"error\",\"error\":\"error messsage\"}", + }, + // Error if there is an authentication issue + { + webServerStatus: 401, + webServerResponse: `{"errors": ["No authenticated user."]}`, + metric: v1alpha1.Metric{ + Name: "foo", + SuccessCondition: "result < 0.001", + FailureCondition: "result >= 0.001", + Provider: v1alpha1.MetricProvider{ + Datadog: &v1alpha1.DatadogMetric{ + Query: "avg:kubernetes.cpu.user.total{*}", + }, + }, + }, + expectedIntervalSeconds: 300, + expectedPhase: v1alpha1.AnalysisPhaseError, + expectedErrorMessage: "received authentication error response code: 401 {\"errors\": [\"No authenticated user.\"]}", + }, + // Error if datadog doesn't return any datapoints + { + webServerStatus: 200, + webServerResponse: `{"status":"ok","series":[{"pointlist":[]}]}`, + metric: v1alpha1.Metric{ + Name: "foo", + SuccessCondition: "result < 0.001", + FailureCondition: "result >= 0.001", + Provider: v1alpha1.MetricProvider{ + Datadog: &v1alpha1.DatadogMetric{ + Query: "avg:kubernetes.cpu.user.total{*}", + }, + }, + }, + expectedIntervalSeconds: 300, + expectedPhase: v1alpha1.AnalysisPhaseError, + expectedErrorMessage: "Datadog returned no value: {\"status\":\"ok\",\"series\":[{\"pointlist\":[]}]}", + }, + + // Error if datadog doesn't return any datapoints + { + webServerStatus: 200, + webServerResponse: `{"status":"ok","series":"invalid"}`, + metric: v1alpha1.Metric{ + Name: "foo", + SuccessCondition: "result < 0.001", + FailureCondition: "result >= 0.001", + Provider: v1alpha1.MetricProvider{ + Datadog: &v1alpha1.DatadogMetric{ + Query: "avg:kubernetes.cpu.user.total{*}", + }, + }, + }, + expectedIntervalSeconds: 300, + expectedPhase: v1alpha1.AnalysisPhaseError, + expectedErrorMessage: "Could not parse JSON body: json: cannot unmarshal string into Go struct field datadogResponse.Series of type []struct { Pointlist [][]float64 \"json:\\\"pointlist\\\"\" }", + }, + } + + // Run + + for _, test := range tests { + // Server setup with response + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + + //Check query variables + actualQuery := req.URL.Query().Get("query") + actualFrom := req.URL.Query().Get("from") + actualTo := req.URL.Query().Get("to") + + if actualQuery != "avg:kubernetes.cpu.user.total{*}" { + t.Errorf("\nquery expected avg:kubernetes.cpu.user.total{*} but got %s", actualQuery) + } + + if from, err := strconv.ParseInt(actualFrom, 10, 64); err == nil && from != unixNow()-test.expectedIntervalSeconds { + t.Errorf("\nfrom %d expected be equal to %d", from, unixNow()-test.expectedIntervalSeconds) + } else if err != nil { + t.Errorf("\nfailed to parse from: %v", err) + } + + if to, err := strconv.ParseInt(actualTo, 10, 64); err == nil && to != unixNow() { + t.Errorf("\nto %d was expected be equal to %d", to, unixNow()) + } else if err != nil { + t.Errorf("\nfailed to parse to: %v", err) + } + + //Check headers + if req.Header.Get("Content-Type") != "application/json" { + t.Errorf("\nContent-Type header expected to be application/json but got %s", req.Header.Get("Content-Type")) + } + if req.Header.Get("DD-API-KEY") != expectedApiKey { + t.Errorf("\nDD-API-KEY header expected %s but got %s", expectedApiKey, req.Header.Get("DD-API-KEY")) + } + if req.Header.Get("DD-APPLICATION-KEY") != expectedAppKey { + t.Errorf("\nDD-APPLICATION-KEY header expected %s but got %s", expectedAppKey, req.Header.Get("DD-APPLICATION-KEY")) + } + + // Return mock response + if test.webServerStatus < 200 || test.webServerStatus >= 300 { + http.Error(rw, test.webServerResponse, test.webServerStatus) + } else { + rw.Header().Set("Content-Type", "application/json") + io.WriteString(rw, test.webServerResponse) + } + })) + defer server.Close() + + tokenSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: DatadogTokensSecretName, + }, + Data: map[string][]byte{ + "address": []byte(server.URL), + "api-key": []byte(expectedApiKey), + "app-key": []byte(expectedAppKey), + }, + } + + logCtx := log.WithField("test", "test") + + fakeClient := k8sfake.NewSimpleClientset() + fakeClient.PrependReactor("get", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { + return true, tokenSecret, nil + }) + + provider, _ := NewDatadogProvider(*logCtx, fakeClient) + + // Get our result + measurement := provider.Run(newAnalysisRun(), test.metric) + + // Common Asserts + assert.NotNil(t, measurement) + assert.Equal(t, string(test.expectedPhase), string(measurement.Phase)) + + // Phase specific cases + switch test.expectedPhase { + case v1alpha1.AnalysisPhaseSuccessful: + assert.NotNil(t, measurement.StartedAt) + assert.Equal(t, test.expectedValue, measurement.Value) + assert.NotNil(t, measurement.FinishedAt) + case v1alpha1.AnalysisPhaseFailed: + assert.NotNil(t, measurement.StartedAt) + assert.Equal(t, test.expectedValue, measurement.Value) + assert.NotNil(t, measurement.FinishedAt) + case v1alpha1.AnalysisPhaseError: + assert.Contains(t, measurement.Message, test.expectedErrorMessage) + } + + } +} + +func newAnalysisRun() *v1alpha1.AnalysisRun { + return &v1alpha1.AnalysisRun{} +} diff --git a/metricproviders/metricproviders.go b/metricproviders/metricproviders.go index b3a903ed30..6cf015836d 100644 --- a/metricproviders/metricproviders.go +++ b/metricproviders/metricproviders.go @@ -5,6 +5,7 @@ import ( "github.com/argoproj/argo-rollouts/metricproviders/wavefront" + "github.com/argoproj/argo-rollouts/metricproviders/datadog" "github.com/argoproj/argo-rollouts/metricproviders/kayenta" "github.com/argoproj/argo-rollouts/metricproviders/webmetric" @@ -61,6 +62,8 @@ func (f *ProviderFactory) NewProvider(logCtx log.Entry, metric v1alpha1.Metric) return nil, err } return webmetric.NewWebMetricProvider(logCtx, c, p), nil + case datadog.ProviderType: + return datadog.NewDatadogProvider(logCtx, f.KubeClient) case wavefront.ProviderType: client, err := wavefront.NewWavefrontAPI(metric, f.KubeClient) if err != nil { @@ -81,6 +84,8 @@ func Type(metric v1alpha1.Metric) string { return kayenta.ProviderType } else if metric.Provider.Web != nil { return webmetric.ProviderType + } else if metric.Provider.Datadog != nil { + return datadog.ProviderType } else if metric.Provider.Wavefront != nil { return wavefront.ProviderType } diff --git a/pkg/apis/rollouts/v1alpha1/analysis_types.go b/pkg/apis/rollouts/v1alpha1/analysis_types.go index fa7a168ebd..a9dac12266 100644 --- a/pkg/apis/rollouts/v1alpha1/analysis_types.go +++ b/pkg/apis/rollouts/v1alpha1/analysis_types.go @@ -128,6 +128,8 @@ type MetricProvider struct { Kayenta *KayentaMetric `json:"kayenta,omitempty"` // Web specifies a generic HTTP web metric Web *WebMetric `json:"web,omitempty"` + // Datadog specifies a datadog metric to query + Datadog *DatadogMetric `json:"datadog,omitempty"` // Wavefront specifies the wavefront metric to query Wavefront *WavefrontMetric `json:"wavefront,omitempty"` // Job specifies the job metric run @@ -350,3 +352,8 @@ type WebMetricHeader struct { Key string `json:"key"` Value string `json:"value"` } + +type DatadogMetric struct { + Interval DurationString `json:"interval,omitempty"` + Query string `json:"query"` +} diff --git a/pkg/apis/rollouts/v1alpha1/openapi_generated.go b/pkg/apis/rollouts/v1alpha1/openapi_generated.go index e9804b79ed..a952ad40d6 100644 --- a/pkg/apis/rollouts/v1alpha1/openapi_generated.go +++ b/pkg/apis/rollouts/v1alpha1/openapi_generated.go @@ -48,6 +48,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.CanaryStrategy": schema_pkg_apis_rollouts_v1alpha1_CanaryStrategy(ref), "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.ClusterAnalysisTemplate": schema_pkg_apis_rollouts_v1alpha1_ClusterAnalysisTemplate(ref), "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.ClusterAnalysisTemplateList": schema_pkg_apis_rollouts_v1alpha1_ClusterAnalysisTemplateList(ref), + "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.DatadogMetric": schema_pkg_apis_rollouts_v1alpha1_DatadogMetric(ref), "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.Experiment": schema_pkg_apis_rollouts_v1alpha1_Experiment(ref), "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.ExperimentAnalysisRunStatus": schema_pkg_apis_rollouts_v1alpha1_ExperimentAnalysisRunStatus(ref), "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.ExperimentAnalysisTemplateRef": schema_pkg_apis_rollouts_v1alpha1_ExperimentAnalysisTemplateRef(ref), @@ -1019,6 +1020,31 @@ func schema_pkg_apis_rollouts_v1alpha1_ClusterAnalysisTemplateList(ref common.Re } } +func schema_pkg_apis_rollouts_v1alpha1_DatadogMetric(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "interval": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "query": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"query"}, + }, + }, + } +} + func schema_pkg_apis_rollouts_v1alpha1_Experiment(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -1806,6 +1832,11 @@ func schema_pkg_apis_rollouts_v1alpha1_MetricProvider(ref common.ReferenceCallba Ref: ref("github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.WebMetric"), }, }, + "datadog": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.DatadogMetric"), + }, + }, "wavefront": { SchemaProps: spec.SchemaProps{ Description: "Wavefront specifies the wavefront metric to query", @@ -1822,7 +1853,7 @@ func schema_pkg_apis_rollouts_v1alpha1_MetricProvider(ref common.ReferenceCallba }, }, Dependencies: []string{ - "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.JobMetric", "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.KayentaMetric", "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.PrometheusMetric", "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.WavefrontMetric", "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.WebMetric"}, + "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.DatadogMetric", "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.JobMetric", "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.KayentaMetric", "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.PrometheusMetric", "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.WavefrontMetric", "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.WebMetric"}, } } diff --git a/pkg/apis/rollouts/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/rollouts/v1alpha1/zz_generated.deepcopy.go index 028ab28867..8198fa0a1c 100644 --- a/pkg/apis/rollouts/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/rollouts/v1alpha1/zz_generated.deepcopy.go @@ -605,6 +605,22 @@ func (in *ClusterAnalysisTemplateList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DatadogMetric) DeepCopyInto(out *DatadogMetric) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatadogMetric. +func (in *DatadogMetric) DeepCopy() *DatadogMetric { + if in == nil { + return nil + } + out := new(DatadogMetric) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Experiment) DeepCopyInto(out *Experiment) { *out = *in @@ -984,6 +1000,11 @@ func (in *MetricProvider) DeepCopyInto(out *MetricProvider) { *out = new(WebMetric) (*in).DeepCopyInto(*out) } + if in.Datadog != nil { + in, out := &in.Datadog, &out.Datadog + *out = new(DatadogMetric) + **out = **in + } if in.Wavefront != nil { in, out := &in.Wavefront, &out.Wavefront *out = new(WavefrontMetric) diff --git a/utils/analysis/factory.go b/utils/analysis/factory.go index b2fca32862..84cf73e525 100644 --- a/utils/analysis/factory.go +++ b/utils/analysis/factory.go @@ -153,6 +153,9 @@ func ValidateMetric(metric v1alpha1.Metric) error { if metric.Provider.Kayenta != nil { numProviders++ } + if metric.Provider.Datadog != nil { + numProviders++ + } if numProviders == 0 { return fmt.Errorf("no provider specified") } diff --git a/utils/analysis/factory_test.go b/utils/analysis/factory_test.go index 5a59ecdf6e..cee8e6f018 100644 --- a/utils/analysis/factory_test.go +++ b/utils/analysis/factory_test.go @@ -299,6 +299,7 @@ func TestValidateMetrics(t *testing.T) { Wavefront: &v1alpha1.WavefrontMetric{}, Kayenta: &v1alpha1.KayentaMetric{}, Web: &v1alpha1.WebMetric{}, + Datadog: &v1alpha1.DatadogMetric{}, }, }, },