diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9a913bf --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.vscode/ +.idea/ +docs/ +deploy/ +Dockerfile diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6b2e394 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.vscode + +.idea/* +# allow shared run configurations +!.idea/runConfigurations/ + +vendor + diff --git a/.idea/runConfigurations/keptn_prometheus_sli_service__Kube_ContDeploy_.xml b/.idea/runConfigurations/keptn_prometheus_sli_service__Kube_ContDeploy_.xml new file mode 100644 index 0000000..a7e9f08 --- /dev/null +++ b/.idea/runConfigurations/keptn_prometheus_sli_service__Kube_ContDeploy_.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..fbfbdb1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,118 @@ +dist: xenial +language: bash +services: + - docker +env: + global: + - GO111MODULE=on +before_install: + - export TZ=Europe/Vienna + - IMAGE=keptn/prometheus-sli-service + - REPO_URL="$(git remote get-url --all origin)" + # get the last tag + - GIT_LAST_TAG=$(git describe --tags $(git rev-list --tags) || echo "0.0.0") + - GIT_NUM_COMMITS_SINCE_LAST_TAG=$(git rev-list ${GIT_LAST_TAG}..HEAD --count || echo "trunk") + # get current branch name + - GIT_BRANCH_NAME=$(git branch | grep \* | cut -d ' ' -f2) + - GIT_SHA=$(git rev-parse --short HEAD) + - GIT_BRANCH_AND_COMMIT=$(git describe --exact-match 2> /dev/null || echo "`git symbolic-ref HEAD 2> /dev/null | cut -b 12-`-`git log --pretty=format:\"%h\" -1`") + # find out if we are on a tag (= exact match) + # if not: use tag + number of commit + commit hash + - VERSION="$(cat version | tr -d '[:space:]')" + - DATE="$(date +'%Y%m%d.%H%M')" + - ./writeManifest.sh + - cat MANIFEST + # uncomment certain lines from Dockerfile that are for travis builds only + - sed -i '/#travis-uncomment/s/^#travis-uncomment //g' Dockerfile +jobs: + include: + - stage: codestyle + # Check Codestyle using go fmt + services: [] + language: go + go: + - 1.12.x + # skip install + install: true + script: + - echo "Checking code style..." + - unformatted=$(gofmt -l .) + - | + if [ ! -z "$unformatted" ]; then + echo "Code Style Check failed for the following files: ${unformatted}". + echo "Please run: gofmt -w ." + echo "After that ammend your commit (e.g.: git add ${unformatted} && git commit --amend --no-edit) and force push the changes (git push -f)." + travis_terminate 1 + fi + + - stage: tests + # Run tests + services: [] + language: go + go: + - 1.12.x + # cache some go files + cache: + directories: + - $HOME/.cache/go-build + - $HOME/gopath/pkg/mod + addons: + sonarcloud: + organization: "keptn-contrib" + script: + - sonar-scanner + - go build + - go test -race -v ./... + + - stage: feature/bug/hotfix/patch + # build docker images for feature/bug/hotfix/patch branches + if: branch =~ ^feature.*$ OR branch =~ ^bug.*$ OR branch =~ ^hotfix.*$ OR branch =~ ^patch.*$ + script: + - echo $TRAVIS_BUILD_STAGE_NAME + - TYPE="$(echo $TRAVIS_BRANCH | cut -d'/' -f1)" + - NUMBER="$(echo $TRAVIS_BRANCH | cut -d'/' -f2)" + - docker build . -t "${IMAGE}:${GIT_SHA}" + - docker tag "${IMAGE}:${GIT_SHA}" "${IMAGE}:${TYPE}.${NUMBER}.${DATE}" + after_success: + - echo "$REGISTRY_PASSWORD" | docker login --username $REGISTRY_USER --password-stdin + - docker push "${IMAGE}:${GIT_SHA}" + - docker push "${IMAGE}:${TYPE}.${NUMBER}.${DATE}" + + + - stage: develop + # build docker images for develop branch + if: branch = develop AND NOT type = pull_request + script: + - echo $TRAVIS_BUILD_STAGE_NAME + - docker build . -t "${IMAGE}:${GIT_SHA}" + - docker tag "${IMAGE}:${GIT_SHA}" "${IMAGE}:${DATE}" + - docker tag "${IMAGE}:${GIT_SHA}" "${IMAGE}:latest" + after_success: + - echo "$REGISTRY_PASSWORD" | docker login --username $REGISTRY_USER --password-stdin + - docker push "${IMAGE}:${GIT_SHA}" + - docker push "${IMAGE}:${DATE}" + - docker push "${IMAGE}:latest" + + - stage: release-branch + # build docker images for release branches + if: branch =~ ^release.*$ AND NOT type = pull_request + script: + - echo $TRAVIS_BUILD_STAGE_NAME + - docker build . -t "${IMAGE}:${GIT_SHA}" + - docker tag "${IMAGE}:${GIT_SHA}" "${IMAGE}:${VERSION}.${DATE}" + - docker tag "${IMAGE}:${GIT_SHA}" "${IMAGE}:${VERSION}.latest" + after_success: + - echo "$REGISTRY_PASSWORD" | docker login --username $REGISTRY_USER --password-stdin + - docker push "${IMAGE}:${GIT_SHA}" + - docker push "${IMAGE}:${VERSION}.${DATE}" + - docker push "${IMAGE}:${VERSION}.latest" + + - stage: tags + # build docker images for tags + if: tag IS present + script: + - echo $TRAVIS_BUILD_STAGE_NAME + - docker build . -t "${IMAGE}:${VERSION}" + after_success: + - echo "$REGISTRY_PASSWORD" | docker login --username $REGISTRY_USER --password-stdin + - docker push "${IMAGE}:${VERSION}" diff --git a/CODEOWNERS b/CODEOWNERS index 9f90b59..9f539c5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,4 +1,5 @@ # Lines starting with '#' are comments. # Each line is a file pattern followed by one or more owners. -# These owners will be the default owners for everything in the repo. \ No newline at end of file +# These owners will be the default owners for everything in the repo. +* @christian-kreuzberger-dtx @bacherfl diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c1e3680 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,54 @@ +# from https://skaffold.dev/docs/workflows/debug/ +# Use the offical Golang image to create a build artifact. +# This is based on Debian and sets the GOPATH to /go. +# https://hub.docker.com/_/golang +FROM golang:1.12 as builder + +WORKDIR /go/src/github.com/keptn-contrib/prometheus-sli-service + +ENV GO111MODULE=on +ENV BUILDFLAGS="" + +# Copy `go.mod` for definitions and `go.sum` to invalidate the next layer +# in case of a change in the dependencies +COPY go.mod go.sum ./ + +# download dependencies +RUN go mod download + +ARG debugBuild + +# set buildflags for debug build +RUN if [ ! -z "$debugBuild" ]; then export BUILDFLAGS='-gcflags "all=-N -l"'; fi + +# finally Copy local code to the container image. +COPY . . + +# Build the command inside the container. +# (You may fetch or manage dependencies here, either manually or with a tool like "godep".) +RUN CGO_ENABLED=0 GOOS=linux go build $BUILDFLAGS -v -o prometheus-sli-service + +# Use a Docker multi-stage build to create a lean production image. +# https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds +FROM alpine:3.7 +RUN apk add --no-cache ca-certificates + +ARG debugBuild + +# IF we are debugging, we need to install libc6-compat for delve to work on alpine based containers +RUN if [ ! -z "$debugBuild" ]; then apk add --no-cache libc6-compat; fi + +# Copy the binary to the production image from the builder stage. +COPY --from=builder /go/src/github.com/keptn-contrib/prometheus-sli-service/prometheus-sli-service /prometheus-sli-service + +EXPOSE 8080 + +# required for external tools to detect this as a go binary +ENV GOTRACEBACK=all + +# KEEP THE FOLLOWING LINES COMMENTED OUT!!! (they will be included within the travis-ci build) +#travis-uncomment ADD MANIFEST / +#travis-uncomment COPY entrypoint.sh / +#travis-uncomment ENTRYPOINT ["/entrypoint.sh"] + +CMD ["/prometheus-sli-service"] diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000..d466a59 --- /dev/null +++ b/MANIFEST @@ -0,0 +1,8 @@ +########## +branch: MANIFEST_BRANCH +repository: MANIFEST_REPOSITORY +commitlink: MANIFEST_REPOSITORY/commit/MANIFEST_COMMIT +repolink: MANIFEST_REPOSITORY/tree/MANIFEST_COMMIT +travisbuild: MANIFEST_TRAVIS_JOB_URL +timestamp: MANIFEST_DATE +########## diff --git a/README.md b/README.md index 06802fd..54a5594 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,103 @@ # Prometheus SLI Service +![GitHub release (latest by date)](https://img.shields.io/github/v/release/keptn-contrib/prometheus-sli-service) +[![Build Status](https://travis-ci.org/keptn-contrib/prometheus-sli-service.svg?branch=master)](https://travis-ci.org/keptn-contrib/prometheus-sli-service) +[![Go Report Card](https://goreportcard.com/badge/github.com/keptn-contrib/prometheus-sli-service)](https://goreportcard.com/report/github.com/keptn-contrib/prometheus-sli-service) -The *prometheus-sli-service* is a Keptn service that is responsible for retrieving the values of Keptn-supported SLIs from a Prometheus API endpoint. +This service is used for retrieving service level indicators (SLIs) from a prometheus API endpoint. Per default, it fetches metrics from the prometheus instance set up by Keptn +(`prometheus-service.monitoring.svc.cluster.local:8080`), but it can also be configures to use any reachable Prometheus endpoint using basic authentication by providing the credentials +via a secret in the `keptn` namespace of the cluster. + +The supported default SLIs are: + + - throughput + - error_rate + - response_time_p50 + - response_time_p90 + - response_time_p95 + +The provided SLIs are based on the [RED metrics](https://grafana.com/files/grafanacon_eu_2018/Tom_Wilkie_GrafanaCon_EU_2018.pdf) + +## Basic Usage + +Per default, the service works with the following assumptions regarding the setup of the Prometheus instance: + + - Each **service** within a **stage** of a **project** has a Prometheus scrape job definition with the name: `--` + + For example, if `project=sockshop`, `stage=production` and `service=carts`, the scrape job name would have to be `carts-sockshop-production`. + + - Every service provides the following Metrics for its corresponding scrape job: + - http_response_time_milliseconds (Histogram) + - http_requests_total (Counter) + + This metric has to contain the `status` label, indicating the HTTP response code of the requests handled by the service. + It is highly recommended that this metric also provides a label to query metric values for specific endpoints, e.g. `handler` + + An example of an entry would look like this: `http_requests_total{method="GET",handler="VersionController.getInformation",status="200",} 4.0` + + - Based on those metrics, the queries for the SLIs are built as follows: + + - **throughput**: `sum(rate(http_requests_total{job="---canary"}[s]))` + - **error_rate**: `sum(rate(http_requests_total{job="---canary",status!~'2..'}[s]))/sum(rate(http_requests_total{job="---canary"}[s]))` + - **response_time_p50**: `histogram_quantile(0.50, sum(rate(http_response_time_milliseconds_bucket{job='---canary'}[s])) by (le))` + - **response_time_p90**: `histogram_quantile(0.90, sum(rate(http_response_time_milliseconds_bucket{job='---canary'}[s])) by (le))` + - **response_time_p95**: `histogram_quantile(0.95, sum(rate(http_response_time_milliseconds_bucket{job='---canary'}[s])) by (le))` + +## Advanced Usage + +### Using an external Prometheus instance +To use a Prometheus instance other than the one that's being managed by Keptn for a certain project, a secret containing the URL and the access credentials has to be deployed into the `keptn` namespace. The secret must have the following format: + +```yaml +user: test +password: test +url: http://prometheus-service.monitoring.svc.cluster.local:8080 +``` + +If this information is stored in a file, e.g. `prometheus-creds.yaml`, it can be stored with the following command (don't forget to replace the `` placeholder with the name of your project: + +```bash +kubectl create secret -n keptn generic prometheus-credentials- --from-file=prometheus-credentials=./mock_secret.yaml +``` + +Please note that there is a naming convention for the secret, because this can be configured per **project**. Therefore, the secret has to have the name `prometheus-credentials-` + + +### Custom SLI queries + +Users can override the predefined queries, as well as add custom SLI queries by creating a `ConfigMap` with the name `prometheus-sli-config-` in the `keptn` namespace. +In this ConfigMap, a YAML object containing the queries can be defined, e.g.: + +```yaml +kind: ConfigMap +apiVersion: v1 +metadata: + name: prometheus-sli-config-sockshop + namespace: keptn +data: + custom-queries: | + throughput: "rate(my_custom_metric{job='$SERVICE-$PROJECT-$STAGE',handler=~'$handler'}[$DURATION_SECONDS])" + error_rate: "sum(rate(my_custom_metric{job='$SERVICE-$PROJECT-$STAGE',handler=~'$handler',status!~'2..'}[1s]))/sum(rate(my_custom_metric{job='$SERVICE-$PROJECT-$STAGE',handler=~'$handler'}[$DURATION_SECONDS]))" + response_time_p50: "histogram_quantile(0.50,sum(rate(my_custom_response_time_metric{job='$SERVICE-$PROJECT-$STAGE'}[$DURATION_SECONDS]))by(le))" + response_time_p90: "histogram_quantile(0.90,sum(rate(my_custom_response_time_metric{job='$SERVICE-$PROJECT-$STAGE'}[$DURATION_SECONDS]))by(le))" + response_time_p95: "histogram_quantile(0.95,sum(rate(my_custom_response_time_metric{job='$SERVICE-$PROJECT-$STAGE'}[$DURATION_SECONDS]))by(le))" + # Example for a custom SLI that is not part of the default SLIs + cpu_usage: avg(rate(container_cpu_usage_seconds_total{namespace="$PROJECT-$STAGE",pod_name=~"$SERVICE-primary-.*"}[5m])) +``` + +Note that, similarly, to the custom endpoint configuration, the name of the ConfigMap has to be `prometheus-sli-config-`, and has to be stored in the `keptn` namespace. + +Within the user-defined queries, the following variables can be used to dynamically build the query, depending on the project/stage/service, and the time frame: + +- $PROJECT: will be replaced with the name of the project +- $STAGE: will be replaced with the name of the stage +- $SERVICE: will be replaced with the name of the service +- $DURATION_SECONDS: will be replaced with the test run duration, e.g. 30s + +For example, if an evaluation for the service **carts** in the stage **production** of the project **sockshop** is triggered, and the tests ran for 30s these will be the resulting queries: + +``` +rate(my_custom_metric{job='$SERVICE-$PROJECT-$STAGE',handler=~'$handler'}[$DURATION_SECONDS]) => rate(my_custom_metric{job='carts-sockshop-production',handler=~'$handler'}[30s]) +``` ## Installation diff --git a/deploy/distributor.yaml b/deploy/distributor.yaml new file mode 100644 index 0000000..bc4f85c --- /dev/null +++ b/deploy/distributor.yaml @@ -0,0 +1,36 @@ +--- +## prometheus-sli-service sh.keptn.internal.event.get-sli-distributor +apiVersion: apps/v1 +kind: Deployment +metadata: + name: prometheus-sli-service-monitoring-configure-distributor + namespace: keptn +spec: + selector: + matchLabels: + run: distributor + replicas: 1 + template: + metadata: + labels: + run: distributor + spec: + containers: + - name: distributor + image: keptn/distributor:latest + ports: + - containerPort: 8080 + resources: + requests: + memory: "32Mi" + cpu: "50m" + limits: + memory: "128Mi" + cpu: "500m" + env: + - name: PUBSUB_URL + value: 'nats://keptn-nats-cluster' + - name: PUBSUB_TOPIC + value: 'sh.keptn.internal.event.get-sli' + - name: PUBSUB_RECIPIENT + value: 'prometheus-sli-service' diff --git a/deploy/service.yaml b/deploy/service.yaml new file mode 100644 index 0000000..365a861 --- /dev/null +++ b/deploy/service.yaml @@ -0,0 +1,47 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: prometheus-sli-service + namespace: keptn +spec: + selector: + matchLabels: + run: prometheus-sli-service + replicas: 1 + template: + metadata: + labels: + run: prometheus-sli-service + spec: + containers: + - name: prometheus-sli-service + image: keptn/prometheus-sli-service:0.1.0 + ports: + - containerPort: 8080 + resources: + requests: + memory: "32Mi" + cpu: "50m" + limits: + memory: "128Mi" + cpu: "500m" + env: + - name: CONFIGURATION_SERVICE + value: 'http://configuration-service.keptn.svc.cluster.local:8080' + - name: EVENTBROKER + value: 'http://event-broker.keptn.svc.cluster.local/keptn' +--- +apiVersion: v1 +kind: Service +metadata: + name: prometheus-sli-service + namespace: keptn + labels: + run: prometheus-sli-service +spec: + ports: + - port: 8080 + protocol: TCP + selector: + run: prometheus-sli-service diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 0000000..5f692ca --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +cat /MANIFEST + +exec "$@" diff --git a/get-sli.http b/get-sli.http new file mode 100644 index 0000000..023e0ae --- /dev/null +++ b/get-sli.http @@ -0,0 +1,68 @@ +# For a quick start check out our HTTP Requests collection (Tools|HTTP Client|Open HTTP Requests Collection) or +# paste cURL into the file and request will be converted to HTTP Request format. +# +# Following HTTP Request Live Templates are available: +# * 'gtrp' and 'gtr' create a GET request with or without query parameters; +# * 'ptr' and 'ptrp' create a POST request with a simple or parameter-like body; +# * 'mptr' and 'fptr' create a POST request to submit a form with a text or file field (multipart/form-data); + +POST http://localhost:8080/ +Accept: application/json +Cache-Control: no-cache +Content-Type: application/cloudevents+json + +{ + "contenttype": "application/json", + "data": { + "sliProvider": "prometheus", + "project": "sockshop", + "service": "carts", + "stage": "dev", + "start": "2019-10-20T07:57:27.152330783Z", + "end": "2019-10-22T08:57:27.152330783Z", + "indicators": ["throughput", "error_rate", "request_latency_p50", "request_latency_p90", "request_latency_p95"] + }, + "id": "b3c3c357-eb3d-4f90-b26e-9ebfedfb8dbf", + "source": "jmeter-service", + "specversion": "0.2", + "time": "2019-10-14T08:00:09.416Z", + "type": "sh.keptn.internal.event.get-sli", + "shkeptncontext": "71270488-b923-400a-8ac5-7f471b15a181" +} +### + +POST http://localhost:8081/ +Accept: application/json +Cache-Control: no-cache +Content-Type: application/cloudevents+json + +{ + "contenttype": "application/json", + "data": { + "sliProvider": "prometheus", + "project": "sockshop", + "service": "carts", + "stage": "dev", + "start": "2019-10-20T07:57:27.152330783Z", + "end": "2019-10-22T08:57:27.152330783Z", + "indicators": ["throughput", "error_rate", "request_latency_p50", "request_latency_p90", "request_latency_p95"], + "customFilters": [ + { + "key": "handler", + "value": "=~.+ItemsController" + } + ] + }, + "id": "b3c3c357-eb3d-4f90-b26e-9ebfedfb8dbf", + "source": "jmeter-service", + "specversion": "0.2", + "time": "2019-10-14T08:00:09.416Z", + "type": "sh.keptn.internal.event.get-sli", + "shkeptncontext": "71270488-b923-400a-8ac5-7f471b15a181" +} + +### + + + + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1322613 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module github.com/keptn-contrib/prometheus-sli-service + +go 1.12 + +require ( + github.com/cloudevents/sdk-go v0.10.0 + github.com/google/uuid v1.1.1 + github.com/kelseyhightower/envconfig v1.4.0 + github.com/keptn/go-utils v0.4.0 + github.com/stretchr/testify v1.4.0 + golang.org/x/net v0.0.0-20191021144547-ec77196f6094 + gopkg.in/yaml.v2 v2.2.4 + k8s.io/apimachinery v0.0.0-20190313205120-d7deff9243b1 + k8s.io/client-go v11.0.0+incompatible +) + +replace github.com/cloudevents/sdk-go => github.com/cloudevents/sdk-go v0.0.0-20190509003705-56931988abe3 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5b8f9d8 --- /dev/null +++ b/go.sum @@ -0,0 +1,439 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +contrib.go.opencensus.io/exporter/ocagent v0.4.12 h1:jGFvw3l57ViIVEPKKEUXPcLYIXJmQxLUh6ey1eJhwyc= +contrib.go.opencensus.io/exporter/ocagent v0.4.12/go.mod h1:450APlNTSR6FrvC3CTRqYosuDstRB9un7SOx2k/9ckA= +github.com/Azure/azure-sdk-for-go v28.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/go-autorest/autorest v0.1.0/go.mod h1:AKyIcETwSUFxIcs/Wnq/C+kwCtlEYGUVd7FPNb2slmg= +github.com/Azure/go-autorest/autorest v0.2.0 h1:zBtSTOQTtjzHVRe+mhkiHvHwRTKHhjBEyo1m6DfI3So= +github.com/Azure/go-autorest/autorest v0.2.0/go.mod h1:AKyIcETwSUFxIcs/Wnq/C+kwCtlEYGUVd7FPNb2slmg= +github.com/Azure/go-autorest/autorest/adal v0.1.0 h1:RSw/7EAullliqwkZvgIGDYZWQm1PGKXI8c4aY/87yuU= +github.com/Azure/go-autorest/autorest/adal v0.1.0/go.mod h1:MeS4XhScH55IST095THyTxElntu7WqB7pNbZo8Q5G3E= +github.com/Azure/go-autorest/autorest/date v0.1.0 h1:YGrhWfrgtFs84+h0o46rJrlmsZtyZRg470CqAXTZaGM= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/to v0.1.0/go.mod h1:GunWKJp1AEqgMaGLV+iocmRAJWqST1wQYhyyjXJ3SJc= +github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQvokg3NZAlQTalVMtOIAs1aGK7G6u8= +github.com/Azure/go-autorest/logger v0.1.0 h1:ruG4BSDXONFRrZZJ2GUXDiUyVpayPmb1GnWeHDdaNKY= +github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/tracing v0.1.0 h1:TRBxC5Pj/fIuh4Qob0ZpkggbfT8RC0SubHbpV3p4/Vc= +github.com/Azure/go-autorest/tracing v0.1.0/go.mod h1:ROEEAFwXycQw7Sn3DXNtEedEvdeRAgDr0izn4z5Ij88= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= +github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= +github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/census-instrumentation/opencensus-proto v0.2.0 h1:LzQXZOgg4CQfE6bFvXGM30YZL1WW/M337pXml+GrcZ4= +github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudevents/sdk-go v0.0.0-20190509003705-56931988abe3 h1:DNM19kh6j6qGBx/FI7OmHKBL2vCW1eN28ESYK1+O5DY= +github.com/cloudevents/sdk-go v0.0.0-20190509003705-56931988abe3/go.mod h1:j1nZWMLGg3om8SswStBoY6/SHvcLM19MuZqwDtMtmzs= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/cyphar/filepath-securejoin v0.2.2 h1:jCwT2GTP+PY5nBz3c/YL5PAIbusElVrPujOBSCj8xRg= +github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= +github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= +github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= +github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/analysis v0.19.4 h1:1TjOzrWkj+9BrjnM1yPAICbaoC0FyfD49oVkTBrSSa0= +github.com/go-openapi/analysis v0.19.4/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.19.2 h1:a2kIyV3w+OS3S97zxUndRVD46+FhGOUBDFY7nmu4CsY= +github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.19.2 h1:o20suLFB4Ri0tuzpWtyHlh7E7HnkqTNLq6aR6WVNS1w= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.2 h1:rf5ArTHmIJxyV5Oiks+Su0mUens1+AjpkPoWr5xFRcI= +github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= +github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= +github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= +github.com/go-openapi/runtime v0.19.4 h1:csnOgcgAiuGoM/Po7PEpKDoNulCcF3FGbSnbHfxgjMI= +github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= +github.com/go-openapi/spec v0.19.3 h1:0XRyw8kguri6Yw4SxhsQA/atC88yqrk0+G4YhI2wabc= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= +github.com/go-openapi/strfmt v0.19.2/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= +github.com/go-openapi/strfmt v0.19.3 h1:eRfyY5SkaNJCAwmmMcADjY31ow9+N7MCLW7oRkbsINA= +github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= +github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= +github.com/go-openapi/validate v0.19.4 h1:LGjO87VyXY3bIKjlYpXSFuLRG2mTeuYlZyeNwFFWpyM= +github.com/go-openapi/validate v0.19.4/go.mod h1:BkJ0ZmXui7yB0bJXWSXgLPNTmbLVeX/3D1xn/N9mMUM= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.3.1 h1:WeAefnSUHlBb0iJKwxFDZdbfGwkd7xRNuV+IpXMJhYk= +github.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU= +github.com/gophercloud/gophercloud v0.6.0 h1:Xb2lcqZtml1XjgYZxbeayEemq7ASbeTp09m36gQFpEU= +github.com/gophercloud/gophercloud v0.6.0/go.mod h1:GICNByuaEBibcjmjvI7QvYJSZEbGkcYwAR7EZK2WMqM= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway v1.8.5 h1:2+KSC78XiO6Qy0hIjfc1OD9H+hsaJdJlb8Kqsd41CTE= +github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0= +github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= +github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= +github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/jarcoal/httpmock v1.0.4/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kelseyhightower/envconfig v1.3.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/keptn/go-utils v0.0.0-20191023080314-74864b263dd5/go.mod h1:YwobU0bBWWvx1b7N++ohTcYyEuz7VZjlbjkZAhzBiS8= +github.com/keptn/go-utils v0.0.0-20191030112956-7e1cba200072 h1:JMzslHnHUWS+aIzto+uycnPyOzVmshbxjKC+k/xuMr4= +github.com/keptn/go-utils v0.0.0-20191030112956-7e1cba200072/go.mod h1:R9a1HXkD+KCrhMFbLcEhtBtHGfYwXhO6dwZzmyoE98c= +github.com/keptn/go-utils v0.0.0-20191030134731-341284448a02 h1:HYv+BJx9h4Dd+prE+D+pMYkIUb9mMiRPWWWnqm2RQ3w= +github.com/keptn/go-utils v0.0.0-20191030134731-341284448a02/go.mod h1:R9a1HXkD+KCrhMFbLcEhtBtHGfYwXhO6dwZzmyoE98c= +github.com/keptn/go-utils v0.3.0 h1:0Gm3Kmv3EXmlq1i3YcHiwLs9j6wCw0COe+IR2pNKq5k= +github.com/keptn/go-utils v0.3.1-0.20191111100301-bff0cac85494 h1:iZ//qDbwRLRx41VbmPJQclhDNPK9QGuwe0K7mDHfsGQ= +github.com/keptn/go-utils v0.3.1-0.20191111100301-bff0cac85494/go.mod h1:R9a1HXkD+KCrhMFbLcEhtBtHGfYwXhO6dwZzmyoE98c= +github.com/keptn/go-utils v0.3.1-0.20191112090613-cf874a31b830 h1:J9CNDME07rc2w9RzP5O/8FSif2SMOfR+vTJJ7NOCAgw= +github.com/keptn/go-utils v0.3.1-0.20191112090613-cf874a31b830/go.mod h1:R9a1HXkD+KCrhMFbLcEhtBtHGfYwXhO6dwZzmyoE98c= +github.com/keptn/go-utils v0.4.0 h1:rVIXiYr05moKJIyorULbC3F/UcmKK7Yr3uNvh6pb5xQ= +github.com/keptn/go-utils v0.4.0/go.mod h1:R9a1HXkD+KCrhMFbLcEhtBtHGfYwXhO6dwZzmyoE98c= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/nats-io/gnatsd v1.4.1/go.mod h1:nqco77VO78hLCJpIcVfygDP2rPGfsEHkGTUk94uh5DQ= +github.com/nats-io/go-nats v1.7.0/go.mod h1:+t7RHT5ApZebkrQdnn6AhQJmhJJiKAvJUio1PiiCtj0= +github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7TDb/4= +github.com/nats-io/nuid v1.0.0/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= +github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= +github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= +go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.1.1 h1:Sq1fR+0c58RME5EoqKdjkiQAmPjmfHlZOoRI6fTUOcs= +go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2 h1:NAfh7zF0/3/HqtMvJNZ/RFrSlCE6ZTlHmKfhL/Dm1Jk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191021144547-ec77196f6094 h1:5O4U9trLjNpuhpynaDsqwCk+Tw6seqJz1EbqbnzHrc8= +golang.org/x/net v0.0.0-20191021144547-ec77196f6094/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190219203350-90b0e4468f99/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e h1:D5TXcfTk7xF7hvieo4QErS3qqCB4teTffacDWr7CI+0= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191023065245-6d3f0bb11be5 h1:KvJW0HDFZT3ZQ2kTXZ0xHbHm2BMbdJV4ff3petXiuxI= +golang.org/x/time v0.0.0-20191023065245-6d3f0bb11be5/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= +google.golang.org/api v0.3.1 h1:oJra/lMfmtm13/rgY/8i3MzjFWYXvQIAKjQ3HqofMk8= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19 h1:Lj2SnHtxkRGJDqnGaSjo+CCdIieEnwVazbOXILwQemk= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.19.1 h1:TrBcJ1yqAl1G++wO39nD/qtgpsW9/1+QGrluyMGEYgM= +google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= +gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= +gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= +gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE= +gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/api v0.0.0-20190313235455-40a48860b5ab h1:DG9A67baNpoeweOy2spF1OWHhnVY5KR7/Ek/+U1lVZc= +k8s.io/api v0.0.0-20190313235455-40a48860b5ab/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA= +k8s.io/api v0.0.0-20190606204050-af9c91bd2759/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA= +k8s.io/apimachinery v0.0.0-20190313205120-d7deff9243b1 h1:IS7K02iBkQXpCeieSiyJjGoLSdVOv2DbPaWHJ+ZtgKg= +k8s.io/apimachinery v0.0.0-20190313205120-d7deff9243b1/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0= +k8s.io/apimachinery v0.0.0-20190404173353-6a84e37a896d/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0= +k8s.io/client-go v11.0.0+incompatible h1:LBbX2+lOwY9flffWlJM7f1Ct8V2SRNiMRDFeiwnJo9o= +k8s.io/client-go v11.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/helm v2.12.3+incompatible/go.mod h1:LZzlS4LQBHfciFOurYBFkCMTaZ0D1l+p0teMg7TSULI= +k8s.io/helm v2.14.3+incompatible h1:uzotTcZXa/b2SWVoUzM1xiCXVjI38TuxMujS/1s+3Gw= +k8s.io/helm v2.14.3+incompatible/go.mod h1:LZzlS4LQBHfciFOurYBFkCMTaZ0D1l+p0teMg7TSULI= +k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/utils v0.0.0-20191010214722-8d271d903fe4 h1:Gi+/O1saihwDqnlmC8Vhv1M5Sp4+rbOmK9TbsLn8ZEA= +k8s.io/utils v0.0.0-20191010214722-8d271d903fe4/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +pack.ag/amqp v0.11.0/go.mod h1:4/cbmt4EJXSKlG6LCfWHoqmN0uFdy5i/+YFz+fTfhV4= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= +sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/lib/prometheus/prometheus.go b/lib/prometheus/prometheus.go new file mode 100644 index 0000000..0aeaf72 --- /dev/null +++ b/lib/prometheus/prometheus.go @@ -0,0 +1,350 @@ +package prometheus + +import ( + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "math" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + keptnevents "github.com/keptn/go-utils/pkg/events" +) + +const Throughput = "throughput" +const ErrorRate = "error_rate" +const RequestLatencyP50 = "request_latency_p50" +const RequestLatencyP90 = "request_latency_p90" +const RequestLatencyP95 = "request_latency_p95" + +type prometheusResponse struct { + Status string `json:"status"` + Data struct { + ResultType string `json:"resultType"` + Result []struct { + Metric struct { + } `json:"metric"` + Value []interface{} `json:"value"` + } `json:"result"` + } `json:"data"` +} + +// Handler interacts with a prometheus API endpoint +type Handler struct { + ApiURL string + Username string + Password string + Project string + Stage string + Service string + HTTPClient *http.Client + CustomFilters []*keptnevents.SLIFilter + CustomQueries map[string]string +} + +// NewPrometheusHandler returns a new prometheus handler that interacts with the Prometheus REST API +func NewPrometheusHandler(apiURL string, project string, stage string, service string, customFilters []*keptnevents.SLIFilter) *Handler { + ph := &Handler{ + ApiURL: apiURL, + Project: project, + Stage: stage, + Service: service, + HTTPClient: &http.Client{}, + CustomFilters: customFilters, + } + + return ph +} + +func (ph *Handler) GetSLIValue(metric string, start string, end string) (float64, error) { + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + + startUnix, err := parseUnixTimestamp(start) + if err != nil { + return 0, err + } + endUnix, _ := parseUnixTimestamp(end) + if err != nil { + return 0, err + } + query, err := ph.getMetricQuery(metric, startUnix, endUnix) + if err != nil { + return 0, err + } + queryString := ph.ApiURL + "/api/v1/query?query=" + url.QueryEscape(query) + "&time=" + strconv.FormatInt(endUnix.Unix(), 10) + req, err := http.NewRequest("GET", queryString, nil) + req.Header.Set("Content-Type", "application/json") + + resp, err := ph.HTTPClient.Do(req) + if err != nil { + return 0, err + } + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + if resp.StatusCode != 200 { + return 0, errors.New("metric could not be received") + } + + prometheusResult := &prometheusResponse{} + + err = json.Unmarshal(body, prometheusResult) + if err != nil { + return 0, err + } + + if len(prometheusResult.Data.Result) == 0 || len(prometheusResult.Data.Result[0].Value) == 0 { + // for the error rate query, the result is received with no value if the error rate is 0, so we have to assume that's OK at this point + return 0, nil + } + + parsedValue := fmt.Sprintf("%v", prometheusResult.Data.Result[0].Value[1]) + floatValue, err := strconv.ParseFloat(parsedValue, 64) + if err != nil { + return 0, nil + } + return floatValue, nil +} + +func (ph *Handler) getMetricQuery(metric string, start time.Time, end time.Time) (string, error) { + query := ph.CustomQueries[metric] + if query != "" { + query = ph.replaceQueryParameters(query, start, end) + + return query, nil + } + switch metric { + case Throughput: + return ph.getThroughputQuery(start, end), nil + case ErrorRate: + return ph.getErrorRateQuery(start, end), nil + case RequestLatencyP50: + return ph.getRequestLatencyQuery("50", start, end), nil + case RequestLatencyP90: + return ph.getRequestLatencyQuery("90", start, end), nil + case RequestLatencyP95: + return ph.getRequestLatencyQuery("95", start, end), nil + default: + return "", errors.New("unsupported SLI") + } +} + +func (ph *Handler) getThroughputQuery(start time.Time, end time.Time) string { + if ph.CustomQueries != nil && ph.CustomQueries["throughput"] != "" { + query := ph.CustomQueries["throughput"] + query = ph.replaceQueryParameters(query, start, end) + return query + } + return ph.getDefaultThroughputQuery(start, end) +} + +func (ph *Handler) replaceQueryParameters(query string, start time.Time, end time.Time) string { + for _, filter := range ph.CustomFilters { + filter.Value = strings.Replace(filter.Value, "'", "", -1) + filter.Value = strings.Replace(filter.Value, "\"", "", -1) + query = strings.Replace(query, "$"+filter.Key, filter.Value, -1) + query = strings.Replace(query, "$"+strings.ToUpper(filter.Key), filter.Value, -1) + } + query = strings.Replace(query, "$PROJECT", ph.Project, -1) + query = strings.Replace(query, "$STAGE", ph.Stage, -1) + query = strings.Replace(query, "$SERVICE", ph.Service, -1) + query = strings.Replace(query, "$project", ph.Project, -1) + query = strings.Replace(query, "$stage", ph.Stage, -1) + query = strings.Replace(query, "$service", ph.Service, -1) + durationString := strconv.FormatInt(getDurationInSeconds(start, end), 10) + "s" + + query = strings.Replace(query, "$DURATION_SECONDS", durationString, -1) + return query +} + +func (ph *Handler) getDefaultThroughputQuery(start time.Time, end time.Time) string { + filterExpr := ph.getDefaultFilterExpression() + durationString := strconv.FormatInt(getDurationInSeconds(start, end), 10) + "s" + // e.g. sum(rate(http_requests_total{job="carts-sockshop-dev"}[30m]))&time=1571649085 + /* + { + "status": "success", + "data": { + "resultType": "vector", + "result": [ + { + "metric": {}, + "value": [ + 1571649085, + "0.20111420612813372" + ] + } + ] + } + } + */ + return "sum(rate(http_requests_total{" + filterExpr + "}[" + durationString + "]))" +} + +func (ph *Handler) getErrorRateQuery(start time.Time, end time.Time) string { + if ph.CustomQueries != nil && ph.CustomQueries["error_rate"] != "" { + query := ph.CustomQueries["error_rate"] + query = ph.replaceQueryParameters(query, start, end) + return query + } + return ph.getDefaultErrorRateQuery(start, end) +} + +func (ph *Handler) getDefaultErrorRateQuery(start time.Time, end time.Time) string { + filterExpr := ph.getDefaultFilterExpression() + durationString := strconv.FormatInt(getDurationInSeconds(start, end), 10) + "s" + // e.g. sum(rate(http_requests_total{job="carts-sockshop-dev",status!~'2..'}[30m]))/sum(rate(http_requests_total{job="carts-sockshop-dev"}[30m]))&time=1571649085 + /* + with value: + { + "status": "success", + "data": { + "resultType": "vector", + "result": [ + { + "metric": {}, + "value": [ + 1571649085, + "1.00505917125441" + ] + } + ] + } + } + + no value (error rate 0): + { + "status": "success", + "data": { + "resultType": "vector", + "result": [] + } + } + */ + return "sum(rate(http_requests_total{" + filterExpr + ",status!~'2..'}[" + durationString + "]))/sum(rate(http_requests_total{" + filterExpr + "}[" + durationString + "]))" +} + +func (ph *Handler) getRequestLatencyQuery(percentile string, start time.Time, end time.Time) string { + if ph.CustomQueries != nil { + query := "" + switch percentile { + case "50": + query = ph.CustomQueries["response_time_p50"] + break + case "90": + query = ph.CustomQueries["response_time_p90"] + break + case "95": + query = ph.CustomQueries["response_time_p95"] + break + default: + query = "" + } + if query != "" { + query = ph.replaceQueryParameters(query, start, end) + return query + } + } + return ph.getDefaultRequestLatencyQuery(start, end, percentile) +} + +func (ph *Handler) getDefaultRequestLatencyQuery(start time.Time, end time.Time, percentile string) string { + filterExpr := ph.getDefaultFilterExpression() + durationString := strconv.FormatInt(getDurationInSeconds(start, end), 10) + "s" + // e.g. histogram_quantile(0.95, sum(rate(http_response_time_milliseconds_bucket{job='carts-sockshop-dev'}[30m])) by (le))&time=1571649085 + /* + { + "status": "success", + "data": { + "resultType": "vector", + "result": [ + { + "metric": {}, + "value": [ + 1571649085, + "4.607481671642585" + ] + } + ] + } + } + */ + return "histogram_quantile(0." + percentile + ",sum(rate(http_response_time_milliseconds_bucket{" + filterExpr + "}[" + durationString + "]))by(le))" +} + +func (ph *Handler) getDefaultFilterExpression() string { + filterExpression := "" + jobFilterFound := false + if ph.CustomFilters != nil && len(ph.CustomFilters) > 0 { + for _, filter := range ph.CustomFilters { + if filter.Key == "job" { + jobFilterFound = true + } + /* if no operator has been included in the label filter, use exact matching (=), e.g. + e.g.: + key: handler + value: ItemsController + */ + if !strings.HasPrefix(filter.Value, "=") && !strings.HasPrefix(filter.Value, "!=") && !strings.HasPrefix(filter.Value, "=~") && !strings.HasPrefix(filter.Value, "!~") { + filter.Value = strings.Replace(filter.Value, "'", "", -1) + filter.Value = strings.Replace(filter.Value, "\"", "", -1) + if filterExpression != "" { + filterExpression = filterExpression + "," + filter.Key + "='" + filter.Value + "'" + } else { + filterExpression = filter.Key + "='" + filter.Value + "'" + } + + } else { + /* if a valid operator (=, !=, =~, !~) is prepended to the value, use that one + e.g.: + key: handler + value: !=HealthCheckController + + OR + + key: handler + value: =~.+ItemsController|.+VersionController + */ + filter.Value = strings.Replace(filter.Value, "\"", "'", -1) + if filterExpression != "" { + filterExpression = filterExpression + "," + filter.Key + filter.Value + } else { + filterExpression = filter.Key + filter.Value + } + } + } + } + if !jobFilterFound { + if filterExpression != "" { + filterExpression = "job='" + ph.Service + "-" + ph.Project + "-" + ph.Stage + "-canary'" + "," + filterExpression + } else { + filterExpression = "job='" + ph.Service + "-" + ph.Project + "-" + ph.Stage + "-canary'" + } + + } + return filterExpression +} + +func parseUnixTimestamp(timestamp string) (time.Time, error) { + parsedTime, err := time.Parse(time.RFC3339, timestamp) + if err == nil { + return parsedTime, nil + } + + timestampInt, err := strconv.ParseInt(timestamp, 10, 64) + if err != nil { + return time.Now(), err + } + unix := time.Unix(timestampInt, 0) + return unix, nil +} + +func getDurationInSeconds(start, end time.Time) int64 { + seconds := end.Sub(start).Seconds() + return int64(math.Ceil(seconds)) +} diff --git a/lib/prometheus/prometheus_test.go b/lib/prometheus/prometheus_test.go new file mode 100644 index 0000000..6c8f378 --- /dev/null +++ b/lib/prometheus/prometheus_test.go @@ -0,0 +1,375 @@ +package prometheus + +import ( + "github.com/stretchr/testify/assert" + "golang.org/x/net/context" + "net" + "net/http" + "net/http/httptest" + "strconv" + "strings" + "testing" + "time" + + keptnevents "github.com/keptn/go-utils/pkg/events" +) + +func testingHTTPClient(handler http.Handler) (*http.Client, func()) { + s := httptest.NewServer(handler) + + cli := &http.Client{ + Transport: &http.Transport{ + DialContext: func(_ context.Context, network, _ string) (net.Conn, error) { + return net.Dial(network, s.Listener.Addr().String()) + }, + }, + } + + return cli, s.Close +} + +func TestGetErrorRateQueryWithoutFilter(t *testing.T) { + ph := NewPrometheusHandler("prometheus", "sockshop", "dev", "carts", nil) + + start := time.Unix(1571649084, 0) + end := time.Unix(1571649085, 0) + query := ph.getErrorRateQuery(start, end) + + expectedQuery := "sum(rate(http_requests_total{job='carts-sockshop-dev-canary',status!~'2..'}[1s]))/sum(rate(http_requests_total{job='carts-sockshop-dev-canary'}[1s]))" + + if strings.Compare(strings.Replace(query, " ", "", -1), strings.Replace(expectedQuery, " ", "", -1)) != 0 { + t.Errorf("Expected query did not match: \n expected: " + expectedQuery + "\n got: " + query) + } +} + +func TestGetErrorRateQueryWithFilter(t *testing.T) { + + var customFilters []*keptnevents.SLIFilter + + customFilters = append(customFilters, &keptnevents.SLIFilter{ + Key: "handler", + Value: "=~'ItemsController'", + }) + + ph := NewPrometheusHandler("prometheus", "sockshop", "dev", "carts", customFilters) + + start := time.Unix(1571649084, 0) + end := time.Unix(1571649085, 0) + query := ph.getErrorRateQuery(start, end) + + expectedQuery := "sum(rate(http_requests_total{job='carts-sockshop-dev-canary',handler=~'ItemsController',status!~'2..'}[1s]))/sum(rate(http_requests_total{job='carts-sockshop-dev-canary',handler=~'ItemsController'}[1s]))" + + if strings.Compare(strings.Replace(query, " ", "", -1), strings.Replace(expectedQuery, " ", "", -1)) != 0 { + t.Errorf("Expected query did not match: \n expected: " + expectedQuery + "\n got: " + query) + } +} + +func TestGetCustomErrorRateQueryWithFilter(t *testing.T) { + + var customFilters []*keptnevents.SLIFilter + + customQueries := map[string]string{} + customQueries["error_rate"] = "sum(rate(my_custom_metric{job='$SERVICE-$PROJECT-$STAGE',handler=~'$HANDLER',status!~'2..'}[$DURATION_SECONDS]))/sum(rate(my_custom_metric{job='$SERVICE-$PROJECT-$STAGE',handler=~'$HANDLER'}[$DURATION_SECONDS]))" + + customFilters = append(customFilters, &keptnevents.SLIFilter{ + Key: "handler", + Value: "'ItemsController'", + }) + + ph := NewPrometheusHandler("prometheus", "sockshop", "dev", "carts", customFilters) + + ph.CustomQueries = customQueries + + start := time.Unix(1571649084, 0) + end := time.Unix(1571649085, 0) + query := ph.getErrorRateQuery(start, end) + + expectedQuery := "sum(rate(my_custom_metric{job='carts-sockshop-dev',handler=~'ItemsController',status!~'2..'}[1s]))/sum(rate(my_custom_metric{job='carts-sockshop-dev',handler=~'ItemsController'}[1s]))" + + if strings.Compare(strings.Replace(query, " ", "", -1), strings.Replace(expectedQuery, " ", "", -1)) != 0 { + t.Errorf("Expected query did not match: \n expected: " + expectedQuery + "\n got: " + query) + } +} + +func TestGetThroughputQuery(t *testing.T) { + ph := NewPrometheusHandler("prometheus", "sockshop", "dev", "carts", nil) + + start := time.Unix(1571649084, 0) + end := time.Unix(1571649085, 0) + query := ph.getThroughputQuery(start, end) + + expectedQuery := "sum(rate(http_requests_total{job='carts-sockshop-dev-canary'}[1s]))" + + if strings.Compare(strings.Replace(query, " ", "", -1), strings.Replace(expectedQuery, " ", "", -1)) != 0 { + t.Errorf("Expected query did not match: \n expected: " + expectedQuery + "\n got: " + query) + } +} + +func TestGetCustomThroughputQueryWithFilter(t *testing.T) { + + var customFilters []*keptnevents.SLIFilter + + customQueries := map[string]string{} + customQueries["throughput"] = "rate(my_custom_metric{job='$SERVICE-$PROJECT-$STAGE',handler=~'$HANDLER'}[$DURATION_SECONDS])" + + customFilters = append(customFilters, &keptnevents.SLIFilter{ + Key: "handler", + Value: "'ItemsController'", + }) + + ph := NewPrometheusHandler("prometheus", "sockshop", "dev", "carts", customFilters) + ph.CustomQueries = customQueries + + start := time.Unix(1571649084, 0) + end := time.Unix(1571649085, 0) + query := ph.getThroughputQuery(start, end) + + expectedQuery := "rate(my_custom_metric{job='carts-sockshop-dev',handler=~'ItemsController'}[1s])" + + if strings.Compare(strings.Replace(query, " ", "", -1), strings.Replace(expectedQuery, " ", "", -1)) != 0 { + t.Errorf("Expected query did not match: \n expected: " + expectedQuery + "\n got: " + query) + } +} + +func TestGetRequestLatencyQuery(t *testing.T) { + ph := NewPrometheusHandler("prometheus", "sockshop", "dev", "carts", nil) + + start := time.Unix(1571649084, 0) + end := time.Unix(1571649085, 0) + query := ph.getRequestLatencyQuery("95", start, end) + + expectedQuery := "histogram_quantile(0.95,sum(rate(http_response_time_milliseconds_bucket{job='carts-sockshop-dev-canary'}[1s]))by(le))" + + if strings.Compare(strings.Replace(query, " ", "", -1), strings.Replace(expectedQuery, " ", "", -1)) != 0 { + t.Errorf("Expected query did not match: \n expected: " + expectedQuery + "\n got: " + query) + } +} + +func TestGetCustomResponseTimeQueryWithFilter(t *testing.T) { + + var customFilters []*keptnevents.SLIFilter + + customQueries := map[string]string{} + customQueries["response_time_p50"] = "histogram_quantile(0.50,sum(rate(my_custom_response_time_metric{job='$SERVICE-$PROJECT-$STAGE'}[$DURATION_SECONDS]))by(le))" + + customFilters = append(customFilters, &keptnevents.SLIFilter{ + Key: "handler", + Value: "'ItemsController'", + }) + + ph := NewPrometheusHandler("prometheus", "sockshop", "dev", "carts", customFilters) + + ph.CustomQueries = customQueries + + start := time.Unix(1571649084, 0) + end := time.Unix(1571649085, 0) + query := ph.getRequestLatencyQuery("50", start, end) + + expectedQuery := "histogram_quantile(0.50,sum(rate(my_custom_response_time_metric{job='carts-sockshop-dev'}[1s]))by(le))" + + if strings.Compare(strings.Replace(query, " ", "", -1), strings.Replace(expectedQuery, " ", "", -1)) != 0 { + t.Errorf("Expected query did not match: \n expected: " + expectedQuery + "\n got: " + query) + } +} + +func TestGetCustomQuery(t *testing.T) { + + customQueries := map[string]string{} + customQueries["custom_query"] = "my_custom_query{job='$SERVICE-$PROJECT-$STAGE'}[$DURATION_SECONDS]" + + ph := NewPrometheusHandler("prometheus", "sockshop", "dev", "carts", nil) + + ph.CustomQueries = customQueries + + start := time.Unix(1571649084, 0) + end := time.Unix(1571649085, 0) + query, _ := ph.getMetricQuery("custom_query", start, end) + + expectedQuery := "my_custom_query{job='carts-sockshop-dev'}[1s]" + + if strings.Compare(strings.Replace(query, " ", "", -1), strings.Replace(expectedQuery, " ", "", -1)) != 0 { + t.Errorf("Expected query did not match: \n expected: " + expectedQuery + "\n got: " + query) + } +} + +func TestGetDefaultFilterExpression(t *testing.T) { + + var customFilters []*keptnevents.SLIFilter + + customFilters = append(customFilters, &keptnevents.SLIFilter{ + Key: "handler", + Value: "ItemsController", + }) + + ph := NewPrometheusHandler("prometheus", "sockshop", "dev", "carts", customFilters) + + filterExpression := ph.getDefaultFilterExpression() + + expectedFilterExpression := "job='carts-sockshop-dev-canary',handler='ItemsController'" + + if strings.Compare(strings.Replace(expectedFilterExpression, " ", "", -1), strings.Replace(filterExpression, " ", "", -1)) != 0 { + t.Errorf("Expected query did not match: \n expected: " + expectedFilterExpression + "\n got: " + filterExpression) + } +} + +func TestGetDefaultFilterExpressionWithOperand(t *testing.T) { + + var customFilters []*keptnevents.SLIFilter + + customFilters = append(customFilters, &keptnevents.SLIFilter{ + Key: "handler", + Value: "!='ItemsController'", + }) + + ph := NewPrometheusHandler("prometheus", "sockshop", "dev", "carts", customFilters) + + filterExpression := ph.getDefaultFilterExpression() + + expectedFilterExpression := "job='carts-sockshop-dev-canary',handler!='ItemsController'" + + if strings.Compare(strings.Replace(expectedFilterExpression, " ", "", -1), strings.Replace(filterExpression, " ", "", -1)) != 0 { + t.Errorf("Expected query did not match: \n expected: " + expectedFilterExpression + "\n got: " + filterExpression) + } +} + +func TestGetDefaultFilterExpressionWithJobName(t *testing.T) { + + var customFilters []*keptnevents.SLIFilter + + customFilters = append(customFilters, &keptnevents.SLIFilter{ + Key: "job", + Value: "my-job", + }) + + ph := NewPrometheusHandler("prometheus", "sockshop", "dev", "carts", customFilters) + + filterExpression := ph.getDefaultFilterExpression() + + expectedFilterExpression := "job='my-job'" + + if strings.Compare(strings.Replace(expectedFilterExpression, " ", "", -1), strings.Replace(filterExpression, " ", "", -1)) != 0 { + t.Errorf("Expected query did not match: \n expected: " + expectedFilterExpression + "\n got: " + filterExpression) + } +} + +func TestGetDefaultFilterExpressionWithSingleQuote(t *testing.T) { + + var customFilters []*keptnevents.SLIFilter + + customFilters = append(customFilters, &keptnevents.SLIFilter{ + Key: "handler", + Value: "'ItemsController'", + }) + + ph := NewPrometheusHandler("prometheus", "sockshop", "dev", "carts", customFilters) + + filterExpression := ph.getDefaultFilterExpression() + + expectedFilterExpression := "job='carts-sockshop-dev-canary',handler='ItemsController'" + + if strings.Compare(strings.Replace(expectedFilterExpression, " ", "", -1), strings.Replace(filterExpression, " ", "", -1)) != 0 { + t.Errorf("Expected query did not match: \n expected: " + expectedFilterExpression + "\n got: " + filterExpression) + } +} + +func TestGetDefaultFilterExpressionWithDoubleQuote(t *testing.T) { + + var customFilters []*keptnevents.SLIFilter + + customFilters = append(customFilters, &keptnevents.SLIFilter{ + Key: "handler", + Value: "\"ItemsController\"", + }) + + ph := NewPrometheusHandler("prometheus", "sockshop", "dev", "carts", customFilters) + + filterExpression := ph.getDefaultFilterExpression() + + expectedFilterExpression := "job='carts-sockshop-dev-canary',handler='ItemsController'" + + if strings.Compare(strings.Replace(expectedFilterExpression, " ", "", -1), strings.Replace(filterExpression, " ", "", -1)) != 0 { + t.Errorf("Expected query did not match: \n expected: " + expectedFilterExpression + "\n got: " + filterExpression) + } +} + +func TestGetSLIValue(t *testing.T) { + + okResponse := `{ + "status": "success", + "data": { + "resultType": "vector", + "result": [ + { + "metric": {}, + "value": [ + 1571649085, + "0.20111420612813372" + ] + } + ] + } + }` + + h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(okResponse)) + }) + + httpClient, teardown := testingHTTPClient(h) + defer teardown() + + ph := NewPrometheusHandler("http://prometheus", "sockshop", "dev", "carts", nil) + ph.HTTPClient = httpClient + + start := strconv.FormatInt(time.Unix(1571649084, 0).UTC().UnixNano(), 10) + end := strconv.FormatInt(time.Unix(1571649085, 0).UTC().UnixNano(), 10) + value, _ := ph.GetSLIValue(Throughput, start, end) + + assert.EqualValues(t, value, 0.20111420612813372) +} + +func TestGetSLIValueWithEmptyResult(t *testing.T) { + + okResponse := `{ + "status": "success", + "data": { + "resultType": "vector", + "result": [] + } + }` + + h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(okResponse)) + }) + + httpClient, teardown := testingHTTPClient(h) + defer teardown() + + ph := NewPrometheusHandler("http://prometheus", "sockshop", "dev", "carts", nil) + ph.HTTPClient = httpClient + + start := strconv.FormatInt(time.Unix(1571649084, 0).UTC().UnixNano(), 10) + end := strconv.FormatInt(time.Unix(1571649085, 0).UTC().UnixNano(), 10) + value, _ := ph.GetSLIValue(Throughput, start, end) + + assert.EqualValues(t, value, 0.0) +} + +func TestGetSLIValueWithErrorResponse(t *testing.T) { + h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // w.Write([]byte(response)) + w.WriteHeader(http.StatusBadRequest) + }) + + httpClient, teardown := testingHTTPClient(h) + defer teardown() + + ph := NewPrometheusHandler("http://prometheus", "sockshop", "dev", "carts", nil) + ph.HTTPClient = httpClient + + start := strconv.FormatInt(time.Unix(1571649084, 0).UTC().UnixNano(), 10) + end := strconv.FormatInt(time.Unix(1571649085, 0).UTC().UnixNano(), 10) + value, err := ph.GetSLIValue(Throughput, start, end) + + assert.EqualValues(t, value, 0.0) + assert.NotNil(t, err, nil) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..5971625 --- /dev/null +++ b/main.go @@ -0,0 +1,346 @@ +package main + +import ( + "context" + "errors" + "fmt" + "github.com/keptn-contrib/prometheus-sli-service/lib/prometheus" + "gopkg.in/yaml.v2" + "log" + "net/url" + "os" + "strings" + "time" + + "github.com/cloudevents/sdk-go/pkg/cloudevents" + "github.com/cloudevents/sdk-go/pkg/cloudevents/client" + cloudeventshttp "github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http" + "github.com/cloudevents/sdk-go/pkg/cloudevents/types" + "github.com/google/uuid" + "github.com/kelseyhightower/envconfig" + keptnevents "github.com/keptn/go-utils/pkg/events" + keptnutils "github.com/keptn/go-utils/pkg/utils" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/client-go/kubernetes/typed/core/v1" +) + +const configservice = "CONFIGURATION_SERVICE" +const eventbroker = "EVENTBROKER" + +type envConfig struct { + // Port on which to listen for cloudevents + Port int `envconfig:"RCV_PORT" default:"8080"` + Path string `envconfig:"RCV_PATH" default:"/"` +} + +type prometheusCredentials struct { + URL string `json:"url" yaml:"url"` + User string `json:"user" yaml:"user"` + Password string `json:"password" yaml:"password"` +} + +func main() { + var env envConfig + if err := envconfig.Process("", &env); err != nil { + log.Fatalf("Failed to process env var: %s", err) + } + os.Exit(_main(os.Args[1:], env)) +} + +func _main(args []string, env envConfig) int { + + ctx := context.Background() + + t, err := cloudeventshttp.New( + cloudeventshttp.WithPort(env.Port), + cloudeventshttp.WithPath(env.Path), + ) + + if err != nil { + log.Fatalf("failed to create transport, %v", err) + } + c, err := client.New(t) + if err != nil { + log.Fatalf("failed to create client, %v", err) + } + + log.Fatalf("failed to start receiver: %s", c.StartReceiver(ctx, gotEvent)) + + return 0 +} + +func gotEvent(ctx context.Context, event cloudevents.Event) error { + + switch event.Type() { + case keptnevents.InternalGetSLIEventType: + return retrieveMetrics(event) // backwards compatibility to Keptn versions <= 0.5.x + default: + return errors.New("received unknown event type") + } +} + +func retrieveMetrics(event cloudevents.Event) error { + var shkeptncontext string + event.Context.ExtensionAs("shkeptncontext", &shkeptncontext) + eventData := &keptnevents.InternalGetSLIEventData{} + err := event.DataAs(eventData) + if err != nil { + return err + } + + // don't continue if SLIProvider != prometheus + if eventData.SLIProvider != "prometheus" { + return nil + } + + stdLogger := keptnutils.NewLogger(shkeptncontext, event.Context.GetID(), "prometheus-sli-service") + stdLogger.Info("Retrieving prometheus metrics") + kubeClient, err := keptnutils.GetKubeAPI(true) + if err != nil { + stdLogger.Error("could not create kube client") + return errors.New("could not create kube client") + } + prometheusApiURL, err := getPrometheusApiURL(eventData.Project, kubeClient, stdLogger) + + if err != nil { + return err + } + + // get custom metrics for Keptn installation + customQueries, err := getGlobalCustomQueries(kubeClient, stdLogger) + + if err != nil { + stdLogger.Error("Failed to get global custom queries") + stdLogger.Error(err.Error()) + return err + } + + // get custom metrics for project + projectCustomQueries, err := getCustomQueriesForProject(eventData.Project, kubeClient, stdLogger) + + if err != nil { + stdLogger.Error("Failed to get custom queries for project " + eventData.Project) + stdLogger.Error(err.Error()) + return err + } + + log.Printf("Custom Query Config\n") + + // make sure custom queries exists + if customQueries == nil { + customQueries = make(map[string]string) + } else { + for k, v := range customQueries { + log.Printf("\tFound custom query %s with value %s\n", k, v) + } + } + + if projectCustomQueries != nil { + log.Println("Merging custom queries with projectCustomQueries") + // merge global custom queries and project custom queries + for k, v := range projectCustomQueries { + // overwrite / append project custom query on global custom queries + customQueries[k] = v + log.Printf("\tOverwriting custom query %s with value %s\n", k, v) + } + } + + prometheusHandler := prometheus.NewPrometheusHandler(prometheusApiURL, eventData.Project, eventData.Stage, eventData.Service, eventData.CustomFilters) + + if customQueries != nil { + prometheusHandler.CustomQueries = customQueries + } + + var sliResults []*keptnevents.SLIResult + + for _, indicator := range eventData.Indicators { + stdLogger.Info("Fetching indicator: " + indicator) + sliValue, err := prometheusHandler.GetSLIValue(indicator, eventData.Start, eventData.End) + if err != nil { + sliResults = append(sliResults, &keptnevents.SLIResult{ + Metric: indicator, + Value: 0, + Success: false, + Message: err.Error(), + }) + } else { + sliResults = append(sliResults, &keptnevents.SLIResult{ + Metric: indicator, + Value: sliValue, + Success: true, + }) + } + } + return sendInternalGetSLIDoneEvent(shkeptncontext, eventData.Project, eventData.Service, eventData.Stage, + sliResults, eventData.Start, eventData.End, eventData.TestStrategy, eventData.DeploymentStrategy) +} + +const keptnPrometheusSliConfigMapName = "prometheus-sli-config" + +// Return Custom Queries for Keptn Installation +func getGlobalCustomQueries(kubeClient v1.CoreV1Interface, logger *keptnutils.Logger) (map[string]string, error) { + logger.Info(fmt.Sprintf("Checking for custom SLI queries for Keptn installation (querying %s)", keptnPrometheusSliConfigMapName)) + + configMap, err := kubeClient.ConfigMaps("keptn").Get(keptnPrometheusSliConfigMapName, metav1.GetOptions{}) + if err != nil { + logger.Info("No global custom queries defined") + return nil, nil + } + + customQueries := make(map[string]string) + err = yaml.Unmarshal([]byte(configMap.Data["custom-queries"]), &customQueries) + + if err != nil { + logger.Info("Global custom queries found, but could not parse them: " + err.Error()) + return nil, err + } + logger.Info("Global custom queries found and parsed") + return customQueries, nil +} + +// Return Custom Queries for Keptn Installation +func getCustomQueriesForProject(project string, kubeClient v1.CoreV1Interface, logger *keptnutils.Logger) (map[string]string, error) { + logger.Info(fmt.Sprintf("Checking for custom SLI queries for Keptn installation (querying %s)", keptnPrometheusSliConfigMapName)) + + configMap, err := kubeClient.ConfigMaps("keptn").Get(keptnPrometheusSliConfigMapName+"-"+project, metav1.GetOptions{}) + if err != nil { + logger.Info("No custom queries defined for project " + project) + return nil, nil + } + + customQueries := make(map[string]string) + err = yaml.Unmarshal([]byte(configMap.Data["custom-queries"]), &customQueries) + + if err != nil { + logger.Info("Project custom queries found, but could not parse them: " + err.Error()) + return nil, err + } + logger.Info("Project custom queries found and parsed") + return customQueries, nil +} + +func getPrometheusApiURL(project string, kubeClient v1.CoreV1Interface, logger *keptnutils.Logger) (string, error) { + logger.Info("Checking if external prometheus instance has been defined for project " + project) + // check if secret 'prometheus-credentials- exists + + secret, err := kubeClient.Secrets("keptn").Get("prometheus-credentials-"+project, metav1.GetOptions{}) + + // return cluster-internal prometheus URL if no secret has been found + if err != nil { + logger.Info("No external prometheus instance defined for project " + project + ". Using default: http://prometheus-service.monitoring.svc.cluster.local:8080") + return "http://prometheus-service.monitoring.svc.cluster.local:8080", nil + } + + /* + data format: + url: string + user: string + password: string + */ + pc := &prometheusCredentials{} + err = yaml.Unmarshal(secret.Data["prometheus-credentials"], pc) + + if err != nil { + logger.Error("Could not parse credentials for external prometheus instance: " + err.Error()) + return "", errors.New("invalid credentials format found in secret 'prometheus-credentials-" + project) + } + logger.Info("Using external prometheus instance for project " + project + ": " + pc.URL) + prometheusURL := generatePrometheusURL(pc) + + return prometheusURL, nil +} + +func generatePrometheusURL(pc *prometheusCredentials) string { + prometheusURL := pc.URL + + credentialsString := "" + + if pc.User != "" && pc.Password != "" { + credentialsString = url.QueryEscape(pc.User) + ":" + url.QueryEscape(pc.Password) + "@" + } + if strings.HasPrefix(prometheusURL, "https://") { + prometheusURL = strings.TrimPrefix(prometheusURL, "https://") + prometheusURL = "https://" + credentialsString + prometheusURL + } else if strings.HasPrefix(prometheusURL, "http://") { + prometheusURL = strings.TrimPrefix(prometheusURL, "http://") + prometheusURL = "http://" + credentialsString + prometheusURL + } else { + // assume https transport + prometheusURL = "https://" + credentialsString + prometheusURL + } + return strings.Replace(prometheusURL, " ", "", -1) +} + +func sendInternalGetSLIDoneEvent(shkeptncontext string, project string, + service string, stage string, indicatorValues []*keptnevents.SLIResult, start string, end string, testStrategy string, deploymentStrategy string) error { + + source, _ := url.Parse("prometheus-sli-service") + contentType := "application/json" + + getSLIEvent := keptnevents.InternalGetSLIDoneEventData{ + Project: project, + Service: service, + Stage: stage, + IndicatorValues: indicatorValues, + Start: start, + End: end, + TestStrategy: testStrategy, + DeploymentStrategy: deploymentStrategy, + } + event := cloudevents.Event{ + Context: cloudevents.EventContextV02{ + ID: uuid.New().String(), + Time: &types.Timestamp{Time: time.Now()}, + Type: keptnevents.InternalGetSLIDoneEventType, + Source: types.URLRef{URL: *source}, + ContentType: &contentType, + Extensions: map[string]interface{}{"shkeptncontext": shkeptncontext}, + }.AsV02(), + Data: getSLIEvent, + } + + return sendEvent(event) +} + +func sendEvent(event cloudevents.Event) error { + endPoint, err := getServiceEndpoint(eventbroker) + if err != nil { + return errors.New("Failed to retrieve endpoint of eventbroker. %s" + err.Error()) + } + + if endPoint.Host == "" { + return errors.New("Host of eventbroker not set") + } + + transport, err := cloudeventshttp.New( + cloudeventshttp.WithTarget(endPoint.String()), + cloudeventshttp.WithEncoding(cloudeventshttp.StructuredV02), + ) + if err != nil { + return errors.New("Failed to create transport:" + err.Error()) + } + + c, err := client.New(transport) + if err != nil { + return errors.New("Failed to create HTTP client:" + err.Error()) + } + + if _, err := c.Send(context.Background(), event); err != nil { + return errors.New("Failed to send cloudevent:, " + err.Error()) + } + return nil +} + +// getServiceEndpoint gets an endpoint stored in an environment variable and sets http as default scheme +func getServiceEndpoint(service string) (url.URL, error) { + url, err := url.Parse(os.Getenv(service)) + if err != nil { + return *url, fmt.Errorf("Failed to retrieve value from ENVIRONMENT_VARIABLE: %s", service) + } + + if url.Scheme == "" { + url.Scheme = "http" + } + + return *url, nil +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..ff787b3 --- /dev/null +++ b/main_test.go @@ -0,0 +1,60 @@ +package main + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +type test struct { + input prometheusCredentials + want string +} + +func TestGeneratePrometheusURL(t *testing.T) { + tests := []test{ + { + input: prometheusCredentials{ + URL: "http://prometheus", + User: "test", + Password: "test", + }, + want: "http://test:test@prometheus", + }, + { + input: prometheusCredentials{ + URL: "https://prometheus", + User: "test", + Password: "test", + }, + want: "https://test:test@prometheus", + }, + { + input: prometheusCredentials{ + URL: "prometheus", + User: "test", + Password: "test", + }, + want: "https://test:test@prometheus", + }, + { + input: prometheusCredentials{ + URL: " prometheus", + User: "test", + Password: "test", + }, + want: "https://test:test@prometheus", + }, + { + input: prometheusCredentials{ + URL: "prometheus", + User: "", + Password: "test", + }, + want: "https://prometheus", + }, + } + for _, test := range tests { + url := generatePrometheusURL(&test.input) + assert.EqualValues(t, test.want, url) + } +} diff --git a/misc/custom_queries.yaml b/misc/custom_queries.yaml new file mode 100644 index 0000000..38ef9bb --- /dev/null +++ b/misc/custom_queries.yaml @@ -0,0 +1,8 @@ +kind: ConfigMap +apiVersion: v1 +metadata: + name: prometheus-sli-config-sockshop + namespace: keptn +data: + custom-queries: | + cpu_usage: avg(rate(container_cpu_usage_seconds_total{namespace="$PROJECT-$STAGE",pod_name=~"$SERVICE-primary-.*"}[5m])) diff --git a/misc/mock_secret.yaml b/misc/mock_secret.yaml new file mode 100644 index 0000000..adb5c51 --- /dev/null +++ b/misc/mock_secret.yaml @@ -0,0 +1,5 @@ +# apply with kubectl create secret -n keptn generic prometheus-credentials-sockshop --from-file=prometheus-credentials=./mock_secret.yaml + +user: test +password: test +url: http://prometheus-service.monitoring.svc.cluster.local:8080 diff --git a/releasenotes/releasenotes_V0.1.0.md b/releasenotes/releasenotes_V0.1.0.md new file mode 100644 index 0000000..e1b2454 --- /dev/null +++ b/releasenotes/releasenotes_V0.1.0.md @@ -0,0 +1,17 @@ +# Release Notes 0.1.0 + +This service is used for retrieving metrics from a prometheus API endpoint. Per default, it fetches metrics from the prometheus instance set up by Keptn +(`prometheus-service.monitoring.svc.cluster.local:8080`), but it can also be configures to use any reachable Prometheus endpoint using basic authentication by providing the credentials +via a secret in the `keptn` namespace of the cluster. + +The supported default SLIs are: + + - throughput + - error_rate + - response_time_p50 + - response_time_p90 + - response_time_p95 + +The queries for those SLIs can be overridden by providing custom Prometheus queries. Similarly, it is also possible to add additional custom SLIs and their queries. +For detailed instructions, please head to the [README section](https://github.com/keptn-contrib/prometheus-sli-service/tree/0.1.0). + diff --git a/skaffold.yaml b/skaffold.yaml new file mode 100644 index 0000000..280cb3a --- /dev/null +++ b/skaffold.yaml @@ -0,0 +1,11 @@ +apiVersion: skaffold/v1beta13 +kind: Config +build: + artifacts: + - image: keptn/prometheus-sli-service + docker: # beta describes an artifact built from a Dockerfile. + dockerfile: Dockerfile +deploy: + kubectl: + manifests: + - deploy/service.yaml diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..fb6395b --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,13 @@ +sonar.projectKey=keptn-contrib_prometheus-sli-service +sonar.organization=keptn-contrib + +# this is the name and version displayed in the SonarCloud UI. +sonar.projectName=prometheus-sli-service +sonar.projectVersion=VERSION_PLACEHOLDER + +# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. +# This property is optional if sonar.modules is set. +sonar.sources=. + +# Encoding of the source code. Default is default system encoding +#sonar.sourceEncoding=UTF-8 \ No newline at end of file diff --git a/version b/version new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/version @@ -0,0 +1 @@ +0.1.0 diff --git a/writeManifest.sh b/writeManifest.sh new file mode 100755 index 0000000..68c5906 --- /dev/null +++ b/writeManifest.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +DATE="$(date +'%Y%m%d.%H%M')" +GIT_SHA="$(git rev-parse --short HEAD)" +REPO_URL=https://github.com/$TRAVIS_REPO_SLUG + +sed -i 's~MANIFEST_REPOSITORY~'"$REPO_URL"'~' MANIFEST +sed -i 's~MANIFEST_BRANCH~'"$TRAVIS_BRANCH"'~' MANIFEST +sed -i 's~MANIFEST_COMMIT~'"$TRAVIS_COMMIT"'~' MANIFEST +sed -i 's~MANIFEST_TRAVIS_JOB_URL~'"$TRAVIS_JOB_WEB_URL"'~' MANIFEST +sed -i 's~MANIFEST_DATE~'"$DATE"'~' MANIFEST \ No newline at end of file