diff --git a/.errcheck_excludes.txt b/.errcheck_excludes.txt new file mode 100644 index 00000000..e70a7f0d --- /dev/null +++ b/.errcheck_excludes.txt @@ -0,0 +1,2 @@ +(github.com/go-kit/kit/log.Logger).Log +(github.com/openshift/telemeter/vendor/github.com/go-kit/kit/log.Logger).Log diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..9a1fd28b --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +/telemeter-benchmark +/telemeter-client +/telemeter-server +/authorization-server +/_output +telemeter-bench +/bin +/benchmark +benchmark.pdf +/docs/telemeter_query + +# These are empty target files, created on every docker build. Their sole +# purpose is to track the last target execution time to evalualte, whether the +# container needds to be rebuild +.hack-*-image + +# ACM specific +.DS_Store +/build-harness +/build-harness-extensions +.build-harness-bootstrap diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..e3b5b257 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,14 @@ +linters: + # disable-all: true + # enable-all: true + disable: + - megacheck + enable: + # megacheck fails to respect build flags, causing compilation failure during linting. + # instead, use the unused, gosimple, and staticcheck linters directly + - gosimple + - staticcheck + - unused +linters-settings: + errcheck: + exclude: .errcheck_excludes.txt diff --git a/COMPONENT_NAME b/COMPONENT_NAME new file mode 100644 index 00000000..2e63dcb6 --- /dev/null +++ b/COMPONENT_NAME @@ -0,0 +1 @@ +metrics-collector diff --git a/COMPONENT_VERSION b/COMPONENT_VERSION new file mode 100644 index 00000000..7ec1d6db --- /dev/null +++ b/COMPONENT_VERSION @@ -0,0 +1 @@ +2.1.0 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..0cf7e610 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM openshift/origin-release:golang-1.13 +ENV GOFLAGS="-mod=vendor" +COPY . /go/src/github.com/openshift/telemeter +RUN cd /go/src/github.com/openshift/telemeter && \ + go build ./cmd/telemeter-client && \ + go build ./cmd/telemeter-server && \ + go build ./cmd/authorization-server + +FROM centos:7 +COPY --from=0 /go/src/github.com/openshift/telemeter/telemeter-client /usr/bin/ +COPY --from=0 /go/src/github.com/openshift/telemeter/telemeter-server /usr/bin/ +COPY --from=0 /go/src/github.com/openshift/telemeter/authorization-server /usr/bin/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..4519ef40 --- /dev/null +++ b/Makefile @@ -0,0 +1,252 @@ +# Copyright (c) 2020 Red Hat, Inc. + +include build/Configfile + +.PHONY: all build image check test-generate test-integration test-benchmark vendor dependencies manifests +SHELL=/usr/bin/env bash -o pipefail + +# Bootstrap (pull) the build harness + +# GITHUB_USER containing '@' char must be escaped with '%40' +export GITHUB_USER := $(shell echo $(GITHUB_USER) | sed 's/@/%40/g') +export GITHUB_TOKEN ?= + +USE_VENDORIZED_BUILD_HARNESS ?= + +ifndef USE_VENDORIZED_BUILD_HARNESS +-include $(shell curl -s -H 'Authorization: token ${GITHUB_TOKEN}' -H 'Accept: application/vnd.github.v4.raw' -L https://api.github.com/repos/open-cluster-management/build-harness-extensions/contents/templates/Makefile.build-harness-bootstrap -o .build-harness-bootstrap; echo .build-harness-bootstrap) +else +-include vbh/.build-harness-vendorized +endif + +GO_PKG=github.com/openshift/telemeter +REPO?=quay.io/openshift/telemeter +TAG?=$(shell git rev-parse --short HEAD) + +PKGS=$(shell go list ./... | grep -v -E '/vendor/|/test/(?!e2e)') +GOLANG_FILES:=$(shell find . -name \*.go -print) +FIRST_GOPATH:=$(firstword $(subst :, ,$(shell go env GOPATH))) +BIN_DIR?=$(shell pwd)/_output/bin +LIB_DIR?=./_output/lib +METRICS_JSON=./_output/metrics.json +GOLANGCI_LINT_BIN=$(BIN_DIR)/golangci-lint +GOLANGCI_LINT_VERSION=v1.18.0 +EMBEDMD_BIN=$(BIN_DIR)/embedmd +THANOS_BIN=$(BIN_DIR)/thanos +UP_BIN=$(BIN_DIR)/up +MEMCACHED_BIN=$(BIN_DIR)/memcached +PROMETHEUS_BIN=$(BIN_DIR)/prometheus +GOJSONTOYAML_BIN=$(BIN_DIR)/gojsontoyaml +JSONNET_BIN?=$(BIN_DIR)/jsonnet +# We need jsonnet on CI; here we default to the user's installed jsonnet binary; if nothing is installed, then install go-jsonnet. +JSONNET_LOCAL_OR_INSTALLED=$(if $(shell which jsonnet 2>/dev/null),$(shell which jsonnet 2>/dev/null),$(JSONNET_BIN)) +JB_BIN=$(BIN_DIR)/jb +JSONNET_SRC=$(shell find ./jsonnet -type f) +BENCHMARK_RESULTS=$(shell find ./benchmark -type f -name '*.json') +BENCHMARK_GOAL?=5000 +JSONNET_VENDOR=jsonnet/jsonnetfile.lock.json jsonnet/vendor +DOCS=$(shell grep -rlF [embedmd] docs) + +export PATH := $(BIN_DIR):$(PATH) + +GO_BUILD_RECIPE=GOOS=linux CGO_ENABLED=0 go build +CONTAINER_CMD:=docker run --rm \ + -u="$(shell id -u):$(shell id -g)" \ + -v "$(shell go env GOCACHE):/.cache/go-build" \ + -v "$(PWD):/go/src/$(GO_PKG):Z" \ + -w "/go/src/$(GO_PKG)" \ + -e GO111MODULE=on \ + quay.io/coreos/jsonnet-ci + +.PHONY: all +all: build manifests $(DOCS) + +.PHONY: clean +clean:: + # Remove all files and directories ignored by git. + git clean -Xfd . + + # Build harness cleanup + @rm -rf $(BUILD_DIR)/_output + @[ "$(BUILD_HARNESS_PATH)" == '/' ] || \ + [ "$(BUILD_HARNESS_PATH)" == '.' ] || \ + rm -rf $(BUILD_HARNESS_PATH) + +############ +# Building # +############ + +.PHONY: build-in-docker +build-in-docker: + $(CONTAINER_CMD) $(MAKE) $(MFLAGS) build + +.PHONY: build +build: + go build ./cmd/telemeter-client + go build ./cmd/telemeter-server + go build ./cmd/authorization-server + go build ./cmd/telemeter-benchmark + +.PHONY: image +image: .hack-operator-image + +.hack-operator-image: Dockerfile +# Create empty target file, for the sole purpose of recording when this target +# was last executed via the last-modification timestamp on the file. See +# https://www.gnu.org/software/make/manual/make.html#Empty-Targets + docker build -t $(REPO):$(TAG) . + touch $@ + +############## +# Generating # +############## + +vendor: + go mod vendor + go mod tidy + go mod verify + +.PHONY: generate +generate: $(DOCS) manifests + +.PHONY: generate-in-docker +generate-in-docker: + $(CONTAINER_CMD) $(MAKE) $(MFLAGS) generate + +$(JSONNET_VENDOR): $(JB_BIN) jsonnet/jsonnetfile.json + cd jsonnet && $(JB_BIN) install + +# Can't add test/timeseries.txt as a dependency, otherwise +# running make --always-make will try to regenerate the timeseries +# on CI, which will fail because there is no OpenShift cluster. +$(DOCS): $(JSONNET_SRC) $(EMBEDMD_BIN) docs/telemeter_query + $(EMBEDMD_BIN) -w $@ + +docs/telemeter_query: $(JSONNET_SRC) + query=""; \ + for rule in $$(jsonnet metrics.json | jq -r '.[]'); do \ + [ ! -z "$$query" ] && query="$$query or "; \ + query="$$query$$rule"; \ + done; \ + echo "$$query" > $@ + +$(METRICS_JSON): + curl -L https://raw.githubusercontent.com/openshift/cluster-monitoring-operator/844e7afabfcfa4162c716ea18cd8e2d010789de1/manifests/0000_50_cluster_monitoring_operator_04-config.yaml | \ + $(GOJSONTOYAML_BIN) --yamltojson | jq -r '.data."metrics.yaml"' | $(GOJSONTOYAML_BIN) --yamltojson | jq -r '.matches' > $@ + +manifests: $(JSONNET_LOCAL_OR_INSTALLED) $(JSONNET_SRC) $(JSONNET_VENDOR) $(GOJSONTOYAML_BIN) $(METRICS_JSON) + rm -rf manifests + mkdir -p manifests/{benchmark,client} + $(JSONNET_LOCAL_OR_INSTALLED) jsonnet/benchmark.jsonnet -J jsonnet/vendor -m manifests/benchmark --tla-code metrics="$$(cat $(METRICS_JSON))" + $(JSONNET_LOCAL_OR_INSTALLED) jsonnet/client.jsonnet -J jsonnet/vendor -m manifests/client + @for f in $$(find manifests -type f); do\ + cat $$f | $(GOJSONTOYAML_BIN) > $$f.yaml && rm $$f;\ + done + +benchmark.pdf: $(BENCHMARK_RESULTS) + find ./benchmark -type f -name '*.json' -print0 | xargs -l -0 python3 test/plot.py && gs -dBATCH -dNOPAUSE -q -sDEVICE=pdfwrite -sOutputFile=$@ benchmark/*.pdf + + +############## +# Formatting # +############## + +.PHONY: lint +lint: $(GOLANGCI_LINT_BIN) + # Check .golangci.yml for configuration + $(GOLANGCI_LINT_BIN) run -c .golangci.yml + +.PHONY: format +format: go-fmt shellcheck + +.PHONY: go-fmt +go-fmt: + go fmt $(PKGS) + +.PHONY: shellcheck +shellcheck: + docker run -v "${PWD}:/mnt" koalaman/shellcheck:stable $(shell find . -type f -name "*.sh" -not -path "*vendor*") + +########### +# Testing # +########### + +.PHONY: test +test: test-unit test-integration test-benchmark + +.PHONY: test-unit +test-unit: + go test -race -short $(PKGS) -count=1 + +# TODO(paulfantom): remove this target after removing it from Prow. +test-generate: + make --always-make && git diff --exit-code + +test-integration: build | $(THANOS_BIN) $(UP_BIN) $(MEMCACHED_BIN) $(PROMETHEUS_BIN) + @echo "Running integration tests: V1" + PATH=$$PATH:$$(pwd)/$(BIN_DIR) ./test/integration.sh + @echo "Running integration tests: V2" + PATH=$$PATH:$$(pwd)/$(BIN_DIR) LD_LIBRARY_PATH=$$LD_LIBRARY_PATH:$$(pwd)/$(LIB_DIR) ./test/integration-v2.sh + +test-benchmark: build $(GOJSONTOYAML_BIN) + # Allow the image to be overridden when running in CI. + if [ -n "$$IMAGE_FORMAT" ]; then \ + f=$$(mktemp) && cat ./manifests/benchmark/statefulSetTelemeterServer.yaml | $(GOJSONTOYAML_BIN) --yamltojson | jq '.spec.template.spec.containers[].image="'"$${IMAGE_FORMAT//\$$\{component\}/telemeter}"'"' | $(GOJSONTOYAML_BIN) > $$f && mv $$f ./manifests/benchmark/statefulSetTelemeterServer.yaml; \ + fi + ./test/benchmark.sh "" "" $(BENCHMARK_GOAL) "" $(BENCHMARK_GOAL) + +test/timeseries.txt: + oc port-forward -n openshift-monitoring prometheus-k8s-0 9090 > /dev/null & \ + sleep 5 ; \ + query="curl --fail --silent -G http://localhost:9090/federate"; \ + for rule in $$(jsonnet metrics.json | jq -r '.[]'); do \ + query="$$query $$(printf -- "--data-urlencode match[]=%s" $$rule)"; \ + done; \ + echo '# This file was generated using `make $@`.' > $@ ; \ + $$query >> $@ ; \ + jobs -p | xargs -r kill + + +############ +# Binaries # +############ + +dependencies: $(JB_BIN) $(THANOS_BIN) $(UP_BIN) $(EMBEDMD_BIN) $(GOJSONTOYAML_BIN) | $(PROMETHEUS_BIN) $(GOLANGCI_LINT_BIN) $(MEMCACHED_BIN) + +$(BIN_DIR): + mkdir -p $@ + +$(LIB_DIR): + mkdir -p $@ + +$(MEMCACHED_BIN): | $(BIN_DIR) $(LIB_DIR) + @echo "Downloading Memcached" + curl -L https://www.archlinux.org/packages/core/x86_64/libevent/download/ | tar -I $(LIB_DIR)/zstd --strip-components=2 -xf - -C $(LIB_DIR) usr/lib + curl -L https://www.archlinux.org/packages/extra/x86_64/memcached/download/ | tar -I $(LIB_DIR)/zstd --strip-components=2 -xf - -C $(BIN_DIR) usr/bin/memcached + +$(PROMETHEUS_BIN): $(BIN_DIR) + @echo "Downloading Prometheus" + curl -L "https://github.com/prometheus/prometheus/releases/download/v2.3.2/prometheus-2.3.2.$$(go env GOOS)-$$(go env GOARCH).tar.gz" | tar --strip-components=1 -xzf - -C $(BIN_DIR) + +$(GOLANGCI_LINT_BIN): $(BIN_DIR) + curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/$(GOLANGCI_LINT_VERSION)/install.sh \ + | sed -e '/install -d/d' \ + | sh -s -- -b $(BIN_DIR) $(GOLANGCI_LINT_VERSION) + +$(THANOS_BIN): $(BIN_DIR) + GOBIN=$(BIN_DIR) go install -mod=readonly -modfile=tools/go.mod github.com/thanos-io/thanos/cmd/thanos + +$(UP_BIN): $(BIN_DIR) + GOBIN=$(BIN_DIR) go install -mod=readonly -modfile=tools/go.mod github.com/observatorium/up/cmd/up + +$(EMBEDMD_BIN): $(BIN_DIR) + GOBIN=$(BIN_DIR) go install -mod=readonly -modfile=tools/go.mod github.com/campoy/embedmd + +$(JSONNET_BIN): $(BIN_DIR) + GOBIN=$(BIN_DIR) go install -mod=readonly -modfile=tools/go.mod github.com/google/go-jsonnet/cmd/jsonnet + +$(JB_BIN): $(BIN_DIR) + GOBIN=$(BIN_DIR) go install -mod=readonly -modfile=tools/go.mod github.com/jsonnet-bundler/jsonnet-bundler/cmd/jb + +$(GOJSONTOYAML_BIN): $(BIN_DIR) + GOBIN=$(BIN_DIR) go install -mod=readonly -modfile=tools/go.mod github.com/brancz/gojsontoyaml diff --git a/OWNERS b/OWNERS new file mode 100644 index 00000000..85fe41d8 --- /dev/null +++ b/OWNERS @@ -0,0 +1,5 @@ +component: "Metrics Collector" + +reviewers: + +approvers: diff --git a/README.md b/README.md index 8e744d7f..cfb5e524 100644 --- a/README.md +++ b/README.md @@ -1 +1,76 @@ -# metrics-collector \ No newline at end of file +# metrics-collector +======= +Telemeter +========= + +Telemeter implements a Prometheus federation push client and server +to allow isolated Prometheus instances that cannot be scraped from a +central Prometheus to instead perform push federation to a central +location. + +1. The local client scrapes `/federate` on a given Prometheus instance. +2. The local client performs cleanup and anonymization and then pushes the metrics to the server. +3. The server authenticates the client, validates and verifies that the metrics are "safe", and then ensures they have a label uniquely identifying the source client. +4. The server holds the metrics in a local disk store until scraped. +5. A centralized Prometheus scrapes each server instance and aggregates all the metrics. + +Since that push is across security boundaries, the server must perform +authentication, authorization, and data integrity checks as well as being +resilient to denial of service. + +Each client is uniquely identified by a cluster ID and all metrics +federated are labelled with that ID. + +Since Telemeter is dependent on Prometheus federation, each server +instance must ensure that all metrics for a given cluster ID are routed +to the same instance, otherwise Prometheus will mark those metrics +series as stale. To do this, the server instances form a cluster using +a secure gossip transport and build a consistent hash ring so that +pushed client metrics are routed internally to the same server. + +For resiliency, each server instance stores the received metrics on disk +hashed by cluster ID until they are accessed by a federation endpoint. + +note: Telemeter is alpha and may change significantly + +Get started +----------- + +To see this in action, run + +``` +make +./test/integration.sh http://localhost:9005 +``` + +The command launches a two instance `telemeter-server` cluster and a single +`telemeter-client` to talk to that server, along with a Prometheus +instance running on http://localhost:9005 that shows the federated metrics. +The client will scrape metrics from the local prometheus, then send those +to the telemeter server cluster, which will then be scraped by that instance. + +To run this test against another Prometheus server, change the URL (and if necessary, +specify the bearer token necessary to talk to that server as the second argument). + +To build binaries, run + +``` +make +``` + +To execute the unit test suite, run + +``` +make check +``` + +To launch a self contained integration test, run: + +``` +make test-integration +``` + +Adding new metrics to send via telemeter +----------- + +Docs on the process on why and how to send these metrics are available [here](https://docs.google.com/document/d/1a6n5iBGM2QaIQRg9Lw4-Npj6QY9--Hpx3XYut-BrUSY/edit?usp=sharing). diff --git a/_output/lib/zstd b/_output/lib/zstd new file mode 100755 index 00000000..cc603b81 Binary files /dev/null and b/_output/lib/zstd differ diff --git a/build/Configfile b/build/Configfile new file mode 100644 index 00000000..45daeec4 --- /dev/null +++ b/build/Configfile @@ -0,0 +1,31 @@ +ifdef GIT +IMAGE_VERSION :=$(shell git rev-parse --short HEAD) +VCS_URL ?=$(shell git config --get remote.origin.url) +endif + + +IMAGE_NAME =$(shell cat COMPONENT_NAME) +IMAGE_DISPLAY_NAME=ACM Metrics Colector +ARCH = $(shell uname -m) +ifeq ($(ARCH), x86_64) + IMAGE_NAME_ARCH=$(IMAGE_NAME)-amd64 +else + IMAGE_NAME_ARCH=$(IMAGE_NAME)-$(ARCH) +endif +IMAGE_MAINTAINER=smeduri@redhat.com +IMAGE_VENDOR=Red Hat +IMAGE_DESCRIPTION=ACM Metrics collector service +IMAGE_SUMMARY =$(IMAGE_DESCRIPTION) +IMAGE_OPENSHIFT_TAGS=ACM Metrics collector + +DOCKER_BUILD_OPTS=--build-arg "VCS_REF=$(SEMVERSION)" \ + --build-arg "VCS_URL=$(VCS_URL)" \ + --build-arg "IMAGE_NAME=$(IMAGE_NAME)" \ + --build-arg "IMAGE_DISPLAY_NAME=$(IMAGE_DISPLAY_NAME)" \ + --build-arg "IMAGE_NAME_ARCH=$(IMAGE_NAME_ARCH)" \ + --build-arg "IMAGE_MAINTAINER=$(IMAGE_MAINTAINER)" \ + --build-arg "IMAGE_VENDOR=$(IMAGE_VENDOR)" \ + --build-arg "IMAGE_VERSION=$(IMAGE_VERSION)" \ + --build-arg "IMAGE_DESCRIPTION=$(IMAGE_DESCRIPTION)" \ + --build-arg "IMAGE_SUMMARY=$(IMAGE_SUMMARY)" \ + --build-arg "IMAGE_OPENSHIFT_TAGS=$(IMAGE_OPENSHIFT_TAGS)" diff --git a/build_deploy.sh b/build_deploy.sh new file mode 100755 index 00000000..50445fc8 --- /dev/null +++ b/build_deploy.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +set -exv + +IMAGE="quay.io/app-sre/telemeter" +IMAGE_TAG=$(git rev-parse --short=7 HEAD) + +docker build -t "${IMAGE}:${IMAGE_TAG}" . + +if [[ -n "$QUAY_USER" && -n "$QUAY_TOKEN" ]]; then + DOCKER_CONF="$PWD/.docker" + mkdir -p "$DOCKER_CONF" + docker --config="$DOCKER_CONF" login -u="$QUAY_USER" -p="$QUAY_TOKEN" quay.io + docker --config="$DOCKER_CONF" push "${IMAGE}:${IMAGE_TAG}" +fi diff --git a/cmd/authorization-server/main.go b/cmd/authorization-server/main.go new file mode 100644 index 00000000..754fb764 --- /dev/null +++ b/cmd/authorization-server/main.go @@ -0,0 +1,50 @@ +package main + +import ( + "encoding/json" + "io/ioutil" + stdlog "log" + "net/http" + "os" + + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + + "github.com/openshift/telemeter/pkg/authorize/tollbooth" +) + +type tokenEntry struct { + Token string `json:"token"` +} + +func main() { + if len(os.Args) != 3 { + stdlog.Fatalf("expected two arguments, the listen address and a path to a JSON file containing responses") + } + + data, err := ioutil.ReadFile(os.Args[2]) + if err != nil { + stdlog.Fatalf("unable to read JSON file: %v", err) + } + + var tokenEntries []tokenEntry + if err := json.Unmarshal(data, &tokenEntries); err != nil { + stdlog.Fatalf("unable to parse contents of %s: %v", os.Args[2], err) + } + + tokenSet := make(map[string]struct{}) + for i := range tokenEntries { + tokenSet[tokenEntries[i].Token] = struct{}{} + } + + l := log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)) + l = log.WithPrefix(l, "ts", log.DefaultTimestampUTC) + l = log.WithPrefix(l, "caller", log.DefaultCaller) + level.Info(l).Log("msg", "telemeter authorization-server initialized") + + s := tollbooth.NewMock(l, tokenSet) + + if err := http.ListenAndServe(os.Args[1], s); err != nil { + stdlog.Fatalf("server exited: %v", err) + } +} diff --git a/cmd/telemeter-benchmark/main.go b/cmd/telemeter-benchmark/main.go new file mode 100644 index 00000000..d4f2f02a --- /dev/null +++ b/cmd/telemeter-benchmark/main.go @@ -0,0 +1,220 @@ +package main + +import ( + "fmt" + stdlog "log" + "net" + "net/http" + "net/url" + "os" + "os/signal" + "path" + "syscall" + "time" + + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/oklog/run" + "github.com/spf13/cobra" + + "github.com/openshift/telemeter/pkg/benchmark" + telemeterhttp "github.com/openshift/telemeter/pkg/http" + "github.com/openshift/telemeter/pkg/logger" +) + +type options struct { + Listen string + + To string + ToAuthorize string + ToUpload string + + ToCAFile string + ToToken string + ToTokenFile string + + Interval time.Duration + MetricsFile string + Workers int + + LogLevel string + Logger log.Logger +} + +var opt options = options{ + Interval: benchmark.DefaultSyncPeriod, + Listen: "localhost:8080", + Workers: 1000, +} + +func main() { + cmd := &cobra.Command{ + Short: "Benchmark Telemeter", + + SilenceErrors: true, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + return runCmd() + }, + } + + cmd.Flags().StringVar(&opt.To, "to", opt.To, "A telemeter server to send metrics to.") + cmd.Flags().StringVar(&opt.ToUpload, "to-upload", opt.ToUpload, "A telemeter server endpoint to push metrics to. Will be defaulted for standard servers.") + cmd.Flags().StringVar(&opt.ToAuthorize, "to-auth", opt.ToAuthorize, "A telemeter server endpoint to exchange the bearer token for an access token. Will be defaulted for standard servers.") + cmd.Flags().StringVar(&opt.ToCAFile, "to-ca-file", opt.ToCAFile, "A file containing the CA certificate to use to verify the --to URL in addition to the system roots certificates.") + cmd.Flags().StringVar(&opt.ToToken, "to-token", opt.ToToken, "A bearer token to use when authenticating to the destination telemeter server.") + cmd.Flags().StringVar(&opt.ToTokenFile, "to-token-file", opt.ToTokenFile, "A file containing a bearer token to use when authenticating to the destination telemeter server.") + cmd.Flags().StringVar(&opt.MetricsFile, "metrics-file", opt.MetricsFile, "A file containing Prometheus metrics to send to the destination telemeter server.") + cmd.Flags().DurationVar(&opt.Interval, "interval", opt.Interval, "The interval between scrapes. Prometheus returns the last 5 minutes of metrics when invoking the federation endpoint.") + cmd.Flags().StringVar(&opt.Listen, "listen", opt.Listen, "A host:port to listen on for health and metrics.") + cmd.Flags().IntVar(&opt.Workers, "workers", opt.Workers, "The number of workers to run in parallel.") + + cmd.Flags().StringVar(&opt.LogLevel, "log-level", opt.LogLevel, "Log filtering level. e.g info, debug, warn, error") + + l := log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)) + lvl, err := cmd.Flags().GetString("log-level") + if err != nil { + level.Error(l).Log("msg", "could not parse log-level.") + } + l = level.NewFilter(l, logger.LogLevelFromString(lvl)) + l = log.WithPrefix(l, "ts", log.DefaultTimestampUTC) + l = log.WithPrefix(l, "caller", log.DefaultCaller) + stdlog.SetOutput(log.NewStdlibAdapter(l)) + opt.Logger = l + + if err := cmd.Execute(); err != nil { + level.Error(l).Log("err", err) + os.Exit(1) + } +} + +func runCmd() error { + var to, toUpload, toAuthorize *url.URL + var err error + if len(opt.MetricsFile) == 0 { + return fmt.Errorf("--metrics-file must be specified") + } + to, err = url.Parse(opt.ToUpload) + if err != nil { + return fmt.Errorf("--to-upload is not a valid URL: %v", err) + } + if len(opt.ToUpload) > 0 { + to, err = url.Parse(opt.ToUpload) + if err != nil { + return fmt.Errorf("--to-upload is not a valid URL: %v", err) + } + } + if len(opt.ToAuthorize) > 0 { + toAuthorize, err = url.Parse(opt.ToAuthorize) + if err != nil { + return fmt.Errorf("--to-auth is not a valid URL: %v", err) + } + } + if len(opt.To) > 0 { + to, err = url.Parse(opt.To) + if err != nil { + return fmt.Errorf("--to is not a valid URL: %v", err) + } + if len(to.Path) == 0 { + to.Path = "/" + } + if toAuthorize == nil { + u := *to + u.Path = path.Join(to.Path, "authorize") + toAuthorize = &u + } + u := *to + u.Path = path.Join(to.Path, "upload") + toUpload = &u + } + + if toUpload == nil || toAuthorize == nil { + return fmt.Errorf("either --to or --to-auth and --to-upload must be specified") + } + + cfg := &benchmark.Config{ + ToAuthorize: toAuthorize, + ToUpload: toUpload, + ToCAFile: opt.ToCAFile, + ToToken: opt.ToToken, + ToTokenFile: opt.ToTokenFile, + Interval: opt.Interval, + MetricsFile: opt.MetricsFile, + Workers: opt.Workers, + Logger: opt.Logger, + } + + b, err := benchmark.New(cfg) + if err != nil { + return fmt.Errorf("failed to configure the Telemeter benchmarking tool: %v", err) + } + + level.Info(opt.Logger).Log("msg", "starting telemeter-benchmark", "to", opt.To, "addr", opt.Listen) + + var g run.Group + { + // Execute the worker's `Run` func. + g.Add(func() error { + b.Run() + return nil + }, func(error) { + b.Stop() + }) + } + + { + // Notify and reload on SIGHUP. + hup := make(chan os.Signal, 1) + signal.Notify(hup, syscall.SIGHUP) + // Cleanup on SIGINT. + in := make(chan os.Signal, 1) + signal.Notify(in, syscall.SIGINT) + cancel := make(chan struct{}) + g.Add(func() error { + for { + select { + case <-hup: + if err := b.Reconfigure(cfg); err != nil { + level.Error(opt.Logger).Log("msg", "failed to reload config", "err", err) + return err + } + case <-in: + level.Warn(opt.Logger).Log("msg", "caught interrupt; exiting gracefully...") + b.Stop() + return nil + case <-cancel: + return nil + } + } + }, func(error) { + close(cancel) + }) + } + + if len(opt.Listen) > 0 { + handlers := http.NewServeMux() + telemeterhttp.DebugRoutes(handlers) + telemeterhttp.HealthRoutes(handlers) + telemeterhttp.MetricRoutes(handlers) + telemeterhttp.ReloadRoutes(handlers, func() error { + return b.Reconfigure(cfg) + }) + l, err := net.Listen("tcp", opt.Listen) + if err != nil { + return fmt.Errorf("failed to listen: %v", err) + } + + // Run the HTTP server. + g.Add(func() error { + if err := http.Serve(l, handlers); err != nil && err != http.ErrServerClosed { + level.Error(opt.Logger).Log("msg", "server exited unexpectedly", "err", err) + return err + } + return nil + }, func(error) { + l.Close() + }) + } + + return g.Run() +} diff --git a/cmd/telemeter-client/main.go b/cmd/telemeter-client/main.go new file mode 100644 index 00000000..bc5962bd --- /dev/null +++ b/cmd/telemeter-client/main.go @@ -0,0 +1,347 @@ +package main + +import ( + "context" + "fmt" + stdlog "log" + "net" + "net/http" + "net/url" + "os" + "os/signal" + "path" + "strings" + "syscall" + "time" + + "github.com/oklog/run" + "github.com/prometheus/common/expfmt" + "github.com/spf13/cobra" + + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + + "github.com/openshift/telemeter/pkg/forwarder" + telemeterhttp "github.com/openshift/telemeter/pkg/http" + "github.com/openshift/telemeter/pkg/logger" + "github.com/openshift/telemeter/pkg/metricfamily" +) + +func main() { + opt := &Options{ + Listen: "localhost:9002", + LimitBytes: 200 * 1024, + Rules: []string{`{__name__="up"}`}, + Interval: 4*time.Minute + 30*time.Second, + } + cmd := &cobra.Command{ + Short: "Federate Prometheus via push", + SilenceErrors: true, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + return opt.Run() + }, + } + + cmd.Flags().StringVar(&opt.Listen, "listen", opt.Listen, "A host:port to listen on for health and metrics.") + cmd.Flags().StringVar(&opt.From, "from", opt.From, "The Prometheus server to federate from.") + cmd.Flags().StringVar(&opt.FromToken, "from-token", opt.FromToken, "A bearer token to use when authenticating to the source Prometheus server.") + cmd.Flags().StringVar(&opt.FromCAFile, "from-ca-file", opt.FromCAFile, "A file containing the CA certificate to use to verify the --from URL in addition to the system roots certificates.") + cmd.Flags().StringVar(&opt.FromTokenFile, "from-token-file", opt.FromTokenFile, "A file containing a bearer token to use when authenticating to the source Prometheus server.") + cmd.Flags().StringVar(&opt.Identifier, "id", opt.Identifier, "The unique identifier for metrics sent with this client.") + cmd.Flags().StringVar(&opt.To, "to", opt.To, "A telemeter server to send metrics to.") + cmd.Flags().StringVar(&opt.ToUpload, "to-upload", opt.ToUpload, "A telemeter server endpoint to push metrics to. Will be defaulted for standard servers.") + cmd.Flags().StringVar(&opt.ToAuthorize, "to-auth", opt.ToAuthorize, "A telemeter server endpoint to exchange the bearer token for an access token. Will be defaulted for standard servers.") + cmd.Flags().StringVar(&opt.ToToken, "to-token", opt.ToToken, "A bearer token to use when authenticating to the destination telemeter server.") + cmd.Flags().StringVar(&opt.ToTokenFile, "to-token-file", opt.ToTokenFile, "A file containing a bearer token to use when authenticating to the destination telemeter server.") + cmd.Flags().DurationVar(&opt.Interval, "interval", opt.Interval, "The interval between scrapes. Prometheus returns the last 5 minutes of metrics when invoking the federation endpoint.") + cmd.Flags().Int64Var(&opt.LimitBytes, "limit-bytes", opt.LimitBytes, "The maxiumum acceptable size of a response returned when scraping Prometheus.") + + // TODO: more complex input definition, such as a JSON struct + cmd.Flags().StringArrayVar(&opt.Rules, "match", opt.Rules, "Match rules to federate.") + cmd.Flags().StringVar(&opt.RulesFile, "match-file", opt.RulesFile, "A file containing match rules to federate, one rule per line.") + + cmd.Flags().StringSliceVar(&opt.LabelFlag, "label", opt.LabelFlag, "Labels to add to each outgoing metric, in key=value form.") + cmd.Flags().StringSliceVar(&opt.RenameFlag, "rename", opt.RenameFlag, "Rename metrics before sending by specifying OLD=NEW name pairs. Defaults to renaming ALERTS to alerts. Defaults to ALERTS=alerts.") + + cmd.Flags().StringSliceVar(&opt.AnonymizeLabels, "anonymize-labels", opt.AnonymizeLabels, "Anonymize the values of the provided values before sending them on.") + cmd.Flags().StringVar(&opt.AnonymizeSalt, "anonymize-salt", opt.AnonymizeSalt, "A secret and unguessable value used to anonymize the input data.") + cmd.Flags().StringVar(&opt.AnonymizeSaltFile, "anonymize-salt-file", opt.AnonymizeSaltFile, "A file containing a secret and unguessable value used to anonymize the input data.") + + cmd.Flags().BoolVarP(&opt.Verbose, "verbose", "v", opt.Verbose, "Show verbose output.") + + cmd.Flags().StringVar(&opt.LogLevel, "log-level", opt.LogLevel, "Log filtering level. e.g info, debug, warn, error") + + l := log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)) + lvl, err := cmd.Flags().GetString("log-level") + if err != nil { + level.Error(l).Log("msg", "could not parse log-level.") + } + l = level.NewFilter(l, logger.LogLevelFromString(lvl)) + l = log.WithPrefix(l, "ts", log.DefaultTimestampUTC) + l = log.WithPrefix(l, "caller", log.DefaultCaller) + stdlog.SetOutput(log.NewStdlibAdapter(l)) + opt.Logger = l + level.Info(l).Log("msg", "telemeter client initialized") + + if err := cmd.Execute(); err != nil { + level.Error(l).Log("err", err) + os.Exit(1) + } +} + +type Options struct { + Listen string + LimitBytes int64 + Verbose bool + + From string + To string + ToUpload string + ToAuthorize string + FromCAFile string + FromToken string + FromTokenFile string + ToToken string + ToTokenFile string + Identifier string + + RenameFlag []string + Renames map[string]string + + AnonymizeLabels []string + AnonymizeSalt string + AnonymizeSaltFile string + + Rules []string + RulesFile string + + LabelFlag []string + Labels map[string]string + + Interval time.Duration + + LogLevel string + Logger log.Logger +} + +func (o *Options) Run() error { + if len(o.From) == 0 { + return fmt.Errorf("you must specify a Prometheus server to federate from (e.g. http://localhost:9090)") + } + + for _, flag := range o.LabelFlag { + values := strings.SplitN(flag, "=", 2) + if len(values) != 2 { + return fmt.Errorf("--label must be of the form key=value: %s", flag) + } + if o.Labels == nil { + o.Labels = make(map[string]string) + } + o.Labels[values[0]] = values[1] + } + + if len(o.RenameFlag) == 0 { + o.RenameFlag = []string{"ALERTS=alerts"} + } + for _, flag := range o.RenameFlag { + if len(flag) == 0 { + continue + } + values := strings.SplitN(flag, "=", 2) + if len(values) != 2 { + return fmt.Errorf("--rename must be of the form OLD_NAME=NEW_NAME: %s", flag) + } + if o.Renames == nil { + o.Renames = make(map[string]string) + } + o.Renames[values[0]] = values[1] + } + + from, err := url.Parse(o.From) + if err != nil { + return fmt.Errorf("--from is not a valid URL: %v", err) + } + from.Path = strings.TrimRight(from.Path, "/") + if len(from.Path) == 0 { + from.Path = "/federate" + } + + var to, toUpload, toAuthorize *url.URL + if len(o.ToUpload) > 0 { + to, err = url.Parse(o.ToUpload) + toUpload = to + if err != nil { + return fmt.Errorf("--to-upload is not a valid URL: %v", err) + } + } + if len(o.ToAuthorize) > 0 { + toAuthorize, err = url.Parse(o.ToAuthorize) + if err != nil { + return fmt.Errorf("--to-auth is not a valid URL: %v", err) + } + } + if len(o.To) > 0 { + to, err = url.Parse(o.To) + if err != nil { + return fmt.Errorf("--to is not a valid URL: %v", err) + } + if len(to.Path) == 0 { + to.Path = "/" + } + if toAuthorize == nil { + u := *to + u.Path = path.Join(to.Path, "authorize") + if len(o.Identifier) > 0 { + q := to.Query() + q.Add("id", o.Identifier) + u.RawQuery = q.Encode() + } + toAuthorize = &u + } + u := *to + u.Path = path.Join(to.Path, "upload") + toUpload = &u + } + level.Info(o.Logger).Log("msg3", toUpload) + // if toUpload == nil || toAuthorize == nil { + // return fmt.Errorf("either --to or --to-auth and --to-upload must be specified") + // } + + var transformer metricfamily.MultiTransformer + + if len(o.Labels) > 0 { + transformer.WithFunc(func() metricfamily.Transformer { + return metricfamily.NewLabel(o.Labels, nil) + }) + } + + if len(o.Renames) > 0 { + transformer.WithFunc(func() metricfamily.Transformer { + return metricfamily.RenameMetrics{Names: o.Renames} + }) + } + + transformer.WithFunc(func() metricfamily.Transformer { + return metricfamily.NewDropInvalidFederateSamples(time.Now().Add(-24 * time.Hour)) + }) + + transformer.With(metricfamily.TransformerFunc(metricfamily.PackMetrics)) + transformer.With(metricfamily.TransformerFunc(metricfamily.SortMetrics)) + + cfg := forwarder.Config{ + From: from, + ToAuthorize: toAuthorize, + ToUpload: toUpload, + FromToken: o.FromToken, + ToToken: o.ToToken, + FromTokenFile: o.FromTokenFile, + ToTokenFile: o.ToTokenFile, + FromCAFile: o.FromCAFile, + + AnonymizeLabels: o.AnonymizeLabels, + AnonymizeSalt: o.AnonymizeSalt, + AnonymizeSaltFile: o.AnonymizeSaltFile, + Debug: o.Verbose, + Interval: o.Interval, + LimitBytes: o.LimitBytes, + Rules: o.Rules, + RulesFile: o.RulesFile, + Transformer: transformer, + + Logger: o.Logger, + } + + worker, err := forwarder.New(cfg) + if err != nil { + return fmt.Errorf("failed to configure Telemeter client: %v", err) + } + + level.Info(o.Logger).Log("msg", "starting telemeter-client", "from", o.From, "to", o.To, "listen", o.Listen) + + var g run.Group + { + // Execute the worker's `Run` func. + ctx, cancel := context.WithCancel(context.Background()) + g.Add(func() error { + worker.Run(ctx) + return nil + }, func(error) { + cancel() + }) + } + + { + // Notify and reload on SIGHUP. + hup := make(chan os.Signal, 1) + signal.Notify(hup, syscall.SIGHUP) + cancel := make(chan struct{}) + g.Add(func() error { + for { + select { + case <-hup: + if err := worker.Reconfigure(cfg); err != nil { + level.Error(o.Logger).Log("msg", "failed to reload config", "err", err) + return err + } + case <-cancel: + return nil + } + } + }, func(error) { + close(cancel) + }) + } + + if len(o.Listen) > 0 { + handlers := http.NewServeMux() + telemeterhttp.DebugRoutes(handlers) + telemeterhttp.HealthRoutes(handlers) + telemeterhttp.MetricRoutes(handlers) + telemeterhttp.ReloadRoutes(handlers, func() error { + return worker.Reconfigure(cfg) + }) + handlers.Handle("/federate", serveLastMetrics(o.Logger, worker)) + l, err := net.Listen("tcp", o.Listen) + if err != nil { + return fmt.Errorf("failed to listen: %v", err) + } + + { + // Run the HTTP server. + g.Add(func() error { + if err := http.Serve(l, handlers); err != nil && err != http.ErrServerClosed { + level.Error(o.Logger).Log("msg", "server exited unexpectedly", "err", err) + return err + } + return nil + }, func(error) { + l.Close() + }) + } + } + + return g.Run() +} + +// serveLastMetrics retrieves the last set of metrics served +func serveLastMetrics(l log.Logger, worker *forwarder.Worker) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if req.Method != "GET" { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + families := worker.LastMetrics() + w.Header().Set("Content-Type", string(expfmt.FmtText)) + encoder := expfmt.NewEncoder(w, expfmt.FmtText) + for _, family := range families { + if family == nil { + continue + } + if err := encoder.Encode(family); err != nil { + level.Error(l).Log("msg", "unable to write metrics for family", "err", err) + break + } + } + }) +} diff --git a/cmd/telemeter-server/main.go b/cmd/telemeter-server/main.go new file mode 100644 index 00000000..32549ac7 --- /dev/null +++ b/cmd/telemeter-server/main.go @@ -0,0 +1,553 @@ +package main + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + "io/ioutil" + stdlog "log" + "net" + "net/http" + "net/url" + "os" + "strings" + "time" + + "github.com/coreos/go-oidc" + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/oklog/run" + "github.com/prometheus/client_golang/prometheus" + "github.com/spf13/cobra" + "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" + + "github.com/openshift/telemeter/pkg/authorize" + "github.com/openshift/telemeter/pkg/authorize/jwt" + "github.com/openshift/telemeter/pkg/authorize/stub" + "github.com/openshift/telemeter/pkg/authorize/tollbooth" + "github.com/openshift/telemeter/pkg/cache" + "github.com/openshift/telemeter/pkg/cache/memcached" + telemeter_http "github.com/openshift/telemeter/pkg/http" + "github.com/openshift/telemeter/pkg/logger" + "github.com/openshift/telemeter/pkg/metricfamily" + "github.com/openshift/telemeter/pkg/receive" + "github.com/openshift/telemeter/pkg/server" +) + +const desc = ` +Receive federated metric push events + +This server acts as an auth proxy for ingesting Prometheus metrics. +The original API implements its own protocol for receiving metrics. +The new API receives metrics via the Prometheus remote write API. +This server authenticates requests, performs local filtering and sanity +checking and then forwards the requests via remote write to another endpoint. + +A client that connects to the server must perform an authorization check, providing +a token and a cluster identifier. +The original API expects a bearer token in the Authorization header and a cluster ID +as a query parameter when making a request against the /authorize endpoint. +The authorize endpoint will forward that request to an upstream server that +may approve or reject the request as well as add additional labels that will be added +to all future metrics from that client. The server will generate a JWT token with a +short lifetime and pass that back to the client, which is expected to use that token +when pushing metrics to /upload. + +The new API expects a bearer token in the Authorization header when making requests +against the /metrics/v1/receive endpoints. This token should consist of a +base64-encoded JSON object containing "authorization_token" and "cluster_id" fields. + +Clients are considered untrusted and so input data is validated, sorted, and +normalized before processing continues. +` + +func main() { + opt := &Options{ + Listen: "0.0.0.0:9003", + ListenInternal: "localhost:9004", + + LimitBytes: 500 * 1024, + TokenExpireSeconds: 24 * 60 * 60, + clusterIDKey: "_id", + Ratelimit: 4*time.Minute + 30*time.Second, + MemcachedExpire: 24 * 60 * 60, + MemcachedInterval: 10, + TenantID: "FB870BF3-9F3A-44FF-9BF7-D7A047A52F43", + } + cmd := &cobra.Command{ + Short: "Aggregate federated metrics pushes", + Long: desc, + SilenceErrors: true, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + return opt.Run() + }, + } + + cmd.Flags().StringVar(&opt.Listen, "listen", opt.Listen, "A host:port to listen on for upload traffic.") + cmd.Flags().StringVar(&opt.ListenInternal, "listen-internal", opt.ListenInternal, "A host:port to listen on for health and metrics.") + + cmd.Flags().StringVar(&opt.TLSKeyPath, "tls-key", opt.TLSKeyPath, "Path to a private key to serve TLS for external traffic.") + cmd.Flags().StringVar(&opt.TLSCertificatePath, "tls-crt", opt.TLSCertificatePath, "Path to a certificate to serve TLS for external traffic.") + + cmd.Flags().StringVar(&opt.InternalTLSKeyPath, "internal-tls-key", opt.InternalTLSKeyPath, "Path to a private key to serve TLS for internal traffic.") + cmd.Flags().StringVar(&opt.InternalTLSCertificatePath, "internal-tls-crt", opt.InternalTLSCertificatePath, "Path to a certificate to serve TLS for internal traffic.") + + cmd.Flags().StringSliceVar(&opt.LabelFlag, "label", opt.LabelFlag, "Labels to add to each outgoing metric, in key=value form.") + cmd.Flags().StringVar(&opt.clusterIDKey, "partition-label", opt.clusterIDKey, "The label to separate incoming data on. This label will be required for callers to include.") + + cmd.Flags().StringVar(&opt.SharedKey, "shared-key", opt.SharedKey, "The path to a private key file that will be used to sign authentication requests.") + cmd.Flags().Int64Var(&opt.TokenExpireSeconds, "token-expire-seconds", opt.TokenExpireSeconds, "The expiration of auth tokens in seconds.") + + cmd.Flags().StringVar(&opt.AuthorizeEndpoint, "authorize", opt.AuthorizeEndpoint, "A URL against which to authorize client requests.") + + cmd.Flags().StringVar(&opt.OIDCIssuer, "oidc-issuer", opt.OIDCIssuer, "The OIDC issuer URL, see https://openid.net/specs/openid-connect-discovery-1_0.html#IssuerDiscovery.") + cmd.Flags().StringVar(&opt.ClientSecret, "client-secret", opt.ClientSecret, "The OIDC client secret, see https://tools.ietf.org/html/rfc6749#section-2.3.") + cmd.Flags().StringVar(&opt.ClientID, "client-id", opt.ClientID, "The OIDC client ID, see https://tools.ietf.org/html/rfc6749#section-2.3.") + cmd.Flags().StringVar(&opt.TenantKey, "tenant-key", opt.TenantKey, "The JSON key in the bearer token whose value to use as the tenant ID.") + cmd.Flags().StringSliceVar(&opt.Memcacheds, "memcached", opt.Memcacheds, "One or more Memcached server addresses.") + cmd.Flags().Int32Var(&opt.MemcachedExpire, "memcached-expire", opt.MemcachedExpire, "Time after which keys stored in Memcached should expire, given in seconds.") + cmd.Flags().Int32Var(&opt.MemcachedInterval, "memcached-interval", opt.MemcachedInterval, "The interval at which to update the Memcached DNS, given in seconds; use 0 to disable.") + cmd.Flags().StringVar(&opt.TenantID, "tenant-id", opt.TenantID, "Tenant ID to use for the system forwarded to.") + + cmd.Flags().DurationVar(&opt.Ratelimit, "ratelimit", opt.Ratelimit, "The rate limit of metric uploads per cluster ID. Uploads happening more often than this limit will be rejected.") + cmd.Flags().StringVar(&opt.ForwardURL, "forward-url", opt.ForwardURL, "All written metrics will be written to this URL additionally") + + cmd.Flags().BoolVarP(&opt.Verbose, "verbose", "v", opt.Verbose, "Show verbose output.") + + cmd.Flags().StringSliceVar(&opt.RequiredLabelFlag, "required-label", opt.RequiredLabelFlag, "Labels that must be present on each incoming metric, in key=value form.") + cmd.Flags().StringArrayVar(&opt.Whitelist, "whitelist", opt.Whitelist, "Allowed rules for incoming metrics. If one of these rules is not matched, the metric is dropped.") + cmd.Flags().StringVar(&opt.WhitelistFile, "whitelist-file", opt.WhitelistFile, "A file of allowed rules for incoming metrics. If one of these rules is not matched, the metric is dropped; one label key per line.") + cmd.Flags().StringArrayVar(&opt.ElideLabels, "elide-label", opt.ElideLabels, "A list of labels to be elided from incoming metrics.") + cmd.Flags().Int64Var(&opt.LimitBytes, "limit-bytes", opt.LimitBytes, "The maxiumum acceptable size of a request made to the upload endpoint.") + + cmd.Flags().StringVar(&opt.LogLevel, "log-level", opt.LogLevel, "Log filtering level. e.g info, debug, warn, error") + + l := log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)) + l = level.NewFilter(l, logger.LogLevelFromString(opt.LogLevel)) + l = log.WithPrefix(l, "ts", log.DefaultTimestampUTC) + l = log.WithPrefix(l, "caller", log.DefaultCaller) + stdlog.SetOutput(log.NewStdlibAdapter(l)) + opt.Logger = l + level.Info(l).Log("msg", "Telemeter server initialized.") + + if err := cmd.Execute(); err != nil { + level.Error(l).Log("err", err) + os.Exit(1) + } +} + +type Options struct { + Listen string + ListenInternal string + + TLSKeyPath string + TLSCertificatePath string + + InternalTLSKeyPath string + InternalTLSCertificatePath string + + SharedKey string + TokenExpireSeconds int64 + + AuthorizeEndpoint string + + OIDCIssuer string + ClientID string + ClientSecret string + TenantKey string + TenantID string + Memcacheds []string + MemcachedExpire int32 + MemcachedInterval int32 + + clusterIDKey string + LabelFlag []string + Labels map[string]string + LimitBytes int64 + RequiredLabelFlag []string + RequiredLabels map[string]string + Whitelist []string + ElideLabels []string + WhitelistFile string + + Ratelimit time.Duration + ForwardURL string + + LogLevel string + Logger log.Logger + + Verbose bool +} + +type Paths struct { + Paths []string `json:"paths"` +} + +func (o *Options) Run() error { + for _, flag := range o.LabelFlag { + values := strings.SplitN(flag, "=", 2) + if len(values) != 2 { + return fmt.Errorf("--label must be of the form key=value: %s", flag) + } + if o.Labels == nil { + o.Labels = make(map[string]string) + } + o.Labels[values[0]] = values[1] + } + + for _, flag := range o.RequiredLabelFlag { + values := strings.SplitN(flag, "=", 2) + if len(values) != 2 { + return fmt.Errorf("--required-label must be of the form key=value: %s", flag) + } + if o.RequiredLabels == nil { + o.RequiredLabels = make(map[string]string) + } + o.RequiredLabels[values[0]] = values[1] + } + + var transport http.RoundTripper = &http.Transport{ + DialContext: (&net.Dialer{Timeout: 10 * time.Second}).DialContext, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 30 * time.Second, + } + + if o.Verbose { + transport = telemeter_http.NewDebugRoundTripper(o.Logger, transport) + } + + // set up the upstream authorization + var authorizeURL *url.URL + var authorizeClient http.Client + ctx := context.Background() + if len(o.AuthorizeEndpoint) > 0 { + u, err := url.Parse(o.AuthorizeEndpoint) + if err != nil { + return fmt.Errorf("--authorize must be a valid URL: %v", err) + } + authorizeURL = u + + authorizeClient = http.Client{ + Timeout: 20 * time.Second, + Transport: telemeter_http.NewInstrumentedRoundTripper("authorize", transport), + } + } + + forwardClient := &http.Client{ + Timeout: 5 * time.Second, + Transport: telemeter_http.NewInstrumentedRoundTripper("forward", transport), + } + + if o.OIDCIssuer != "" { + provider, err := oidc.NewProvider(ctx, o.OIDCIssuer) + if err != nil { + return fmt.Errorf("OIDC provider initialization failed: %v", err) + } + + ctx = context.WithValue(ctx, oauth2.HTTPClient, + &http.Client{ + Timeout: 20 * time.Second, + Transport: telemeter_http.NewInstrumentedRoundTripper("oauth", transport), + }, + ) + + cfg := clientcredentials.Config{ + ClientID: o.ClientID, + ClientSecret: o.ClientSecret, + TokenURL: provider.Endpoint().TokenURL, + } + + authorizeClient.Transport = &oauth2.Transport{ + Base: authorizeClient.Transport, + Source: cfg.TokenSource(ctx), + } + forwardClient.Transport = &oauth2.Transport{ + Base: forwardClient.Transport, + Source: cfg.TokenSource(ctx), + } + } + + switch { + case (len(o.TLSCertificatePath) == 0) != (len(o.TLSKeyPath) == 0): + return fmt.Errorf("both --tls-key and --tls-crt must be provided") + case (len(o.InternalTLSCertificatePath) == 0) != (len(o.InternalTLSKeyPath) == 0): + return fmt.Errorf("both --internal-tls-key and --internal-tls-crt must be provided") + } + + var g run.Group + { + internal := http.NewServeMux() + + // TODO: Refactor to not take *http.Mux + telemeter_http.DebugRoutes(internal) + telemeter_http.MetricRoutes(internal) + telemeter_http.HealthRoutes(internal) + + r := chi.NewRouter() + r.Mount("/", internal) + + r.Get("/", func(w http.ResponseWriter, req *http.Request) { + internalPathJSON, _ := json.MarshalIndent(Paths{Paths: []string{"/", "/metrics", "/debug/pprof", "/healthz", "/healthz/ready"}}, "", " ") + + w.Header().Add("Content-Type", "application/json") + if _, err := w.Write(internalPathJSON); err != nil { + level.Error(o.Logger).Log("msg", "could not write internal paths", "err", err) + } + }) + + s := &http.Server{ + Handler: r, + } + + internalListener, err := net.Listen("tcp", o.ListenInternal) + if err != nil { + return err + } + + // Run the internal server. + g.Add(func() error { + if len(o.InternalTLSCertificatePath) > 0 { + if err := s.ServeTLS(internalListener, o.InternalTLSCertificatePath, o.InternalTLSKeyPath); err != nil && err != http.ErrServerClosed { + level.Error(o.Logger).Log("msg", "internal HTTPS server exited", "err", err) + return err + } + } else { + if err := s.Serve(internalListener); err != nil && err != http.ErrServerClosed { + level.Error(o.Logger).Log("msg", "internal HTTP server exited", "err", err) + return err + } + } + return nil + }, func(error) { + _ = s.Shutdown(context.TODO()) + internalListener.Close() + }) + } + { + external := chi.NewRouter() + external.Use(middleware.RequestID) + + // TODO: Refactor HealthRoutes to not take *http.Mux + mux := http.NewServeMux() + telemeter_http.HealthRoutes(mux) + external.Mount("/", mux) + + // v1 routes + { + var ( + publicKey crypto.PublicKey + privateKey crypto.PrivateKey + ) + + if len(o.SharedKey) > 0 { + data, err := ioutil.ReadFile(o.SharedKey) + if err != nil { + return fmt.Errorf("unable to read --shared-key: %v", err) + } + + key, err := loadPrivateKey(data) + if err != nil { + return err + } + + switch t := key.(type) { + case *ecdsa.PrivateKey: + privateKey, publicKey = t, t.Public() + case *rsa.PrivateKey: + privateKey, publicKey = t, t.Public() + default: + return fmt.Errorf("unknown key type in --shared-key") + } + } else { + level.Warn(o.Logger).Log("msg", "Using a generated shared-key") + + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return fmt.Errorf("key generation failed: %v", err) + } + + privateKey, publicKey = key, key.Public() + } + + // Configure the whitelist. + if len(o.WhitelistFile) > 0 { + data, err := ioutil.ReadFile(o.WhitelistFile) + if err != nil { + return fmt.Errorf("unable to read --whitelist-file: %v", err) + } + o.Whitelist = append(o.Whitelist, strings.Split(string(data), "\n")...) + } + for i := 0; i < len(o.Whitelist); { + s := strings.TrimSpace(o.Whitelist[i]) + if len(s) == 0 { + o.Whitelist = append(o.Whitelist[:i], o.Whitelist[i+1:]...) + continue + } + o.Whitelist[i] = s + i++ + } + whitelister, err := metricfamily.NewWhitelist(o.Whitelist) + if err != nil { + return err + } + + const issuer = "telemeter.selfsigned" + const audience = "telemeter-client" + + jwtAuthorizer := jwt.NewClientAuthorizer( + issuer, + []crypto.PublicKey{publicKey}, + jwt.NewValidator(o.Logger, []string{audience}), + ) + signer := jwt.NewSigner(issuer, privateKey) + + // configure the authenticator and incoming data validator + var clusterAuth authorize.ClusterAuthorizer = authorize.ClusterAuthorizerFunc(stub.Authorize) + if authorizeURL != nil { + clusterAuth = tollbooth.NewAuthorizer(o.Logger, &authorizeClient, authorizeURL) + } + + auth := jwt.NewAuthorizeClusterHandler(o.Logger, o.clusterIDKey, o.TokenExpireSeconds, signer, o.RequiredLabels, clusterAuth) + + forwardURL, err := url.Parse(o.ForwardURL) + if err != nil { + return fmt.Errorf("--forward-url must be a valid URL: %v", err) + } + + transforms := metricfamily.MultiTransformer{} + transforms.With(whitelister) + if len(o.Labels) > 0 { + transforms.With(metricfamily.NewLabel(o.Labels, nil)) + } + transforms.With(metricfamily.NewElide(o.ElideLabels...)) + + external.Post("/authorize", + server.InstrumentedHandler("authorize", + auth, + ).ServeHTTP) + + external.Post("/upload", + server.InstrumentedHandler("upload", + authorize.NewAuthorizeClientHandler(jwtAuthorizer, + server.ClusterID(o.Logger, o.clusterIDKey, + server.Ratelimit(o.Logger, o.Ratelimit, time.Now, + server.Snappy( + server.Validate(o.Logger, transforms, 24*time.Hour, o.LimitBytes, time.Now, + server.ForwardHandler(o.Logger, forwardURL, o.TenantID, forwardClient), + ), + ), + ), + ), + ), + ).ServeHTTP, + ) + } + + // v2 routes + { + v2AuthorizeClient := authorizeClient + + if len(o.Memcacheds) > 0 { + mc := memcached.New(context.Background(), o.MemcachedInterval, o.MemcachedExpire, o.Memcacheds...) + l := log.With(o.Logger, "component", "cache") + v2AuthorizeClient.Transport = cache.NewRoundTripper(mc, tollbooth.ExtractToken, v2AuthorizeClient.Transport, l, prometheus.DefaultRegisterer) + } + + receiver := receive.NewHandler(o.Logger, o.ForwardURL, prometheus.DefaultRegisterer, o.TenantID) + + external.Handle("/metrics/v1/receive", + server.InstrumentedHandler("receive", + authorize.NewHandler(o.Logger, &v2AuthorizeClient, authorizeURL, o.TenantKey, + receive.LimitBodySize(receive.DefaultRequestLimit, + receive.ValidateLabels( + o.Logger, + http.HandlerFunc(receiver.Receive), + o.clusterIDKey, // TODO: Enforce the same labels for v1 and v2 + ), + ), + ), + ), + ) + } + + externalPathJSON, _ := json.MarshalIndent(Paths{Paths: []string{"/", "/authorize", "/upload", "/healthz", "/healthz/ready", "/metrics/v1/receive"}}, "", " ") + + external.Get("/", func(w http.ResponseWriter, req *http.Request) { + w.Header().Add("Content-Type", "application/json") + if _, err := w.Write(externalPathJSON); err != nil { + level.Error(o.Logger).Log("msg", "could not write external paths", "err", err) + } + }) + + s := &http.Server{ + Handler: external, + } + + externalListener, err := net.Listen("tcp", o.Listen) + if err != nil { + return err + } + + // Run the external server. + g.Add(func() error { + if len(o.TLSCertificatePath) > 0 { + if err := s.ServeTLS(externalListener, o.TLSCertificatePath, o.TLSKeyPath); err != nil && err != http.ErrServerClosed { + level.Error(o.Logger).Log("msg", "external HTTPS server exited", "err", err) + return err + } + } else { + if err := s.Serve(externalListener); err != nil && err != http.ErrServerClosed { + level.Error(o.Logger).Log("msg", "external HTTP server exited", "err", err) + return err + } + } + return nil + }, func(error) { + _ = s.Shutdown(context.TODO()) + externalListener.Close() + }) + } + + level.Info(o.Logger).Log("msg", "starting telemeter-server", "listen", o.Listen, "internal", o.ListenInternal) + + return g.Run() +} + +// loadPrivateKey loads a private key from PEM/DER-encoded data. +func loadPrivateKey(data []byte) (crypto.PrivateKey, error) { + input := data + + block, _ := pem.Decode(data) + if block != nil { + input = block.Bytes + } + + var priv interface{} + priv, err0 := x509.ParsePKCS1PrivateKey(input) + if err0 == nil { + return priv, nil + } + + priv, err1 := x509.ParsePKCS8PrivateKey(input) + if err1 == nil { + return priv, nil + } + + priv, err2 := x509.ParseECPrivateKey(input) + if err2 == nil { + return priv, nil + } + + return nil, fmt.Errorf("unable to parse private key data: '%s', '%s' and '%s'", err0, err1, err2) +} diff --git a/docs/benchmarking.md b/docs/benchmarking.md new file mode 100644 index 00000000..70a93fad --- /dev/null +++ b/docs/benchmarking.md @@ -0,0 +1,40 @@ +# Benchmarking Telemeter + +The goal of the benchmarking suite is to determine the capability of the Telemeter stack to successfully handle client requests. +Identifying the functional limit of a given configuration allows the Telemeter team to predict the stack's ability to meet its design requirements as well as quantify the acceptance of new metrics into the Telemeter pipeline. +This document explains the design and operation of the Telemeter benchmarking suite as well as the results and analysis of current benchmarking tests. + +## Prerequisites + +An existing OpenShift 4.0 cluster is required in order to run the benchmarking suite. +The cluster must have a properly configured wildcard DNS record, or DNS controller, and ingress controller so that any created routes correctly forward traffic to their corresponding services. +Without this, the stack cannot equitably distribute client requests across the replicas in the StatefulSet and the test could result in one overloaded instance forwarding requests to the rest of the cluster, causing the test to fail prematurely. +A standard 4.0 cluster created with the [OpenShift Installer](https://github.com/openshift/installer) on AWS is sufficient. + +Generating the CPU and memory utilization plots for Telemeter server requires the following tools: +* jq +* python3 +* ghostscript +* matplotlib + +## Running + +To run the benchmarking suite: +1. create an OpenShift cluster +2. `export KUBECONFIG=...` +3. `make test-benchmark` + +The benchmarking suite will generate CPU and memory utilization results for the test run. +To generate plots for these results, run invoke the following Makefile target: +```shell +make benchmark.pdf +``` + +## Further Reading + +The test/timeseries.txt file contains the time series used by the telemeter-benchmark binary to create payloads to send to the Telemeter server. + +To regenerate the time series list: +1. create an OpenShift cluster +2. `export KUBECONFIG=...` +3. `make test/timeseries.txt` diff --git a/docs/data-collection.md b/docs/data-collection.md new file mode 100644 index 00000000..bfb39cc7 --- /dev/null +++ b/docs/data-collection.md @@ -0,0 +1,3 @@ +# OpenShift 4 Data Collection + +This documentation has moved to a [new location](https://github.com/openshift/cluster-monitoring-operator/blob/master/Documentation/data-collection.md). diff --git a/docs/sample-metrics.md b/docs/sample-metrics.md new file mode 100644 index 00000000..23c1feff --- /dev/null +++ b/docs/sample-metrics.md @@ -0,0 +1,3 @@ +# Sample Metrics + +This documentation has moved to a [new location](https://github.com/openshift/cluster-monitoring-operator/blob/master/Documentation/sample-metrics.md). diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..4adaf011 --- /dev/null +++ b/go.mod @@ -0,0 +1,35 @@ +module github.com/openshift/telemeter + +go 1.13 + +require ( + github.com/OneOfOne/xxhash v1.2.6 // indirect + github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect + github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b + github.com/coreos/go-oidc v2.0.0+incompatible + github.com/go-chi/chi v4.0.4+incompatible + github.com/go-kit/kit v0.9.0 + github.com/gogo/protobuf v1.3.1 + github.com/golang/protobuf v1.3.2 + github.com/golang/snappy v0.0.1 + github.com/grpc-ecosystem/grpc-gateway v1.12.1 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/oklog/run v1.0.0 + github.com/pkg/errors v0.8.1 + github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect + github.com/prometheus/client_golang v1.2.1 + github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 + github.com/prometheus/common v0.7.0 + github.com/prometheus/prometheus v1.8.2-0.20200110114423-1e64d757f711 // Prometheus master v2.14.0 + github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b + github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/spf13/cobra v0.0.3 + golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708 // indirect + golang.org/x/net v0.0.0-20191112182307-2180aed22343 // indirect + golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 + golang.org/x/time v0.0.0-20191024005414-555d28b269f0 + google.golang.org/appengine v1.6.5 // indirect + google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9 // indirect + google.golang.org/grpc v1.25.1 // indirect + gopkg.in/square/go-jose.v2 v2.0.0-20180411045311-89060dee6a84 +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..5eb0f087 --- /dev/null +++ b/go.sum @@ -0,0 +1,458 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +contrib.go.opencensus.io/exporter/ocagent v0.6.0/go.mod h1:zmKjrJcdo0aYcVS7bmEeSEBLPA9YJp5bjrofdU3pIXs= +github.com/Azure/azure-sdk-for-go v23.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/go-autorest v11.1.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest v11.2.8+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/OneOfOne/xxhash v1.2.6 h1:U68crOE3y3MPttCMQGywZOLrTeF5HHJ3/vDBCJn9/bA= +github.com/OneOfOne/xxhash v1.2.6/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= +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/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/aws/aws-sdk-go v1.25.48/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0= +github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= +github.com/cenkalti/backoff v0.0.0-20181003080854-62661b46c409/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v0.0.0-20181017004759-096ff4a8a059/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.0 h1:yTUvW7Vhb89inJ+8irsUqiWjh8iT6sQPZiQzI6ReGkA= +github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/go-oidc v2.0.0+incompatible h1:+RStIopZ8wooMx+Vs5Bt8zMXxV1ABl5LbakNExNmZIg= +github.com/coreos/go-oidc v2.0.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +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 v0.0.0-20160705203006-01aeca54ebda/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20190329191031-25c5027a8c7b/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +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/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +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-chi/chi v4.0.4+incompatible h1:7fVnpr0gAXG15uDbtH+LwSeMztvIvlHrBNRkTzgphS0= +github.com/go-chi/chi v4.0.4+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +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.17.2/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.17.2/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +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.17.2/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +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.17.2/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.17.2/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= +github.com/go-openapi/runtime v0.18.0/go.mod h1:uI6pHuxWYTy94zZxgcwJkUWa9wbIlhteGfloI10GD4U= +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.17.2/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.17.2/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.19.2/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.17.2/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/validate v0.17.2/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= +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/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.2.2-0.20190730201129-28a6bbf47e48/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 h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +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/mock v1.2.0/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.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20160524151835-7d79101e329e/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= +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 v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190723021845-34ac40c74b70/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gnostic v0.0.0-20170426233943-68f4ded48ba9/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4= +github.com/gophercloud/gophercloud v0.3.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= +github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.9.4/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.12.1 h1:zCy2xE9ablevUOrUZc3Dl72Dt+ya2FNAvC2yLYMHzi4= +github.com/grpc-ecosystem/grpc-gateway v1.12.1/go.mod h1:8XEsbTttt/W+VvjtQhLACqCisSPWTxCZ7sBRjU6iH9c= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +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/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/influxdata/influxdb v1.7.7/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY= +github.com/jessevdk/go-flags v0.0.0-20180331124232-1c38ed7ad0cc/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +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 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +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/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kylelemons/godebug v0.0.0-20160406211939-eadb3ce320cb/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +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/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.15/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +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/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/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/oklog/ulid v0.0.0-20170117200651-66bb6560562f/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +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.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v0.0.0-20190113212917-5533ce8a0da3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/opentracing-contrib/go-stdlib v0.0.0-20190519235532-cf7a6c988dc9/go.mod h1:PLldrQSroqzH70Xl+1DQcGnefIbqsKR7UDaiux3zV+w= +github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU= +github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/alertmanager v0.18.0/go.mod h1:WcxHBl40VSPuOaqWae6l6HpnEOVRIycEJ7i9iYkadEE= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.2.0/go.mod h1:XMU6Z2MjaRKVu/dC1qupJI9SiNkDYzz3xecMgSW/F+U= +github.com/prometheus/client_golang v1.2.1 h1:JnMpQc6ppsNgw9QPAGF6Dod479itz7lvlsMzzNayLOI= +github.com/prometheus/client_golang v1.2.1/go.mod h1:XMU6Z2MjaRKVu/dC1qupJI9SiNkDYzz3xecMgSW/F+U= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQlL8= +github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/prometheus v0.0.0-20180315085919-58e2a31db8de/go.mod h1:oAIUtOny2rjMX0OWN5vPR5/q/twIROJvdqnQKDdil/s= +github.com/prometheus/prometheus v1.8.2-0.20200110114423-1e64d757f711 h1:uEq+8hKI4kfycPLSKNw844YYkdMNpC2eZpov73AvlFk= +github.com/prometheus/prometheus v1.8.2-0.20200110114423-1e64d757f711/go.mod h1:7U90zPoLkWjEIQcy/rweQla82OCTUzxVHE51G3OhJbI= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/samuel/go-zookeeper v0.0.0-20190810000440-0ceca61e4d75/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/satori/go.uuid v0.0.0-20160603004225-b111a074d5ef/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b h1:gQZ0qzfKHQIybLANtM3mBXNUtOfsCFXeTsnBqCsx1KM= +github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/vfsgen v0.0.0-20180825020608-02ddb050ef6b/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= +github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +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/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= +go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/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-20191112222119-e1110fd1c708 h1:pXVtWnwHkrWD9ru3sDxY/qFK/bfc0egRovX91EjWjf4= +golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +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/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +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-20181023162649-9b4f9f5ad519/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-20181201002055-351d144fa1fc/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-20190206173232-65e2d4e15006/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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +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-20190628185345-da137c7871d7/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-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191112182307-2180aed22343 h1:00ohfJ4K98s3m6BGUoBd8nyfp4Yl0GoIKvw5abItTjI= +golang.org/x/net v0.0.0-20191112182307-2180aed22343/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/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/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-20180823144017-11551d06cbcc/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-20181026203630-95b1ffbd15a5/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-20190209173611-3b5209105503/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY= +golang.org/x/sys v0.0.0-20191010194322-b09406accb47/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.1-0.20180805044716-cb6730876b98/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +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-20161028155119-f51c12702a4d/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/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-20190118193359-16909d206f00/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-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190918214516-5a1a30219888/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9 h1:6XzpBoANz1NqMNfDXzc2QmHmbb1vyMsvRfoP5rM+K1I= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +google.golang.org/grpc v1.25.1 h1:wdKvqQk7IttEw92GoRyKG2IDrUIpgpj6H6m81yfeMW0= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +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 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +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/fsnotify/fsnotify.v1 v1.4.7/go.mod h1:Fyux9zXlo4rWoMSIzpn9fDAYjalPqJ/K1qJ27s+7ltE= +gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.0.0-20180411045311-89060dee6a84 h1:ELQJ5WuT+ydETLCpWvAuw8iGBQRGoJq+A3RAbbAcZUY= +gopkg.in/square/go-jose.v2 v2.0.0-20180411045311-89060dee6a84/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +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.3 h1:fvjTMHxHEw/mxHbtzPi3JCcKXQRAnQTBRo6YCJSVHKI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/api v0.0.0-20190620084959-7cf5895f2711/go.mod h1:TBhBqb1AWbBQbW3XRusr7n7E4v2+5ZY8r8sAMnyFC5A= +k8s.io/api v0.0.0-20190813020757-36bff7324fb7/go.mod h1:3Iy+myeAORNCLgjd/Xu9ebwN7Vh59Bw0vh9jhoX+V58= +k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719/go.mod h1:I4A+glKBHiTgiEjQiCCQfCAIcIMFGt291SmsvcrFzJA= +k8s.io/apimachinery v0.0.0-20190809020650-423f5d784010/go.mod h1:Waf/xTS2FGRrgXCkO5FP3XxTOWh0qLf2QhL1qFZZ/R8= +k8s.io/client-go v0.0.0-20190620085101-78d2af792bab/go.mod h1:E95RaSlHr79aHaX0aGSwcPNfygDiPKOVXdmivCIZT0k= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.4.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= +k8s.io/kube-openapi v0.0.0-20190709113604-33be087ad058/go.mod h1:nfDlWeOsu3pUf4yWGL+ERqohP4YsZcBJXWMK+gkzOA4= +k8s.io/utils v0.0.0-20190221042446-c2654d5206da/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/jsonnet/.gitignore b/jsonnet/.gitignore new file mode 100644 index 00000000..48b8bf90 --- /dev/null +++ b/jsonnet/.gitignore @@ -0,0 +1 @@ +vendor/ diff --git a/jsonnet/benchmark.jsonnet b/jsonnet/benchmark.jsonnet new file mode 100644 index 00000000..15f0fca5 --- /dev/null +++ b/jsonnet/benchmark.jsonnet @@ -0,0 +1,10 @@ +function(metrics=[]) + local t = (import 'telemeter/benchmark.libsonnet') { config+: { telemeterServer+: { whitelist+: metrics } } }; + { [name + 'TelemeterServer']: t.telemeterServer[name] for name in std.objectFields(t.telemeterServer) } + + { [name + 'ThanosReceiveController']: t.thanosReceiveController[name] for name in std.objectFields(t.thanosReceiveController) } + + { + [name + 'ThanosReceive' + hashring]: t.receivers[hashring][name] + for hashring in std.objectFields(t.receivers) + for name in std.objectFields(t.receivers[hashring]) + } + + { [name + 'ThanosQuery']: t.query[name] for name in std.objectFields(t.query) } diff --git a/jsonnet/client.jsonnet b/jsonnet/client.jsonnet new file mode 100644 index 00000000..481fe725 --- /dev/null +++ b/jsonnet/client.jsonnet @@ -0,0 +1,3 @@ +local t = (import 'telemeter/client.libsonnet'); + +{ [name]: t.telemeterClient[name] for name in std.objectFields(t.telemeterClient) } diff --git a/jsonnet/jsonnetfile.json b/jsonnet/jsonnetfile.json new file mode 100644 index 00000000..05717f48 --- /dev/null +++ b/jsonnet/jsonnetfile.json @@ -0,0 +1,33 @@ +{ + "version": 1, + "dependencies": [ + { + "source": { + "git": { + "remote": "https://github.com/observatorium/thanos-receive-controller.git", + "subdir": "jsonnet/lib" + } + }, + "version": "master", + "name": "thanos-receive-controller" + }, + { + "source": { + "git": { + "remote": "https://github.com/thanos-io/kube-thanos.git", + "subdir": "jsonnet/kube-thanos" + } + }, + "version": "master" + }, + { + "source": { + "local": { + "directory": "telemeter" + } + }, + "version": "" + } + ], + "legacyImports": true +} diff --git a/jsonnet/jsonnetfile.lock.json b/jsonnet/jsonnetfile.lock.json new file mode 100644 index 00000000..e3cf698b --- /dev/null +++ b/jsonnet/jsonnetfile.lock.json @@ -0,0 +1,46 @@ +{ + "version": 1, + "dependencies": [ + { + "source": { + "git": { + "remote": "https://github.com/ksonnet/ksonnet-lib.git", + "subdir": "" + } + }, + "version": "0d2f82676817bbf9e4acf6495b2090205f323b9f", + "sum": "h28BXZ7+vczxYJ2sCt8JuR9+yznRtU/iA6DCpQUrtEg=", + "name": "ksonnet" + }, + { + "source": { + "git": { + "remote": "https://github.com/observatorium/thanos-receive-controller.git", + "subdir": "jsonnet/lib" + } + }, + "version": "11e63ca1d3b9834e66ee85107df002bde29a4a59", + "sum": "iUbwzPZ/0QrozWTfoXlmRhd1CMAnkcCaWtATTlSnoMk=", + "name": "thanos-receive-controller" + }, + { + "source": { + "git": { + "remote": "https://github.com/thanos-io/kube-thanos.git", + "subdir": "jsonnet/kube-thanos" + } + }, + "version": "5e7af669ee35af6c8beaa37e7a30d865368d75d8", + "sum": "kyza6itqcvQHoKeJqsFjFQuMpj8RZsudC5/2vVxw5R0=" + }, + { + "source": { + "local": { + "directory": "telemeter" + } + }, + "version": "" + } + ], + "legacyImports": false +} diff --git a/jsonnet/server.jsonnet b/jsonnet/server.jsonnet new file mode 100644 index 00000000..7626d590 --- /dev/null +++ b/jsonnet/server.jsonnet @@ -0,0 +1,4 @@ +local t = (import 'telemeter/server.libsonnet'); + +{ [name]: t.telemeterServer[name] for name in std.objectFields(t.telemeterServer) } + +{ [name + 'Memcached']: t.memcached[name] for name in std.objectFields(t.memcached) if t.memcached.replicas > 0 } diff --git a/jsonnet/telemeter/benchmark.libsonnet b/jsonnet/telemeter/benchmark.libsonnet new file mode 100644 index 00000000..4b0cabff --- /dev/null +++ b/jsonnet/telemeter/benchmark.libsonnet @@ -0,0 +1,65 @@ +(import 'benchmark/kubernetes.libsonnet') { + local b = self, + config+:: { + local defaultConfig = self, + namespace: 'telemeter-benchmark', + name: 'benchmark', + thanosVersion: 'master-2020-02-13-adfef4b5', + thanosImage: 'quay.io/thanos/thanos:' + defaultConfig.thanosVersion, + hashrings: [ + { + hashring: 'default', + tenants: [ + ], + }, + ], + objectStorageConfig: { + name: 'thanos-objectstorage', + key: 'thanos.yaml', + }, + commonLabels: { + 'app.kubernetes.io/part-of': 'telemeter-benchmark', + }, + thanosReceiveController+: { + local trcConfig = self, + version: 'master-2020-02-06-b66e0c8', + image: 'quay.io/observatorium/thanos-receive-controller:' + trcConfig.version, + hashrings: defaultConfig.hashrings, + }, + receivers+: { + image: defaultConfig.thanosImage, + version: defaultConfig.thanosVersion, + hashrings: defaultConfig.hashrings, + objectStorageConfig: defaultConfig.objectStorageConfig, + replicas: 3, + }, + query: { + image: defaultConfig.thanosImage, + version: defaultConfig.thanosVersion, + }, + telemeterServer+: { + image: 'quay.io/openshift/origin-telemeter:v4.0', + replicas: 10, + whitelist: [], + }, + }, + + thanosReceiveController+:: { + config+:: b.config.thanosReceiveController, + }, + + telemeterServer+:: { + config+:: b.config.telemeterServer, + }, + + receivers+:: { + [hashring.hashring]+: { + config+:: b.config.receivers, + } + for hashring in b.config.hashrings + }, + + query+:: { + config+:: b.config.query, + }, +} diff --git a/jsonnet/telemeter/benchmark/kubernetes.libsonnet b/jsonnet/telemeter/benchmark/kubernetes.libsonnet new file mode 100644 index 00000000..ad9a5090 --- /dev/null +++ b/jsonnet/telemeter/benchmark/kubernetes.libsonnet @@ -0,0 +1,241 @@ +local k = import 'ksonnet/ksonnet.beta.4/k.libsonnet'; +local t = (import 'kube-thanos/thanos.libsonnet'); +local secretName = 'telemeter-server'; +local secretMountPath = '/etc/telemeter'; +local secretVolumeName = 'secret-telemeter-server'; +local tlsSecret = 'telemeter-server-shared'; +local tlsVolumeName = 'telemeter-server-tls'; +local tlsMountPath = '/etc/pki/service'; +local authorizePort = 8083; +local externalPort = 8080; +local internalPort = 8081; +local tokensFileName = 'tokens.json'; + +{ + local b = self, + + config+:: { + telemeterServer+:: { + authorizeURL: 'http://localhost:' + authorizePort, + }, + }, + + telemeterServer+:: { + local ts = self, + route: { + apiVersion: 'v1', + kind: 'Route', + metadata: { + name: 'telemeter-server', + namespace: b.config.namespace, + }, + spec: { + to: { + kind: 'Service', + name: 'telemeter-server', + }, + port: { + targetPort: 'external', + }, + tls: { + termination: 'edge', + insecureEdgeTerminationPolicy: 'Allow', + }, + }, + }, + + statefulSet: + local statefulSet = k.apps.v1.statefulSet; + local container = k.apps.v1.statefulSet.mixin.spec.template.spec.containersType; + local volume = k.apps.v1.statefulSet.mixin.spec.template.spec.volumesType; + local containerPort = container.portsType; + local containerVolumeMount = container.volumeMountsType; + local containerEnv = container.envType; + + local podLabels = { 'k8s-app': 'telemeter-server' }; + local tlsMount = containerVolumeMount.new(tlsVolumeName, tlsMountPath); + local tlsVolume = volume.fromSecret(tlsVolumeName, tlsSecret); + local name = containerEnv.fromFieldPath('NAME', 'metadata.name'); + local secretMount = containerVolumeMount.new(secretVolumeName, secretMountPath); + local secretVolume = volume.fromSecret(secretVolumeName, secretName); + + local whitelist = std.map( + function(rule) '--whitelist=%s' % std.strReplace(rule, 'ALERTS', 'alerts'), + ts.config.whitelist + ); + + local telemeterServer = + container.new('telemeter-server', ts.config.image) + + container.withCommand([ + '/usr/bin/telemeter-server', + '--listen=0.0.0.0:' + externalPort, + '--listen-internal=0.0.0.0:' + internalPort, + '--shared-key=%s/tls.key' % tlsMountPath, + '--authorize=' + b.config.telemeterServer.authorizeURL, + '--forward-url=http://%s.%s.svc:19291/api/v1/receive' % [b.receivers[b.config.hashrings[0].hashring].config.name, b.config.namespace], + ] + whitelist) + + container.withPorts([ + containerPort.newNamed(externalPort, 'external'), + containerPort.newNamed(internalPort, 'internal'), + ]) + + container.withVolumeMounts([secretMount, tlsMount]) + + container.withEnv([name]) + { + livenessProbe: { + httpGet: { + path: '/healthz', + port: externalPort, + scheme: 'HTTP', + }, + }, + readinessProbe: { + httpGet: { + path: '/healthz/ready', + port: externalPort, + scheme: 'HTTP', + }, + }, + }; + + local authorizationServer = + container.new('authorization-server', ts.config.image) + + container.withCommand([ + '/usr/bin/authorization-server', + 'localhost:' + authorizePort, + '%s/%s' % [secretMountPath, tokensFileName], + ]) + + container.withVolumeMounts([secretMount]); + + statefulSet.new('telemeter-server', ts.config.replicas, [telemeterServer, authorizationServer], [], podLabels) + + statefulSet.mixin.metadata.withNamespace(b.config.namespace) + + statefulSet.mixin.spec.selector.withMatchLabels(podLabels) + + statefulSet.mixin.spec.withPodManagementPolicy('Parallel') + + statefulSet.mixin.spec.withServiceName('telemeter-server') + + statefulSet.mixin.spec.template.spec.withServiceAccountName('telemeter-server') + + statefulSet.mixin.spec.template.spec.withVolumes([secretVolume, tlsVolume]) + + { + spec+: { + volumeClaimTemplates:: null, + }, + }, + + secret: + local secret = k.core.v1.secret; + + secret.new(secretName, { + [tokensFileName]: std.base64(std.toString([{ token: 'benchmark' }])), + }) + + secret.mixin.metadata.withNamespace(b.config.namespace) + + secret.mixin.metadata.withLabels({ 'k8s-app': 'telemeter-server' }), + + service: + local service = k.core.v1.service; + local servicePort = k.core.v1.service.mixin.spec.portsType; + + local servicePortExternal = servicePort.newNamed('external', externalPort, 'external'); + local servicePortInternal = servicePort.newNamed('internal', internalPort, 'internal'); + + service.new('telemeter-server', ts.statefulSet.spec.selector.matchLabels, [servicePortExternal, servicePortInternal]) + + service.mixin.metadata.withNamespace(b.config.namespace) + + service.mixin.metadata.withLabels({ 'k8s-app': 'telemeter-server' }) + + service.mixin.spec.withClusterIp('None') + + service.mixin.metadata.withAnnotations({ + 'service.alpha.openshift.io/serving-cert-secret-name': tlsSecret, + }), + + serviceAccount: + local serviceAccount = k.core.v1.serviceAccount; + + serviceAccount.new('telemeter-server') + + serviceAccount.mixin.metadata.withNamespace(b.config.namespace), + }, + + thanosReceiveController:: (import 'thanos-receive-controller/thanos-receive-controller.libsonnet') + { + config+:: { + local cfg = self, + name: b.config.name + '-' + cfg.commonLabels['app.kubernetes.io/name'], + namespace: b.config.namespace, + replicas: 1, + commonLabels+:: b.config.commonLabels, + }, + }, + + receivers:: { + [hashring.hashring]: + t.receive + + t.receive.withRetention + + t.receive.withHashringConfigMap + { + config+:: { + local cfg = self, + name: b.config.name + '-' + cfg.commonLabels['app.kubernetes.io/name'] + '-' + hashring.hashring, + namespace: b.config.namespace, + replicas: 3, + replicationFactor: 3, + retention: '6h', + hashringConfigMapName: '%s-generated' % b.thanosReceiveController.configmap.metadata.name, + commonLabels+:: + b.config.commonLabels { + 'controller.receive.thanos.io/hashring': hashring.hashring, + }, + }, + statefulSet+: { + metadata+: { + labels+: { + 'controller.receive.thanos.io': 'thanos-receive-controller', + }, + }, + spec+: { + template+: { + spec+: { + containers: [ + if c.name == 'thanos-receive' then c { + args: std.filter(function(a) !std.startsWith(a, '--objstore'), super.args), + env: std.filter(function(e) e.name != 'OBJSTORE_CONFIG', super.env), + } else c + for c in super.containers + ], + }, + }, + }, + }, + } + for hashring in b.config.hashrings + }, + + query:: t.query { + config+:: { + local cfg = self, + name: b.config.name + '-' + cfg.commonLabels['app.kubernetes.io/name'], + namespace: b.config.namespace, + commonLabels+:: b.config.commonLabels, + replicas: 1, + stores: [ + 'dnssrv+_grpc._tcp.%s.%s.svc.cluster.local' % [service.metadata.name, service.metadata.namespace] + for service in + [b.receivers[hashring].service for hashring in std.objectFields(b.receivers)] + ], + replicaLabels: ['prometheus_replica', 'ruler_replica', 'replica'], + }, + route: { + apiVersion: 'v1', + kind: 'Route', + metadata: { + name: b.query.config.name, + namespace: b.config.namespace, + labels: b.query.config.commonLabels, + }, + spec: { + to: { + kind: 'Service', + name: b.query.config.name, + }, + port: { + targetPort: 'web', + }, + tls: { + termination: 'edge', + insecureEdgeTerminationPolicy: 'Allow', + }, + }, + }, + }, +} diff --git a/jsonnet/telemeter/client.libsonnet b/jsonnet/telemeter/client.libsonnet new file mode 100644 index 00000000..6e9643f3 --- /dev/null +++ b/jsonnet/telemeter/client.libsonnet @@ -0,0 +1,10 @@ +(import 'client/kubernetes.libsonnet') + { + _config+:: { + jobs+: { + TelemeterClient: 'job="telemeter-client"', + }, + telemeterClient+: { + matchRules+: [], + }, + }, +} diff --git a/jsonnet/telemeter/client/kubernetes.libsonnet b/jsonnet/telemeter/client/kubernetes.libsonnet new file mode 100644 index 00000000..39e2318f --- /dev/null +++ b/jsonnet/telemeter/client/kubernetes.libsonnet @@ -0,0 +1,237 @@ +local k = import 'ksonnet/ksonnet.beta.4/k.libsonnet'; +local secretName = 'telemeter-client'; +local secretVolumeName = 'secret-telemeter-client'; +local secretMountPath = '/etc/telemeter'; +local tlsSecret = 'telemeter-client-tls'; +local tlsVolumeName = 'telemeter-client-tls'; +local tlsMountPath = '/etc/tls/private'; +local servingCertsCABundle = 'serving-certs-ca-bundle'; +local servingCertsCABundleFileName = 'service-ca.crt'; +local servingCertsCABundleMountPath = '/etc/%s' % servingCertsCABundle; +local fromTokenFile = '/var/run/secrets/kubernetes.io/serviceaccount/token'; +local insecurePort = 8080; +local securePort = 8443; + +{ + _config+:: { + namespace: 'openshift-monitoring', + + telemeterClient+:: { + anonymizeLabels: [], + from: 'https://prometheus-k8s.%(namespace)s.svc:9091' % $._config, + id: '', + matchRules: [], + salt: '', + serverName: 'server-name-replaced-at-runtime', + to: 'https://infogw.api.openshift.com', + token: '', + }, + + versions+:: { + // TODO(squat): change this to v4.0 once that image is built + configmapReload: 'v3.11', + kubeRbacProxy: 'v0.3.1', + telemeterClient: 'v4.0', + }, + + imageRepos+:: { + configmapReload: 'quay.io/openshift/origin-configmap-reload', + kubeRbacProxy: 'quay.io/coreos/kube-rbac-proxy', + telemeterClient: 'quay.io/openshift/origin-telemeter', + }, + }, + + telemeterClient+:: { + clusterRoleBinding: + local clusterRoleBinding = k.rbac.v1.clusterRoleBinding; + + clusterRoleBinding.new() + + clusterRoleBinding.mixin.metadata.withName('telemeter-client') + + clusterRoleBinding.mixin.roleRef.withApiGroup('rbac.authorization.k8s.io') + + clusterRoleBinding.mixin.roleRef.withName('telemeter-client') + + clusterRoleBinding.mixin.roleRef.mixinInstance({ kind: 'ClusterRole' }) + + clusterRoleBinding.withSubjects([{ kind: 'ServiceAccount', name: 'telemeter-client', namespace: $._config.namespace }]), + + clusterRole: + local clusterRole = k.rbac.v1.clusterRole; + local policyRule = clusterRole.rulesType; + + local authenticationRule = policyRule.new() + + policyRule.withApiGroups(['authentication.k8s.io']) + + policyRule.withResources([ + 'tokenreviews', + ]) + + policyRule.withVerbs(['create']); + + local authorizationRule = policyRule.new() + + policyRule.withApiGroups(['authorization.k8s.io']) + + policyRule.withResources([ + 'subjectaccessreviews', + ]) + + policyRule.withVerbs(['create']); + + clusterRole.new() + + clusterRole.mixin.metadata.withName('telemeter-client') + + clusterRole.withRules([authenticationRule, authorizationRule]), + + clusterRoleBindingView: + local clusterRoleBinding = k.rbac.v1.clusterRoleBinding; + + clusterRoleBinding.new() + + clusterRoleBinding.mixin.metadata.withName('telemeter-client-view') + + clusterRoleBinding.mixin.roleRef.withApiGroup('rbac.authorization.k8s.io') + + clusterRoleBinding.mixin.roleRef.withName('cluster-monitoring-view') + + clusterRoleBinding.mixin.roleRef.mixinInstance({ kind: 'ClusterRole' }) + + clusterRoleBinding.withSubjects([{ kind: 'ServiceAccount', name: 'telemeter-client', namespace: $._config.namespace }]), + + deployment: + local deployment = k.apps.v1.deployment; + local container = k.apps.v1.deployment.mixin.spec.template.spec.containersType; + local volume = k.apps.v1.deployment.mixin.spec.template.spec.volumesType; + local containerPort = container.portsType; + local containerVolumeMount = container.volumeMountsType; + local containerEnv = container.envType; + + local podLabels = { 'k8s-app': 'telemeter-client' }; + local secretMount = containerVolumeMount.new(secretVolumeName, secretMountPath); + local secretVolume = volume.fromSecret(secretVolumeName, secretName); + local tlsMount = containerVolumeMount.new(tlsVolumeName, tlsMountPath); + local tlsVolume = volume.fromSecret(tlsVolumeName, tlsSecret); + local sccabMount = containerVolumeMount.new(servingCertsCABundle, servingCertsCABundleMountPath); + local sccabVolume = volume.withName(servingCertsCABundle) + volume.mixin.configMap.withName('telemeter-client-serving-certs-ca-bundle'); + local anonymize = containerEnv.new('ANONYMIZE_LABELS', std.join(',', $._config.telemeterClient.anonymizeLabels)); + local from = containerEnv.new('FROM', $._config.telemeterClient.from); + local id = containerEnv.new('ID', ''); + local to = containerEnv.new('TO', $._config.telemeterClient.to); + local httpProxy = containerEnv.new('HTTP_PROXY', ''); + local httpsProxy = containerEnv.new('HTTPS_PROXY', ''); + local noProxy = containerEnv.new('NO_PROXY', ''); + + local matchRules = std.map( + function(rule) '--match=%s' % rule, + $._config.telemeterClient.matchRules + ); + + local telemeterClient = + container.new('telemeter-client', $._config.imageRepos.telemeterClient + ':' + $._config.versions.telemeterClient) + + container.withCommand([ + '/usr/bin/telemeter-client', + '--id=$(ID)', + '--from=$(FROM)', + '--from-ca-file=%s/%s' % [servingCertsCABundleMountPath, servingCertsCABundleFileName], + '--from-token-file=' + fromTokenFile, + '--to=$(TO)', + '--to-token-file=%s/token' % secretMountPath, + '--listen=localhost:' + insecurePort, + '--anonymize-salt-file=%s/salt' % secretMountPath, + '--anonymize-labels=$(ANONYMIZE_LABELS)', + ] + matchRules) + + container.withPorts(containerPort.newNamed(insecurePort, 'http')) + + container.withVolumeMounts([sccabMount, secretMount]) + + container.withEnv([anonymize, from, id, to, httpProxy, httpsProxy, noProxy]) + + container.mixin.resources.withRequests({ cpu: '1m' }); + + local reload = + container.new('reload', $._config.imageRepos.configmapReload + ':' + $._config.versions.configmapReload) + + container.withArgs([ + '--webhook-url=http://localhost:%s/-/reload' % insecurePort, + '--volume-dir=' + servingCertsCABundleMountPath, + ]) + + container.withVolumeMounts([sccabMount]) + + container.mixin.resources.withRequests({ cpu: '1m' }); + + local proxy = + container.new('kube-rbac-proxy', $._config.imageRepos.kubeRbacProxy + ':' + $._config.versions.kubeRbacProxy) + + container.withArgs([ + '--secure-listen-address=:' + securePort, + '--upstream=http://127.0.0.1:%s/' % insecurePort, + '--tls-cert-file=%s/tls.crt' % tlsMountPath, + '--tls-private-key-file=%s/tls.key' % tlsMountPath, + ] + if std.objectHas($._config, 'tlsCipherSuites') then [ + '--tls-cipher-suites=' + std.join(',', $._config.tlsCipherSuites), + ] else []) + + container.withPorts(containerPort.new(securePort) + containerPort.withName('https')) + + container.mixin.resources.withRequests({ cpu: '1m', memory: '20Mi' }) + + container.withVolumeMounts([tlsMount]); + + + deployment.new('telemeter-client', 1, [telemeterClient, reload, proxy], podLabels) + + deployment.mixin.metadata.withNamespace($._config.namespace) + + deployment.mixin.metadata.withLabels(podLabels) + + deployment.mixin.spec.selector.withMatchLabels(podLabels) + + deployment.mixin.spec.template.spec.withServiceAccountName('telemeter-client') + + deployment.mixin.spec.template.spec.withPriorityClassName('system-cluster-critical') + + deployment.mixin.spec.template.spec.withNodeSelector({ 'beta.kubernetes.io/os': 'linux' }) + + deployment.mixin.spec.template.spec.withVolumes([sccabVolume, secretVolume, tlsVolume]), + + secret: + local secret = k.core.v1.secret; + + secret.new(secretName, { + salt: std.base64($._config.telemeterClient.salt), + token: std.base64($._config.telemeterClient.token), + }) + + secret.mixin.metadata.withNamespace($._config.namespace) + + secret.mixin.metadata.withLabels({ 'k8s-app': 'telemeter-client' }), + + service: + local service = k.core.v1.service; + local servicePort = k.core.v1.service.mixin.spec.portsType; + + local servicePortHTTPS = servicePort.newNamed('https', securePort, 'https'); + + service.new('telemeter-client', $.telemeterClient.deployment.spec.selector.matchLabels, [servicePortHTTPS]) + + service.mixin.metadata.withNamespace($._config.namespace) + + service.mixin.metadata.withLabels({ 'k8s-app': 'telemeter-client' }) + + service.mixin.spec.withClusterIp('None') + + service.mixin.metadata.withAnnotations({ + 'service.alpha.openshift.io/serving-cert-secret-name': tlsSecret, + }), + + serviceAccount: + local serviceAccount = k.core.v1.serviceAccount; + + serviceAccount.new('telemeter-client') + + serviceAccount.mixin.metadata.withNamespace($._config.namespace), + + serviceMonitor: + { + apiVersion: 'monitoring.coreos.com/v1', + kind: 'ServiceMonitor', + metadata: { + name: 'telemeter-client', + namespace: $._config.namespace, + labels: { + 'k8s-app': 'telemeter-client', + }, + }, + spec: { + jobLabel: 'k8s-app', + selector: { + matchLabels: { + 'k8s-app': 'telemeter-client', + }, + }, + endpoints: [ + { + bearerTokenFile: '/var/run/secrets/kubernetes.io/serviceaccount/token', + interval: '30s', + port: 'https', + scheme: 'https', + tlsConfig: { + caFile: '/etc/prometheus/configmaps/serving-certs-ca-bundle/%s' % servingCertsCABundleFileName, + serverName: $._config.telemeterClient.serverName, + }, + }, + ], + }, + }, + + servingCertsCABundle+: + local configmap = k.core.v1.configMap; + + configmap.new('telemeter-client-serving-certs-ca-bundle', { [servingCertsCABundleFileName]: '' }) + + configmap.mixin.metadata.withNamespace($._config.namespace) + + configmap.mixin.metadata.withAnnotations({ 'service.alpha.openshift.io/inject-cabundle': 'true' }), + }, +} diff --git a/jsonnet/telemeter/jsonnetfile.json b/jsonnet/telemeter/jsonnetfile.json new file mode 100644 index 00000000..d31ac110 --- /dev/null +++ b/jsonnet/telemeter/jsonnetfile.json @@ -0,0 +1,14 @@ +{ + "dependencies": [ + { + "name": "ksonnet", + "source": { + "git": { + "remote": "https://github.com/ksonnet/ksonnet-lib", + "subdir": "" + } + }, + "version": "master" + } + ] +} diff --git a/jsonnet/telemeter/lib/list.libsonnet b/jsonnet/telemeter/lib/list.libsonnet new file mode 100644 index 00000000..2e2a68db --- /dev/null +++ b/jsonnet/telemeter/lib/list.libsonnet @@ -0,0 +1,191 @@ +{ + asList(name, data, parameters):: { + apiVersion: 'v1', + kind: 'Template', + metadata: { + name: name, + }, + objects: [data[k] for k in std.objectFields(data)], + parameters: parameters, + }, + + withResourceRequestsAndLimits(containerName, requests, limits):: { + local varContainerName = std.strReplace(containerName, '-', '_'), + local setResourceRequestsAndLimits(object) = + if object.kind == 'StatefulSet' then { + spec+: { + template+: { + spec+: { + containers: [ + if c.name == containerName then + c { + resources: { + requests: { + cpu: '${' + std.asciiUpper(varContainerName) + '_CPU_REQUEST}', + memory: '${' + std.asciiUpper(varContainerName) + '_MEMORY_REQUEST}', + }, + limits: { + cpu: '${' + std.asciiUpper(varContainerName) + '_CPU_LIMIT}', + memory: '${' + std.asciiUpper(varContainerName) + '_MEMORY_LIMIT}', + }, + }, + } + else c + for c in super.containers + ], + }, + }, + }, + } + else {}, + objects: [ + o + setResourceRequestsAndLimits(o) + for o in super.objects + ], + parameters+: [ + { name: std.asciiUpper(varContainerName) + '_CPU_REQUEST', value: if std.objectHas(requests, 'cpu') then requests.cpu else '100m' }, + { name: std.asciiUpper(varContainerName) + '_CPU_LIMIT', value: if std.objectHas(limits, 'cpu') then limits.cpu else '1' }, + { name: std.asciiUpper(varContainerName) + '_MEMORY_REQUEST', value: if std.objectHas(requests, 'memory') then requests.memory else '500Mi' }, + { name: std.asciiUpper(varContainerName) + '_MEMORY_LIMIT', value: if std.objectHas(limits, 'memory') then limits.memory else '1Gi' }, + ], + }, + + withAuthorizeURL(_config):: { + local setAuthorizeURL(object) = + if object.kind == 'StatefulSet' then { + spec+: { + template+: { + spec+: { + containers: [ + c { + command: [ + if std.startsWith(c, '--authorize=') then '--authorize=${AUTHORIZE_URL}' else c + for c in super.command + ], + } + for c in super.containers + ], + }, + }, + }, + } + else {}, + objects: [ + o + setAuthorizeURL(o) + for o in super.objects + ], + parameters+: [ + { name: 'AUTHORIZE_URL', value: _config.telemeterServer.authorizeURL }, + ], + }, + + withServerImage(_config):: { + local setImage(object) = + if object.kind == 'StatefulSet' then { + spec+: { + template+: { + spec+: { + containers: [ + c { + image: if c.name == 'telemeter-server' then '${IMAGE}:${IMAGE_TAG}' else c.image, + } + for c in super.containers + ], + }, + }, + }, + } + else {}, + objects: [ + o + setImage(o) + for o in super.objects + ], + parameters+: [ + { name: 'IMAGE', value: _config.imageRepos.telemeterServer }, + { name: 'IMAGE_TAG', value: _config.versions.telemeterServer }, + ], + }, + + withNamespace(_config):: { + local setNamespace(object) = + if std.objectHas(object, 'metadata') && std.objectHas(object.metadata, 'namespace') then { + metadata+: { + namespace: '${NAMESPACE}', + }, + } + else {}, + local setSubjectNamespace(object) = + if std.endsWith(object.kind, 'Binding') then { + subjects: [ + s { namespace: '${NAMESPACE}' } + for s in super.subjects + ], + } + else {}, + local setClusterRoleRuleNamespace(object) = + if object.kind == 'ClusterRole' then { + rules: [ + r + if std.objectHas(r, 'resources') && r.resources[0] == 'namespaces' then { + resourceNames: ['${NAMESPACE}'], + } else {} + for r in super.rules + ], + } + else {}, + local setServiceMonitorServerNameNamespace(object) = + if object.kind == 'ServiceMonitor' then { + spec+: { + endpoints: [ + e + if std.objectHas(e, 'tlsConfig') && !std.objectHas(e.tlsConfig, 'insecureSkipVerify') then { + tlsConfig+: if std.length(std.split(super.tlsConfig.serverName, '.')) == 3 && std.split(super.tlsConfig.serverName, '.')[1] == _config.namespace && std.split(e.tlsConfig.serverName, '.')[2] == 'svc' then { + serverName: '%s.%s.svc' % [std.split(e.tlsConfig.serverName, '.')[0], '${NAMESPACE}'], + } else {}, + } else {} + for e in super.endpoints + ], + }, + } + else {}, + local namespaceNonNamespacedObjects(object) = + (if std.objectHas(object, 'metadata') && !std.objectHas(object.metadata, 'namespace') && std.objectHas(object.metadata, 'name') then { + metadata+: { + name: '%s-${NAMESPACE}' % super.name, + }, + } + else {}) + + (if object.kind == 'ClusterRoleBinding' then { + roleRef+: { + name: '%s-${NAMESPACE}' % super.name, + }, + } + else {}), + + local setMemcachedNamespace(object) = + if object.kind == 'StatefulSet' then { + spec+: { + template+: { + spec+: { + containers: [ + c + (if std.objectHas(c, 'command') then { + command: [ + if std.startsWith(c, '--memcached=') then std.join('.', std.splitLimit(c, '.', 2)[:2] + ['${NAMESPACE}'] + [std.splitLimit(c, '.', 3)[3]]) else c + for c in super.command + ], + } else {}) + for c in super.containers + ], + }, + }, + }, + } + else {}, + + objects: [ + o + setNamespace(o) + setSubjectNamespace(o) + setServiceMonitorServerNameNamespace(o) + setClusterRoleRuleNamespace(o) + namespaceNonNamespacedObjects(o) + setMemcachedNamespace(o) + for o in super.objects + ], + parameters+: [ + { name: 'NAMESPACE', value: _config.namespace }, + ], + }, +} diff --git a/jsonnet/telemeter/rules.libsonnet b/jsonnet/telemeter/rules.libsonnet new file mode 100644 index 00000000..af2c2c7f --- /dev/null +++ b/jsonnet/telemeter/rules.libsonnet @@ -0,0 +1,44 @@ +{ + prometheus+:: { + recordingrules+: { + groups+: [ + { + name: 'telemeter.rules', + interval: '1m', + rules: [ + { + record: 'name_reason:cluster_operator_degraded:count', + expr: ||| + count by (name,reason) (cluster_operator_conditions{condition="Degraded"} == 1) + |||, + }, + { + record: 'name_reason:cluster_operator_unavailable:count', + expr: ||| + count by (name,reason) (cluster_operator_conditions{condition="Available"} == 0) + |||, + }, + { + record: 'id_code:apiserver_request_error_rate_sum:max', + expr: ||| + sort_desc(max by (_id,code) (code:apiserver_request_count:rate:sum{code=~"(4|5)\\d\\d"}) > 0.5) + |||, + }, + { + record: 'id_version:cluster_available', + expr: ||| + bottomk by (_id) (1, max by (_id, version) (0 * cluster_version{type="failure"}) or max by (_id, version) (1 + 0 * cluster_version{type="current"})) + |||, + }, + { + record: 'id_version_ebs_account_internal:cluster_subscribed', + expr: ||| + topk by (_id) (1, max by (_id, managed, ebs_account, internal) (label_replace(label_replace((subscription_labels{support=~"Standard|Premium|Layered"} * 0 + 1) or subscription_labels * 0, "internal", "true", "email_domain", "redhat.com|(.*\\.|^)ibm.com"), "managed", "", "managed", "false")) + on(_id) group_left(version) (topk by (_id) (1, 0*cluster_version{type="current"}))) + |||, + }, + ], + }, + ], + }, + }, +} diff --git a/jsonnet/telemeter/server.libsonnet b/jsonnet/telemeter/server.libsonnet new file mode 100644 index 00000000..d19ba194 --- /dev/null +++ b/jsonnet/telemeter/server.libsonnet @@ -0,0 +1,55 @@ +local list = import 'lib/list.libsonnet'; + +(import 'server/kubernetes.libsonnet') + { + local ts = super.telemeterServer, + local m = super.memcached, + local tsList = list.asList('telemeter', ts, []) + + list.withAuthorizeURL($._config) + + list.withNamespace($._config) + + list.withServerImage($._config) + + list.withResourceRequestsAndLimits('telemeter-server', $._config.telemeterServer.resourceRequests, $._config.telemeterServer.resourceLimits), + local mList = list.asList('memcached', m, [ + { + name: 'MEMCACHED_IMAGE', + value: m.images.memcached, + }, + { + name: 'MEMCACHED_IMAGE_TAG', + value: m.tags.memcached, + }, + { + name: 'MEMCACHED_EXPORTER_IMAGE', + value: m.images.exporter, + }, + { + name: 'MEMCACHED_EXPORTER_IMAGE_TAG', + value: m.tags.exporter, + }, + ]) + + list.withResourceRequestsAndLimits('memcached', $.memcached.resourceRequests, $.memcached.resourceLimits) + + list.withNamespace($._config), + + telemeterServer+:: { + list: list.asList('telemeter', {}, []) + { + objects: + tsList.objects + + mList.objects, + + parameters: + tsList.parameters + + mList.parameters, + }, + }, +} + { + _config+:: { + jobs+: { + TelemeterServer: 'job="telemeter-server"', + }, + telemeterServer+: { + whitelist+: [], + elideLabels+: [ + 'prometheus_replica', + ], + }, + }, +} diff --git a/jsonnet/telemeter/server/kubernetes.libsonnet b/jsonnet/telemeter/server/kubernetes.libsonnet new file mode 100644 index 00000000..e8ca754c --- /dev/null +++ b/jsonnet/telemeter/server/kubernetes.libsonnet @@ -0,0 +1,299 @@ +local k = import 'ksonnet/ksonnet.beta.4/k.libsonnet'; +local secretName = 'telemeter-server'; +local secretVolumeName = 'secret-telemeter-server'; +local tlsSecret = 'telemeter-server-shared'; +local tlsVolumeName = 'telemeter-server-tls'; +local tlsMountPath = '/etc/pki/service'; +local externalPort = 8443; +local internalPort = 8081; + +{ + _config+:: { + namespace: 'telemeter', + + telemeterServer+:: { + replicas: 10, + oidcIssuer: '', + clientSecret: '', + clientID: '', + whitelist: [], + elideLabels: [], + resourceLimits: {}, + resourceRequests: {}, + }, + + versions+:: { + telemeterServer: 'v4.0', + }, + + imageRepos+:: { + telemeterServer: 'quay.io/openshift/origin-telemeter', + }, + }, + + telemeterServer+:: { + statefulSet: + local statefulSet = k.apps.v1.statefulSet; + local container = k.apps.v1.statefulSet.mixin.spec.template.spec.containersType; + local volume = k.apps.v1.statefulSet.mixin.spec.template.spec.volumesType; + local containerPort = container.portsType; + local containerVolumeMount = container.volumeMountsType; + local containerEnv = container.envType; + + local podLabels = { 'k8s-app': 'telemeter-server' }; + local tlsMount = containerVolumeMount.new(tlsVolumeName, tlsMountPath); + local tlsVolume = volume.fromSecret(tlsVolumeName, tlsSecret); + local authorizeURL = containerEnv.fromSecretRef('AUTHORIZE_URL', secretName, 'authorize_url'); + local oidcIssuer = containerEnv.fromSecretRef('OIDC_ISSUER', secretName, 'oidc_issuer'); + local clientSecret = containerEnv.fromSecretRef('CLIENT_SECRET', secretName, 'client_secret'); + local clientID = containerEnv.fromSecretRef('CLIENT_ID', secretName, 'client_id'); + local secretVolume = volume.fromSecret(secretVolumeName, secretName); + + local whitelist = std.map( + function(rule) '--whitelist=%s' % std.strReplace(rule, 'ALERTS', 'alerts'), + $._config.telemeterServer.whitelist + ); + + local elide = std.map( + function(label) '--elide-label=%s' % label, + $._config.telemeterServer.elideLabels + ); + + local memcachedReplicas = std.range(0, $.memcached.replicas - 1); + local memcached = [ + '--memcached=%s-%d.%s.%s.svc.cluster.local:%d' % [ + $.memcached.statefulSet.metadata.name, + i, + $.memcached.service.metadata.name, + $.memcached.service.metadata.namespace, + $.memcached.service.spec.ports[0].port, + ] + for i in memcachedReplicas + ]; + + + local telemeterServer = + container.new('telemeter-server', $._config.imageRepos.telemeterServer + ':' + $._config.versions.telemeterServer) + + container.withCommand([ + '/usr/bin/telemeter-server', + '--listen=0.0.0.0:8443', + '--listen-internal=0.0.0.0:8081', + '--shared-key=%s/tls.key' % tlsMountPath, + '--tls-key=%s/tls.key' % tlsMountPath, + '--tls-crt=%s/tls.crt' % tlsMountPath, + '--internal-tls-key=%s/tls.key' % tlsMountPath, + '--internal-tls-crt=%s/tls.crt' % tlsMountPath, + '--authorize=$(AUTHORIZE_URL)', + '--oidc-issuer=$(OIDC_ISSUER)', + '--client-id=$(CLIENT_ID)', + '--client-secret=$(CLIENT_SECRET)', + ] + memcached + whitelist + elide) + + container.withPorts([ + containerPort.newNamed(externalPort, 'external'), + containerPort.newNamed(internalPort, 'internal'), + ]) + + container.mixin.resources.withLimitsMixin($._config.telemeterServer.resourceLimits) + + container.mixin.resources.withRequestsMixin($._config.telemeterServer.resourceRequests) + + container.withVolumeMounts([tlsMount]) + + container.withEnv([authorizeURL, oidcIssuer, clientSecret, clientID]) + { + livenessProbe: { + httpGet: { + path: '/healthz', + port: externalPort, + scheme: 'HTTPS', + }, + }, + readinessProbe: { + httpGet: { + path: '/healthz/ready', + port: externalPort, + scheme: 'HTTPS', + }, + }, + }; + + statefulSet.new('telemeter-server', $._config.telemeterServer.replicas, [telemeterServer], [], podLabels) + + statefulSet.mixin.metadata.withNamespace($._config.namespace) + + statefulSet.mixin.spec.selector.withMatchLabels(podLabels) + + statefulSet.mixin.spec.withPodManagementPolicy('Parallel') + + statefulSet.mixin.spec.withServiceName('telemeter-server') + + statefulSet.mixin.spec.template.spec.withServiceAccountName('telemeter-server') + + statefulSet.mixin.spec.template.spec.withVolumes([secretVolume, tlsVolume]) + + { + spec+: { + volumeClaimTemplates:: null, + }, + }, + + secret: + local secret = k.core.v1.secret; + + secret.new(secretName, { + authorize_url: '', + oidc_issuer: std.base64($._config.telemeterServer.oidcIssuer), + client_secret: std.base64($._config.telemeterServer.clientSecret), + client_id: std.base64($._config.telemeterServer.clientID), + }) + + secret.mixin.metadata.withNamespace($._config.namespace) + + secret.mixin.metadata.withLabels({ 'k8s-app': 'telemeter-server' }), + + + service: + local service = k.core.v1.service; + local servicePort = k.core.v1.service.mixin.spec.portsType; + + local servicePortExternal = servicePort.newNamed('external', externalPort, 'external'); + local servicePortInternal = servicePort.newNamed('internal', internalPort, 'internal'); + + service.new('telemeter-server', $.telemeterServer.statefulSet.spec.selector.matchLabels, [servicePortExternal, servicePortInternal]) + + service.mixin.metadata.withNamespace($._config.namespace) + + service.mixin.metadata.withLabels({ 'k8s-app': 'telemeter-server' }) + + service.mixin.spec.withClusterIp('None') + + service.mixin.metadata.withAnnotations({ + 'service.alpha.openshift.io/serving-cert-secret-name': tlsSecret, + }), + + serviceAccount: + local serviceAccount = k.core.v1.serviceAccount; + + serviceAccount.new('telemeter-server') + + serviceAccount.mixin.metadata.withNamespace($._config.namespace), + + serviceMonitor: + { + apiVersion: 'monitoring.coreos.com/v1', + kind: 'ServiceMonitor', + metadata: { + name: 'telemeter-server', + namespace: $._config.namespace, + labels: { + 'k8s-app': 'telemeter-server', + endpoint: 'metrics', + }, + }, + spec: { + jobLabel: 'k8s-app', + selector: { + matchLabels: { + 'k8s-app': 'telemeter-server', + }, + }, + endpoints: [ + { + bearerTokenFile: '/var/run/secrets/kubernetes.io/serviceaccount/token', + interval: '30s', + port: 'internal', + scheme: 'https', + tlsConfig: { + caFile: '/etc/prometheus/configmaps/serving-certs-ca-bundle/service-ca.crt', + serverName: 'telemeter-server.%s.svc' % $._config.namespace, + }, + }, + ], + }, + }, + }, + + memcached+:: { + images:: { + memcached: 'docker.io/memcached', + exporter: 'docker.io/prom/memcached-exporter', + }, + tags:: { + memcached: '1.5.20-alpine', + exporter: 'v0.6.0', + }, + replicas:: 3, + maxItemSize:: '1m', + memoryLimitMB:: 1024, + overprovisionFactor:: 1.2, + connectionLimit:: 1024, + resourceLimits:: { + cpu: '3', + memory: std.ceil($.memcached.memoryLimitMB * $.memcached.overprovisionFactor * 1.5) + 'Mi', + }, + resourceRequests:: { + cpu: '500m', + memory: std.ceil(($.memcached.memoryLimitMB * $.memcached.overprovisionFactor) + 100) + 'Mi', + }, + + service: + local service = k.core.v1.service; + local ports = service.mixin.spec.portsType; + + service.new( + 'memcached', + $.memcached.statefulSet.metadata.labels, + [ + ports.newNamed('client', 11211, 11211), + ports.newNamed('metrics', 9150, 9150), + ] + ) + + service.mixin.metadata.withNamespace($._config.namespace) + + service.mixin.metadata.withLabels({ 'app.kubernetes.io/name': $.memcached.service.metadata.name }) + + service.mixin.spec.withClusterIp('None'), + + statefulSet: + local sts = k.apps.v1.statefulSet; + local container = k.apps.v1.statefulSet.mixin.spec.template.spec.containersType; + local containerPort = container.portsType; + + local c = + container.new('memcached', $.memcached.images.memcached + ':' + $.memcached.tags.memcached) + + container.withPorts([containerPort.newNamed($.memcached.service.spec.ports[0].port, $.memcached.service.spec.ports[0].name)]) + + container.withArgs([ + '-m %(memoryLimitMB)s' % self, + '-I %(maxItemSize)s' % self, + '-c %(connectionLimit)s' % self, + '-v', + ]) + + container.mixin.resources.withLimitsMixin($.memcached.resourceLimits) + + container.mixin.resources.withRequestsMixin($.memcached.resourceRequests); + + local exporter = + container.new('exporter', $.memcached.images.exporter + ':' + $.memcached.tags.exporter) + + container.withPorts([containerPort.newNamed($.memcached.service.spec.ports[1].port, $.memcached.service.spec.ports[1].name)]) + + container.withArgs([ + '--memcached.address=localhost:%d' % $.memcached.service.spec.ports[0].port, + '--web.listen-address=0.0.0.0:%d' % $.memcached.service.spec.ports[1].port, + ]); + + sts.new('memcached', $.memcached.replicas, [c, exporter], [], $.memcached.statefulSet.metadata.labels) + + sts.mixin.metadata.withNamespace($._config.namespace) + + sts.mixin.metadata.withLabels({ 'app.kubernetes.io/name': $.memcached.statefulSet.metadata.name }) + + sts.mixin.spec.withServiceName($.memcached.service.metadata.name) + + sts.mixin.spec.selector.withMatchLabels($.memcached.statefulSet.metadata.labels) + + { + spec+: { + volumeClaimTemplates:: null, + }, + }, + + serviceMonitor: + { + apiVersion: 'monitoring.coreos.com/v1', + kind: 'ServiceMonitor', + metadata: { + name: 'memcached', + namespace: $._config.namespace, + labels: { + 'app.kubernetes.io/name': $.memcached.statefulSet.metadata.name, + }, + }, + spec: { + jobLabel: 'app.kubernetes.io/name', + selector: { + matchLabels: { + 'app.kubernetes.io/name': $.memcached.statefulSet.metadata.name, + }, + }, + endpoints: [ + { + interval: '30s', + port: $.memcached.service.spec.ports[1].name, + }, + ], + }, + }, + }, +} diff --git a/manifests/benchmark/configmapThanosReceiveController.yaml b/manifests/benchmark/configmapThanosReceiveController.yaml new file mode 100644 index 00000000..aca73004 --- /dev/null +++ b/manifests/benchmark/configmapThanosReceiveController.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +data: + hashrings.json: |- + [ + { + "hashring": "default", + "tenants": [ + + ] + } + ] +kind: ConfigMap +metadata: + labels: + app.kubernetes.io/component: kubernetes-controller + app.kubernetes.io/instance: benchmark-thanos-receive-controller + app.kubernetes.io/name: thanos-receive-controller + app.kubernetes.io/part-of: telemeter-benchmark + app.kubernetes.io/version: master-2020-02-06-b66e0c8 + name: benchmark-thanos-receive-controller-tenants + namespace: telemeter-benchmark diff --git a/manifests/benchmark/deploymentThanosQuery.yaml b/manifests/benchmark/deploymentThanosQuery.yaml new file mode 100644 index 00000000..76127d45 --- /dev/null +++ b/manifests/benchmark/deploymentThanosQuery.yaml @@ -0,0 +1,74 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/component: query-layer + app.kubernetes.io/instance: benchmark-thanos-query + app.kubernetes.io/name: thanos-query + app.kubernetes.io/part-of: telemeter-benchmark + app.kubernetes.io/version: master-2020-02-13-adfef4b5 + name: benchmark-thanos-query + namespace: telemeter-benchmark +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/component: query-layer + app.kubernetes.io/instance: benchmark-thanos-query + app.kubernetes.io/name: thanos-query + app.kubernetes.io/part-of: telemeter-benchmark + template: + metadata: + labels: + app.kubernetes.io/component: query-layer + app.kubernetes.io/instance: benchmark-thanos-query + app.kubernetes.io/name: thanos-query + app.kubernetes.io/part-of: telemeter-benchmark + app.kubernetes.io/version: master-2020-02-13-adfef4b5 + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/name + operator: In + values: + - thanos-query + namespaces: + - telemeter-benchmark + topologyKey: kubernetes.io/hostname + weight: 100 + containers: + - args: + - query + - --grpc-address=0.0.0.0:10901 + - --http-address=0.0.0.0:9090 + - --query.replica-label=prometheus_replica + - --query.replica-label=ruler_replica + - --query.replica-label=replica + - --store=dnssrv+_grpc._tcp.benchmark-thanos-receive-default.telemeter-benchmark.svc.cluster.local + image: quay.io/thanos/thanos:master-2020-02-13-adfef4b5 + livenessProbe: + failureThreshold: 4 + httpGet: + path: /-/healthy + port: 9090 + scheme: HTTP + periodSeconds: 30 + name: thanos-query + ports: + - containerPort: 10901 + name: grpc + - containerPort: 9090 + name: http + readinessProbe: + failureThreshold: 20 + httpGet: + path: /-/ready + port: 9090 + scheme: HTTP + periodSeconds: 5 + terminationMessagePolicy: FallbackToLogsOnError + terminationGracePeriodSeconds: 120 diff --git a/manifests/benchmark/deploymentThanosReceiveController.yaml b/manifests/benchmark/deploymentThanosReceiveController.yaml new file mode 100644 index 00000000..56c68122 --- /dev/null +++ b/manifests/benchmark/deploymentThanosReceiveController.yaml @@ -0,0 +1,45 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/component: kubernetes-controller + app.kubernetes.io/instance: benchmark-thanos-receive-controller + app.kubernetes.io/name: thanos-receive-controller + app.kubernetes.io/part-of: telemeter-benchmark + app.kubernetes.io/version: master-2020-02-06-b66e0c8 + name: benchmark-thanos-receive-controller + namespace: telemeter-benchmark +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/component: kubernetes-controller + app.kubernetes.io/instance: benchmark-thanos-receive-controller + app.kubernetes.io/name: thanos-receive-controller + app.kubernetes.io/part-of: telemeter-benchmark + template: + metadata: + labels: + app.kubernetes.io/component: kubernetes-controller + app.kubernetes.io/instance: benchmark-thanos-receive-controller + app.kubernetes.io/name: thanos-receive-controller + app.kubernetes.io/part-of: telemeter-benchmark + app.kubernetes.io/version: master-2020-02-06-b66e0c8 + spec: + containers: + - args: + - --configmap-name=benchmark-thanos-receive-controller-tenants + - --configmap-generated-name=benchmark-thanos-receive-controller-tenants-generated + - --file-name=hashrings.json + - --namespace=$(NAMESPACE) + env: + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + image: quay.io/observatorium/thanos-receive-controller:master-2020-02-06-b66e0c8 + name: thanos-receive-controller + ports: + - containerPort: 8080 + name: http + serviceAccount: benchmark-thanos-receive-controller diff --git a/manifests/benchmark/roleBindingThanosReceiveController.yaml b/manifests/benchmark/roleBindingThanosReceiveController.yaml new file mode 100644 index 00000000..70a41ca9 --- /dev/null +++ b/manifests/benchmark/roleBindingThanosReceiveController.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/component: kubernetes-controller + app.kubernetes.io/instance: benchmark-thanos-receive-controller + app.kubernetes.io/name: thanos-receive-controller + app.kubernetes.io/part-of: telemeter-benchmark + app.kubernetes.io/version: master-2020-02-06-b66e0c8 + name: benchmark-thanos-receive-controller + namespace: telemeter-benchmark +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: benchmark-thanos-receive-controller +subjects: +- kind: ServiceAccount + name: benchmark-thanos-receive-controller + namespace: telemeter-benchmark diff --git a/manifests/benchmark/roleThanosReceiveController.yaml b/manifests/benchmark/roleThanosReceiveController.yaml new file mode 100644 index 00000000..a45815f8 --- /dev/null +++ b/manifests/benchmark/roleThanosReceiveController.yaml @@ -0,0 +1,30 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/component: kubernetes-controller + app.kubernetes.io/instance: benchmark-thanos-receive-controller + app.kubernetes.io/name: thanos-receive-controller + app.kubernetes.io/part-of: telemeter-benchmark + app.kubernetes.io/version: master-2020-02-06-b66e0c8 + name: benchmark-thanos-receive-controller + namespace: telemeter-benchmark +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - list + - watch + - get + - create + - update +- apiGroups: + - apps + resources: + - statefulsets + verbs: + - list + - watch + - get diff --git a/manifests/benchmark/routeTelemeterServer.yaml b/manifests/benchmark/routeTelemeterServer.yaml new file mode 100644 index 00000000..de14b830 --- /dev/null +++ b/manifests/benchmark/routeTelemeterServer.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Route +metadata: + name: telemeter-server + namespace: telemeter-benchmark +spec: + port: + targetPort: external + tls: + insecureEdgeTerminationPolicy: Allow + termination: edge + to: + kind: Service + name: telemeter-server diff --git a/manifests/benchmark/routeThanosQuery.yaml b/manifests/benchmark/routeThanosQuery.yaml new file mode 100644 index 00000000..bf955c03 --- /dev/null +++ b/manifests/benchmark/routeThanosQuery.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Route +metadata: + labels: + app.kubernetes.io/component: query-layer + app.kubernetes.io/instance: benchmark-thanos-query + app.kubernetes.io/name: thanos-query + app.kubernetes.io/part-of: telemeter-benchmark + app.kubernetes.io/version: master-2020-02-13-adfef4b5 + name: benchmark-thanos-query + namespace: telemeter-benchmark +spec: + port: + targetPort: web + tls: + insecureEdgeTerminationPolicy: Allow + termination: edge + to: + kind: Service + name: benchmark-thanos-query diff --git a/manifests/benchmark/secretTelemeterServer.yaml b/manifests/benchmark/secretTelemeterServer.yaml new file mode 100644 index 00000000..70d8daf5 --- /dev/null +++ b/manifests/benchmark/secretTelemeterServer.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +data: + tokens.json: W3sidG9rZW4iOiAiYmVuY2htYXJrIn1d +kind: Secret +metadata: + labels: + k8s-app: telemeter-server + name: telemeter-server + namespace: telemeter-benchmark +type: Opaque diff --git a/manifests/benchmark/serviceAccountTelemeterServer.yaml b/manifests/benchmark/serviceAccountTelemeterServer.yaml new file mode 100644 index 00000000..b25e5710 --- /dev/null +++ b/manifests/benchmark/serviceAccountTelemeterServer.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: telemeter-server + namespace: telemeter-benchmark diff --git a/manifests/benchmark/serviceAccountThanosReceiveController.yaml b/manifests/benchmark/serviceAccountThanosReceiveController.yaml new file mode 100644 index 00000000..9097233c --- /dev/null +++ b/manifests/benchmark/serviceAccountThanosReceiveController.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/component: kubernetes-controller + app.kubernetes.io/instance: benchmark-thanos-receive-controller + app.kubernetes.io/name: thanos-receive-controller + app.kubernetes.io/part-of: telemeter-benchmark + app.kubernetes.io/version: master-2020-02-06-b66e0c8 + name: benchmark-thanos-receive-controller + namespace: telemeter-benchmark diff --git a/manifests/benchmark/serviceTelemeterServer.yaml b/manifests/benchmark/serviceTelemeterServer.yaml new file mode 100644 index 00000000..6c200d3e --- /dev/null +++ b/manifests/benchmark/serviceTelemeterServer.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Service +metadata: + annotations: + service.alpha.openshift.io/serving-cert-secret-name: telemeter-server-shared + labels: + k8s-app: telemeter-server + name: telemeter-server + namespace: telemeter-benchmark +spec: + clusterIP: None + ports: + - name: external + port: 8080 + targetPort: external + - name: internal + port: 8081 + targetPort: internal + selector: + k8s-app: telemeter-server diff --git a/manifests/benchmark/serviceThanosQuery.yaml b/manifests/benchmark/serviceThanosQuery.yaml new file mode 100644 index 00000000..0bde4072 --- /dev/null +++ b/manifests/benchmark/serviceThanosQuery.yaml @@ -0,0 +1,24 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/component: query-layer + app.kubernetes.io/instance: benchmark-thanos-query + app.kubernetes.io/name: thanos-query + app.kubernetes.io/part-of: telemeter-benchmark + app.kubernetes.io/version: master-2020-02-13-adfef4b5 + name: benchmark-thanos-query + namespace: telemeter-benchmark +spec: + ports: + - name: grpc + port: 10901 + targetPort: grpc + - name: http + port: 9090 + targetPort: http + selector: + app.kubernetes.io/component: query-layer + app.kubernetes.io/instance: benchmark-thanos-query + app.kubernetes.io/name: thanos-query + app.kubernetes.io/part-of: telemeter-benchmark diff --git a/manifests/benchmark/serviceThanosReceiveController.yaml b/manifests/benchmark/serviceThanosReceiveController.yaml new file mode 100644 index 00000000..3fd3d14c --- /dev/null +++ b/manifests/benchmark/serviceThanosReceiveController.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/component: kubernetes-controller + app.kubernetes.io/instance: benchmark-thanos-receive-controller + app.kubernetes.io/name: thanos-receive-controller + app.kubernetes.io/part-of: telemeter-benchmark + app.kubernetes.io/version: master-2020-02-06-b66e0c8 + name: benchmark-thanos-receive-controller + namespace: telemeter-benchmark +spec: + ports: + - name: http + port: 8080 + targetPort: 8080 + selector: + app.kubernetes.io/component: kubernetes-controller + app.kubernetes.io/instance: benchmark-thanos-receive-controller + app.kubernetes.io/name: thanos-receive-controller + app.kubernetes.io/part-of: telemeter-benchmark diff --git a/manifests/benchmark/serviceThanosReceivedefault.yaml b/manifests/benchmark/serviceThanosReceivedefault.yaml new file mode 100644 index 00000000..c4f7f6e7 --- /dev/null +++ b/manifests/benchmark/serviceThanosReceivedefault.yaml @@ -0,0 +1,30 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/component: database-write-hashring + app.kubernetes.io/instance: benchmark-thanos-receive-default + app.kubernetes.io/name: thanos-receive + app.kubernetes.io/part-of: telemeter-benchmark + app.kubernetes.io/version: master-2020-02-13-adfef4b5 + controller.receive.thanos.io/hashring: default + name: benchmark-thanos-receive-default + namespace: telemeter-benchmark +spec: + clusterIP: None + ports: + - name: grpc + port: 10901 + targetPort: 10901 + - name: http + port: 10902 + targetPort: 10902 + - name: remote-write + port: 19291 + targetPort: 19291 + selector: + app.kubernetes.io/component: database-write-hashring + app.kubernetes.io/instance: benchmark-thanos-receive-default + app.kubernetes.io/name: thanos-receive + app.kubernetes.io/part-of: telemeter-benchmark + controller.receive.thanos.io/hashring: default diff --git a/manifests/benchmark/statefulSetTelemeterServer.yaml b/manifests/benchmark/statefulSetTelemeterServer.yaml new file mode 100644 index 00000000..47fdab41 --- /dev/null +++ b/manifests/benchmark/statefulSetTelemeterServer.yaml @@ -0,0 +1,122 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: telemeter-server + namespace: telemeter-benchmark +spec: + podManagementPolicy: Parallel + replicas: 10 + selector: + matchLabels: + k8s-app: telemeter-server + serviceName: telemeter-server + template: + metadata: + labels: + k8s-app: telemeter-server + spec: + containers: + - command: + - /usr/bin/telemeter-server + - --listen=0.0.0.0:8080 + - --listen-internal=0.0.0.0:8081 + - --shared-key=/etc/pki/service/tls.key + - --authorize=http://localhost:8083 + - --forward-url=http://benchmark-thanos-receive-default.telemeter-benchmark.svc:19291/api/v1/receive + - --whitelist={__name__=~"cluster:usage:.*"} + - --whitelist={__name__="count:up0"} + - --whitelist={__name__="count:up1"} + - --whitelist={__name__="cluster_version"} + - --whitelist={__name__="cluster_version_available_updates"} + - --whitelist={__name__="cluster_operator_up"} + - --whitelist={__name__="cluster_operator_conditions"} + - --whitelist={__name__="cluster_version_payload"} + - --whitelist={__name__="cluster_installer"} + - --whitelist={__name__="cluster_infrastructure_provider"} + - --whitelist={__name__="cluster_feature_set"} + - --whitelist={__name__="instance:etcd_object_counts:sum"} + - --whitelist={__name__="alerts",alertstate="firing"} + - --whitelist={__name__="code:apiserver_request_count:rate:sum"} + - --whitelist={__name__="cluster:capacity_cpu_cores:sum"} + - --whitelist={__name__="cluster:capacity_memory_bytes:sum"} + - --whitelist={__name__="cluster:cpu_usage_cores:sum"} + - --whitelist={__name__="cluster:memory_usage_bytes:sum"} + - --whitelist={__name__="openshift:cpu_usage_cores:sum"} + - --whitelist={__name__="openshift:memory_usage_bytes:sum"} + - --whitelist={__name__="workload:cpu_usage_cores:sum"} + - --whitelist={__name__="workload:memory_usage_bytes:sum"} + - --whitelist={__name__="cluster:virt_platform_nodes:sum"} + - --whitelist={__name__="cluster:node_instance_type_count:sum"} + - --whitelist={__name__="cnv:vmi_status_running:count"} + - --whitelist={__name__="node_role_os_version_machine:cpu_capacity_cores:sum"} + - --whitelist={__name__="node_role_os_version_machine:cpu_capacity_sockets:sum"} + - --whitelist={__name__="subscription_sync_total"} + - --whitelist={__name__="csv_succeeded"} + - --whitelist={__name__="csv_abnormal"} + - --whitelist={__name__="ceph_cluster_total_bytes"} + - --whitelist={__name__="ceph_cluster_total_used_raw_bytes"} + - --whitelist={__name__="ceph_health_status"} + - --whitelist={__name__="job:ceph_osd_metadata:count"} + - --whitelist={__name__="job:kube_pv:count"} + - --whitelist={__name__="job:ceph_pools_iops:total"} + - --whitelist={__name__="job:ceph_pools_iops_bytes:total"} + - --whitelist={__name__="job:ceph_versions_running:count"} + - --whitelist={__name__="job:noobaa_total_unhealthy_buckets:sum"} + - --whitelist={__name__="job:noobaa_bucket_count:sum"} + - --whitelist={__name__="job:noobaa_total_object_count:sum"} + - --whitelist={__name__="noobaa_accounts_num"} + - --whitelist={__name__="noobaa_total_usage"} + - --whitelist={__name__="console_url"} + - --whitelist={__name__="cluster:network_attachment_definition_instances:max"} + - --whitelist={__name__="cluster:network_attachment_definition_enabled_instance_up:max"} + - --whitelist={__name__="insightsclient_request_send_total"} + - --whitelist={__name__="cam_app_workload_migrations"} + - --whitelist={__name__="cluster:apiserver_current_inflight_requests:sum:max_over_time:2m"} + - --whitelist={__name__="cluster:telemetry_selected_series:count"} + env: + - name: NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + image: quay.io/openshift/origin-telemeter:v4.0 + livenessProbe: + httpGet: + path: /healthz + port: 8080 + scheme: HTTP + name: telemeter-server + ports: + - containerPort: 8080 + name: external + - containerPort: 8081 + name: internal + readinessProbe: + httpGet: + path: /healthz/ready + port: 8080 + scheme: HTTP + volumeMounts: + - mountPath: /etc/telemeter + name: secret-telemeter-server + readOnly: false + - mountPath: /etc/pki/service + name: telemeter-server-tls + readOnly: false + - command: + - /usr/bin/authorization-server + - localhost:8083 + - /etc/telemeter/tokens.json + image: quay.io/openshift/origin-telemeter:v4.0 + name: authorization-server + volumeMounts: + - mountPath: /etc/telemeter + name: secret-telemeter-server + readOnly: false + serviceAccountName: telemeter-server + volumes: + - name: secret-telemeter-server + secret: + secretName: telemeter-server + - name: telemeter-server-tls + secret: + secretName: telemeter-server-shared diff --git a/manifests/benchmark/statefulSetThanosReceivedefault.yaml b/manifests/benchmark/statefulSetThanosReceivedefault.yaml new file mode 100644 index 00000000..8949c919 --- /dev/null +++ b/manifests/benchmark/statefulSetThanosReceivedefault.yaml @@ -0,0 +1,107 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + labels: + app.kubernetes.io/component: database-write-hashring + app.kubernetes.io/instance: benchmark-thanos-receive-default + app.kubernetes.io/name: thanos-receive + app.kubernetes.io/part-of: telemeter-benchmark + app.kubernetes.io/version: master-2020-02-13-adfef4b5 + controller.receive.thanos.io: thanos-receive-controller + controller.receive.thanos.io/hashring: default + name: benchmark-thanos-receive-default + namespace: telemeter-benchmark +spec: + replicas: 3 + selector: + matchLabels: + app.kubernetes.io/component: database-write-hashring + app.kubernetes.io/instance: benchmark-thanos-receive-default + app.kubernetes.io/name: thanos-receive + app.kubernetes.io/part-of: telemeter-benchmark + controller.receive.thanos.io/hashring: default + serviceName: benchmark-thanos-receive-default + template: + metadata: + labels: + app.kubernetes.io/component: database-write-hashring + app.kubernetes.io/instance: benchmark-thanos-receive-default + app.kubernetes.io/name: thanos-receive + app.kubernetes.io/part-of: telemeter-benchmark + app.kubernetes.io/version: master-2020-02-13-adfef4b5 + controller.receive.thanos.io/hashring: default + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/instance + operator: In + values: + - benchmark-thanos-receive-default + namespaces: + - telemeter-benchmark + topologyKey: kubernetes.io/hostname + weight: 100 + containers: + - args: + - receive + - --grpc-address=0.0.0.0:10901 + - --http-address=0.0.0.0:10902 + - --remote-write.address=0.0.0.0:19291 + - --receive.replication-factor=3 + - --tsdb.path=/var/thanos/receive + - --label=replica="$(NAME)" + - --label=receive="true" + - --receive.local-endpoint=$(NAME).benchmark-thanos-receive-default.$(NAMESPACE).svc.cluster.local:10901 + - --tsdb.retention=6h + - --receive.hashrings-file=/var/lib/thanos-receive/hashrings.json + env: + - name: NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + image: quay.io/thanos/thanos:master-2020-02-13-adfef4b5 + livenessProbe: + failureThreshold: 8 + httpGet: + path: /-/healthy + port: 10902 + scheme: HTTP + periodSeconds: 30 + name: thanos-receive + ports: + - containerPort: 10901 + name: grpc + - containerPort: 10902 + name: http + - containerPort: 19291 + name: remote-write + readinessProbe: + failureThreshold: 20 + httpGet: + path: /-/ready + port: 10902 + scheme: HTTP + periodSeconds: 5 + terminationMessagePolicy: FallbackToLogsOnError + volumeMounts: + - mountPath: /var/thanos/receive + name: data + readOnly: false + - mountPath: /var/lib/thanos-receive + name: hashring-config + terminationGracePeriodSeconds: 120 + volumes: + - emptyDir: {} + name: data + - configMap: + name: benchmark-thanos-receive-controller-tenants-generated + name: hashring-config + volumeClaimTemplates: null diff --git a/manifests/client/clusterRole.yaml b/manifests/client/clusterRole.yaml new file mode 100644 index 00000000..947191bc --- /dev/null +++ b/manifests/client/clusterRole.yaml @@ -0,0 +1,17 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: telemeter-client +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create diff --git a/manifests/client/clusterRoleBinding.yaml b/manifests/client/clusterRoleBinding.yaml new file mode 100644 index 00000000..6fc1a239 --- /dev/null +++ b/manifests/client/clusterRoleBinding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: telemeter-client +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: telemeter-client +subjects: +- kind: ServiceAccount + name: telemeter-client + namespace: openshift-monitoring diff --git a/manifests/client/clusterRoleBindingView.yaml b/manifests/client/clusterRoleBindingView.yaml new file mode 100644 index 00000000..74b1a2c2 --- /dev/null +++ b/manifests/client/clusterRoleBindingView.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: telemeter-client-view +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-monitoring-view +subjects: +- kind: ServiceAccount + name: telemeter-client + namespace: openshift-monitoring diff --git a/manifests/client/deployment.yaml b/manifests/client/deployment.yaml new file mode 100644 index 00000000..bddc9e4b --- /dev/null +++ b/manifests/client/deployment.yaml @@ -0,0 +1,103 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + k8s-app: telemeter-client + name: telemeter-client + namespace: openshift-monitoring +spec: + replicas: 1 + selector: + matchLabels: + k8s-app: telemeter-client + template: + metadata: + labels: + k8s-app: telemeter-client + spec: + containers: + - command: + - /usr/bin/telemeter-client + - --id=$(ID) + - --from=$(FROM) + - --from-ca-file=/etc/serving-certs-ca-bundle/service-ca.crt + - --from-token-file=/var/run/secrets/kubernetes.io/serviceaccount/token + - --to=$(TO) + - --to-token-file=/etc/telemeter/token + - --listen=localhost:8080 + - --anonymize-salt-file=/etc/telemeter/salt + - --anonymize-labels=$(ANONYMIZE_LABELS) + env: + - name: ANONYMIZE_LABELS + value: "" + - name: FROM + value: https://prometheus-k8s.openshift-monitoring.svc:9091 + - name: ID + value: "" + - name: TO + value: https://infogw.api.openshift.com + - name: HTTP_PROXY + value: "" + - name: HTTPS_PROXY + value: "" + - name: NO_PROXY + value: "" + image: quay.io/openshift/origin-telemeter:v4.0 + name: telemeter-client + ports: + - containerPort: 8080 + name: http + resources: + requests: + cpu: 1m + volumeMounts: + - mountPath: /etc/serving-certs-ca-bundle + name: serving-certs-ca-bundle + readOnly: false + - mountPath: /etc/telemeter + name: secret-telemeter-client + readOnly: false + - args: + - --webhook-url=http://localhost:8080/-/reload + - --volume-dir=/etc/serving-certs-ca-bundle + image: quay.io/openshift/origin-configmap-reload:v3.11 + name: reload + resources: + requests: + cpu: 1m + volumeMounts: + - mountPath: /etc/serving-certs-ca-bundle + name: serving-certs-ca-bundle + readOnly: false + - args: + - --secure-listen-address=:8443 + - --upstream=http://127.0.0.1:8080/ + - --tls-cert-file=/etc/tls/private/tls.crt + - --tls-private-key-file=/etc/tls/private/tls.key + image: quay.io/coreos/kube-rbac-proxy:v0.3.1 + name: kube-rbac-proxy + ports: + - containerPort: 8443 + name: https + resources: + requests: + cpu: 1m + memory: 20Mi + volumeMounts: + - mountPath: /etc/tls/private + name: telemeter-client-tls + readOnly: false + nodeSelector: + beta.kubernetes.io/os: linux + priorityClassName: system-cluster-critical + serviceAccountName: telemeter-client + volumes: + - configMap: + name: telemeter-client-serving-certs-ca-bundle + name: serving-certs-ca-bundle + - name: secret-telemeter-client + secret: + secretName: telemeter-client + - name: telemeter-client-tls + secret: + secretName: telemeter-client-tls diff --git a/manifests/client/secret.yaml b/manifests/client/secret.yaml new file mode 100644 index 00000000..c793f85a --- /dev/null +++ b/manifests/client/secret.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +data: + salt: "" + token: "" +kind: Secret +metadata: + labels: + k8s-app: telemeter-client + name: telemeter-client + namespace: openshift-monitoring +type: Opaque diff --git a/manifests/client/service.yaml b/manifests/client/service.yaml new file mode 100644 index 00000000..243250f5 --- /dev/null +++ b/manifests/client/service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + annotations: + service.alpha.openshift.io/serving-cert-secret-name: telemeter-client-tls + labels: + k8s-app: telemeter-client + name: telemeter-client + namespace: openshift-monitoring +spec: + clusterIP: None + ports: + - name: https + port: 8443 + targetPort: https + selector: + k8s-app: telemeter-client diff --git a/manifests/client/serviceAccount.yaml b/manifests/client/serviceAccount.yaml new file mode 100644 index 00000000..56ce95cd --- /dev/null +++ b/manifests/client/serviceAccount.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: telemeter-client + namespace: openshift-monitoring diff --git a/manifests/client/serviceMonitor.yaml b/manifests/client/serviceMonitor.yaml new file mode 100644 index 00000000..d082da07 --- /dev/null +++ b/manifests/client/serviceMonitor.yaml @@ -0,0 +1,20 @@ +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + k8s-app: telemeter-client + name: telemeter-client + namespace: openshift-monitoring +spec: + endpoints: + - bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + interval: 30s + port: https + scheme: https + tlsConfig: + caFile: /etc/prometheus/configmaps/serving-certs-ca-bundle/service-ca.crt + serverName: server-name-replaced-at-runtime + jobLabel: k8s-app + selector: + matchLabels: + k8s-app: telemeter-client diff --git a/manifests/client/servingCertsCABundle.yaml b/manifests/client/servingCertsCABundle.yaml new file mode 100644 index 00000000..fe77faab --- /dev/null +++ b/manifests/client/servingCertsCABundle.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +data: + service-ca.crt: "" +kind: ConfigMap +metadata: + annotations: + service.alpha.openshift.io/inject-cabundle: "true" + name: telemeter-client-serving-certs-ca-bundle + namespace: openshift-monitoring diff --git a/pkg/authorize/client.go b/pkg/authorize/client.go new file mode 100644 index 00000000..13d801f6 --- /dev/null +++ b/pkg/authorize/client.go @@ -0,0 +1,23 @@ +package authorize + +import ( + "context" +) + +type ClientAuthorizer interface { + AuthorizeClient(token string) (*Client, bool, error) +} + +type Client struct { + ID string + Labels map[string]string +} + +func WithClient(ctx context.Context, client *Client) context.Context { + return context.WithValue(ctx, clientKey, client) +} + +func FromContext(ctx context.Context) (*Client, bool) { + client, ok := ctx.Value(clientKey).(*Client) + return client, ok +} diff --git a/pkg/authorize/cluster.go b/pkg/authorize/cluster.go new file mode 100644 index 00000000..2c86aae7 --- /dev/null +++ b/pkg/authorize/cluster.go @@ -0,0 +1,11 @@ +package authorize + +type ClusterAuthorizerFunc func(token, cluster string) (subject string, err error) + +func (f ClusterAuthorizerFunc) AuthorizeCluster(token, cluster string) (subject string, err error) { + return f(token, cluster) +} + +type ClusterAuthorizer interface { + AuthorizeCluster(token, cluster string) (subject string, err error) +} diff --git a/pkg/authorize/context.go b/pkg/authorize/context.go new file mode 100644 index 00000000..f25a7489 --- /dev/null +++ b/pkg/authorize/context.go @@ -0,0 +1,8 @@ +package authorize + +type key int + +const ( + clientKey key = iota + TenantKey +) diff --git a/pkg/authorize/handler.go b/pkg/authorize/handler.go new file mode 100644 index 00000000..3d28ec46 --- /dev/null +++ b/pkg/authorize/handler.go @@ -0,0 +1,157 @@ +package authorize + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" +) + +func NewAuthorizeClientHandler(authorizer ClientAuthorizer, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + auth := strings.SplitN(req.Header.Get("Authorization"), " ", 2) + if strings.ToLower(auth[0]) != "bearer" { + http.Error(w, "Only bearer authorization allowed", http.StatusUnauthorized) + return + } + if len(auth) != 2 || len(strings.TrimSpace(auth[1])) == 0 { + http.Error(w, "Invalid Authorization header", http.StatusUnauthorized) + return + } + + client, ok, err := authorizer.AuthorizeClient(auth[1]) + if err != nil { + http.Error(w, fmt.Sprintf("Not authorized: %v", err), http.StatusUnauthorized) + return + } + if !ok { + http.Error(w, "Not authorized", http.StatusUnauthorized) + return + } + + next.ServeHTTP(w, req.WithContext(WithClient(req.Context(), client))) + }) +} + +type errorWithCode struct { + error + code int +} + +type ErrorWithCode interface { + error + HTTPStatusCode() int +} + +func NewErrorWithCode(err error, code int) ErrorWithCode { + return errorWithCode{error: err, code: code} +} + +func (e errorWithCode) HTTPStatusCode() int { + return e.code +} + +const requestBodyLimit = 32 * 1024 // 32MiB + +func AgainstEndpoint(logger log.Logger, client *http.Client, endpoint *url.URL, token []byte, cluster string, validate func(*http.Response) error) ([]byte, error) { + logger = log.With(logger, "component", "authorize") + req, err := http.NewRequest("POST", endpoint.String(), bytes.NewReader(token)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + + if client == nil { + client = http.DefaultClient + } + res, err := client.Do(req) + if err != nil { + return nil, err + } + defer func() { + // read the body to keep the upstream connection open + if res.Body != nil { + if _, err := io.Copy(ioutil.Discard, res.Body); err != nil { + level.Error(logger).Log("msg", "error copying body", "err", err) + } + res.Body.Close() + } + }() + + body, err := ioutil.ReadAll(io.LimitReader(res.Body, requestBodyLimit)) + if err != nil { + return nil, err + } + + if validate != nil { + if err := validate(res); err != nil { + return body, err + } + } + + switch res.StatusCode { + case http.StatusUnauthorized: + return body, NewErrorWithCode(fmt.Errorf("unauthorized"), http.StatusUnauthorized) + case http.StatusTooManyRequests: + return body, NewErrorWithCode(fmt.Errorf("rate limited, please try again later"), http.StatusTooManyRequests) + case http.StatusConflict: + return body, NewErrorWithCode(fmt.Errorf("the provided cluster identifier is already in use under a different account or is not sufficiently random"), http.StatusConflict) + case http.StatusNotFound: + return body, NewErrorWithCode(fmt.Errorf("not found"), http.StatusNotFound) + case http.StatusOK, http.StatusCreated: + return body, nil + default: + level.Warn(logger).Log("msg", "upstream server rejected request", "cluster", cluster, "body", string(body)) + return body, NewErrorWithCode(fmt.Errorf("upstream rejected request with code %d", res.StatusCode), http.StatusInternalServerError) + } +} + +// NewHandler returns an http.HandlerFunc that is able to authorize requests against Tollbooth. +// The handler function expects a bearer token in the Authorization header consisting of a +// base64-encoded JSON object containing "authorization_token" and "cluster_id" fields. +func NewHandler(logger log.Logger, client *http.Client, endpoint *url.URL, tenantKey string, next http.Handler) http.HandlerFunc { + logger = log.With(logger, "component", "authorize") + return func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Authorization") + authParts := strings.Split(string(authHeader), " ") + if len(authParts) != 2 || strings.ToLower(authParts[0]) != "bearer" { + level.Warn(logger).Log("msg", "bad authorization header") + w.WriteHeader(http.StatusBadRequest) + return + } + + token, err := base64.StdEncoding.DecodeString(authParts[1]) + if err != nil { + level.Warn(logger).Log("msg", "failed to extract token", "err", err) + w.WriteHeader(http.StatusBadRequest) + return + } + var tenant string + if tenantKey != "" { + fields := make(map[string]string) + if err := json.Unmarshal(token, &fields); err != nil { + level.Warn(logger).Log("msg", "failed to read token", "err", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + tenant = fields[tenantKey] + } + if _, err := AgainstEndpoint(logger, client, endpoint, token, tenant, nil); err != nil { + level.Warn(logger).Log("msg", "unauthorized request made:", "err", err) + w.WriteHeader(http.StatusUnauthorized) + return + } + + next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), TenantKey, tenant))) + } +} diff --git a/pkg/authorize/jwt/claims.go b/pkg/authorize/jwt/claims.go new file mode 100644 index 00000000..9b301212 --- /dev/null +++ b/pkg/authorize/jwt/claims.go @@ -0,0 +1,32 @@ +package jwt + +import ( + "time" + + "gopkg.in/square/go-jose.v2/jwt" +) + +type telemeter struct { + Labels map[string]string `json:"labels,omitempty"` +} + +type privateClaims struct { + Telemeter telemeter `json:"telemeter.openshift.io,omitempty"` +} + +func Claims(subject string, labels map[string]string, expirationSeconds int64, audience []string) (*jwt.Claims, interface{}) { + now := now() + sc := &jwt.Claims{ + Subject: subject, + Audience: jwt.Audience(audience), + IssuedAt: jwt.NewNumericDate(now), + NotBefore: jwt.NewNumericDate(now), + Expiry: jwt.NewNumericDate(now.Add(time.Duration(expirationSeconds) * time.Second)), + } + pc := &privateClaims{ + Telemeter: telemeter{ + Labels: labels, + }, + } + return sc, pc +} diff --git a/pkg/authorize/jwt/client_authorizer.go b/pkg/authorize/jwt/client_authorizer.go new file mode 100644 index 00000000..341eb210 --- /dev/null +++ b/pkg/authorize/jwt/client_authorizer.go @@ -0,0 +1,97 @@ +package jwt + +import ( + "crypto" + "encoding/base64" + "encoding/json" + "strings" + + "github.com/openshift/telemeter/pkg/authorize" + "gopkg.in/square/go-jose.v2/jwt" +) + +// NewClientAuthorizer authenticates tokens as JWT tokens produced by JWTTokenGenerator +// Token signatures are verified using each of the given public keys until one works (allowing key rotation) +// If lookup is true, the service account and secret referenced as claims inside the token are retrieved and verified with the provided ServiceAccountTokenGetter +func NewClientAuthorizer(issuer string, keys []crypto.PublicKey, v Validator) *clientAuthorizer { + return &clientAuthorizer{ + iss: issuer, + keys: keys, + validator: v, + } +} + +type clientAuthorizer struct { + iss string + keys []crypto.PublicKey + validator Validator +} + +func (j *clientAuthorizer) AuthorizeClient(tokenData string) (*authorize.Client, bool, error) { + if !j.hasCorrectIssuer(tokenData) { + return nil, false, nil + } + + tok, err := jwt.ParseSigned(tokenData) + if err != nil { + return nil, false, nil + } + + public := &jwt.Claims{} + private := j.validator.NewPrivateClaims() + + var ( + found bool + errs []error + ) + for _, key := range j.keys { + if err := tok.Claims(key, public, private); err != nil { + errs = append(errs, err) + continue + } + found = true + break + } + + if !found { + return nil, false, multipleErrors(errs) + } + + // If we get here, we have a token with a recognized signature and + // issuer string. + client, err := j.validator.Validate(tokenData, public, private) + if err != nil { + return nil, false, err + } + + return client, true, nil +} + +// hasCorrectIssuer returns true if tokenData is a valid JWT in compact +// serialization format and the "iss" claim matches the iss field of this token +// authenticator, and otherwise returns false. +// +// Note: go-jose currently does not allow access to unverified JWS payloads. +// See https://github.com/square/go-jose/issues/169 +func (j *clientAuthorizer) hasCorrectIssuer(tokenData string) bool { + parts := strings.SplitN(tokenData, ".", 4) + if len(parts) != 3 { + return false + } + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return false + } + claims := struct { + // WARNING: this JWT is not verified. Do not trust these claims. + Issuer string `json:"iss"` + }{} + if err := json.Unmarshal(payload, &claims); err != nil { + return false + } + if claims.Issuer != j.iss { + return false + } + return true + +} diff --git a/pkg/authorize/jwt/handler.go b/pkg/authorize/jwt/handler.go new file mode 100644 index 00000000..be63de32 --- /dev/null +++ b/pkg/authorize/jwt/handler.go @@ -0,0 +1,127 @@ +package jwt + +import ( + "encoding/json" + "fmt" + "math/rand" + "net/http" + "strings" + + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + + "github.com/openshift/telemeter/pkg/authorize" +) + +type authorizeClusterHandler struct { + clusterIDKey string + labels map[string]string + expireInSeconds int64 + signer *Signer + clusterAuth authorize.ClusterAuthorizer + logger log.Logger +} + +// NewAuthorizerHandler creates an authorizer HTTP endpoint that will authorize the cluster +// given by the "id" form request parameter using the given cluster authorizer. +// +// Upon success, the given cluster authorizer returns a subject which is used as the client identifier +// in a generated signed JWT which is returned to the client, along with any labels. +// +// A single cluster ID key parameter must be passed to uniquely identify the caller's data. +func NewAuthorizeClusterHandler(logger log.Logger, clusterIDKey string, expireInSeconds int64, signer *Signer, labels map[string]string, ca authorize.ClusterAuthorizer) *authorizeClusterHandler { + return &authorizeClusterHandler{ + clusterIDKey: clusterIDKey, + expireInSeconds: expireInSeconds, + signer: signer, + labels: labels, + clusterAuth: ca, + logger: log.With(logger, "component", "authorize/jwt"), + } +} + +func (a *authorizeClusterHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + if req.Method != "POST" { + http.Error(w, "Only POST is allowed to this endpoint", http.StatusMethodNotAllowed) + return + } + + req.Body = http.MaxBytesReader(w, req.Body, 4*1024) + defer req.Body.Close() + + if err := req.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + uniqueIDKey := "id" + clusterID := req.Form.Get(uniqueIDKey) + if len(clusterID) == 0 { + http.Error(w, fmt.Sprintf("The '%s' parameter must be specified via URL or url-encoded form body", uniqueIDKey), http.StatusBadRequest) + return + } + + auth := strings.SplitN(req.Header.Get("Authorization"), " ", 2) + if strings.ToLower(auth[0]) != "bearer" { + http.Error(w, "Only bearer authorization allowed", http.StatusUnauthorized) + return + } + if len(auth) != 2 || len(strings.TrimSpace(auth[1])) == 0 { + http.Error(w, "Invalid Authorization header", http.StatusUnauthorized) + return + } + clientToken := auth[1] + + subject, err := a.clusterAuth.AuthorizeCluster(clientToken, clusterID) + if err != nil { + if scerr, ok := err.(authorize.ErrorWithCode); ok { + if scerr.HTTPStatusCode() >= http.StatusInternalServerError { + level.Error(a.logger).Log("msg", "unable to authorize request", "err", scerr) + } + if scerr.HTTPStatusCode() == http.StatusTooManyRequests { + w.Header().Set("Retry-After", "300") + } + http.Error(w, scerr.Error(), scerr.HTTPStatusCode()) + return + } + + // always hide errors from the upstream service from the client + uid := rand.Int63() + level.Error(a.logger).Log("msg", "unable to authorize request", "uid", uid, "err", err) + http.Error(w, fmt.Sprintf("Internal server error, requestid=%d", uid), http.StatusInternalServerError) + return + } + + labels := map[string]string{ + a.clusterIDKey: clusterID, + } + for k, v := range a.labels { + labels[k] = v + } + + // create a token that asserts the client and the labels + authToken, err := a.signer.GenerateToken(Claims(subject, labels, a.expireInSeconds, []string{"telemeter-client"})) + if err != nil { + level.Error(a.logger).Log("msg", "unable to generate token", "err", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // write the data back to the client + data, err := json.Marshal(authorize.TokenResponse{ + Version: 1, + Token: authToken, + ExpiresInSeconds: a.expireInSeconds, + Labels: labels, + }) + + if err != nil { + level.Error(a.logger).Log("msg", "unable to marshal token", "err", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if _, err := w.Write(data); err != nil { + level.Error(a.logger).Log("msg", "writing auth token failed", "err", err) + } +} diff --git a/pkg/authorize/jwt/handler_test.go b/pkg/authorize/jwt/handler_test.go new file mode 100644 index 00000000..7916851f --- /dev/null +++ b/pkg/authorize/jwt/handler_test.go @@ -0,0 +1,204 @@ +package jwt + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "encoding/json" + "errors" + "fmt" + "testing" + + "net/http" + "net/http/httptest" + "net/url" + + "github.com/go-kit/kit/log" + + "github.com/openshift/telemeter/pkg/authorize" +) + +type statusCodeErr struct { + code int + err string +} + +func newStatusCodeErr(code int, err string) statusCodeErr { + return statusCodeErr{ + code: code, + err: err, + } +} + +func (e statusCodeErr) Error() string { + return e.err +} + +func (e statusCodeErr) HTTPStatusCode() int { + return e.code +} + +func newTestClusterAuthorizer(subject string, err error) authorize.ClusterAuthorizer { + return authorize.ClusterAuthorizerFunc(func(token, cluster string) (string, error) { + return subject, err + }) +} + +type requestBuilder struct{ *http.Request } + +func (r requestBuilder) WithHeaders(kvs ...string) requestBuilder { + r.Header = make(http.Header) + for i := 0; i < len(kvs)/2; i++ { + k := kvs[i*2] + v := kvs[i*2+1] + r.Header.Set(k, v) + } + return r +} + +func (r requestBuilder) WithForm(kvs ...string) requestBuilder { + r.Form = make(url.Values) + for i := 0; i < len(kvs)/2; i++ { + k := kvs[i*2] + v := kvs[i*2+1] + r.Form.Set(k, v) + } + return r +} + +func TestAuthorizeClusterHandler(t *testing.T) { + clusterIDKey := "_id" + labels := map[string]string{ + "foo": "bar", + "baz": "qux", + } + pk, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + t.Fatal(err) + } + + type checkFunc func(*httptest.ResponseRecorder) error + + labelsEqual := func(labels map[string]string, id string) checkFunc { + return func(rec *httptest.ResponseRecorder) error { + var tr authorize.TokenResponse + if err := json.Unmarshal(rec.Body.Bytes(), &tr); err != nil { + return fmt.Errorf("failed to unmarshal TokenResponse: %v", err) + } + if tr.Labels[clusterIDKey] != id { + return fmt.Errorf("expected response to have '%s=%s', got '%s=%s'", clusterIDKey, id, clusterIDKey, tr.Labels[clusterIDKey]) + } + delete(tr.Labels, clusterIDKey) + if len(labels) > len(tr.Labels) { + for k, v := range labels { + if v != tr.Labels[k] { + return fmt.Errorf("expected response to have '%s=%s', got '%s=%s'", k, v, k, tr.Labels[k]) + } + } + } + for k, v := range tr.Labels { + if v != labels[k] { + return fmt.Errorf("unexpected label in response: got '%s=%s', expected '%s=%s'", k, v, k, labels[k]) + } + } + + return nil + } + } + + responseCodeIs := func(code int) checkFunc { + return func(rec *httptest.ResponseRecorder) error { + if got := rec.Code; got != code { + return fmt.Errorf("want HTTP response code %d, got %d", code, got) + } + return nil + } + } + + for _, tc := range []struct { + name string + signer *Signer + clusterAuth authorize.ClusterAuthorizer + req *http.Request + check checkFunc + }{ + { + name: "invalid method", + req: httptest.NewRequest("GET", "https://telemeter", nil), + check: responseCodeIs(405), + }, + + { + name: "no auth header", + req: httptest.NewRequest("POST", "https://telemeter", nil), + check: responseCodeIs(400), + }, + + { + name: "invalid auth header", + req: requestBuilder{httptest.NewRequest("POST", "https://telemeter", nil)}. + WithForm("id", "test"). + WithHeaders("Authorization", "invalid"). + Request, + check: responseCodeIs(401), + }, + + { + name: "cluster auth failed", + req: requestBuilder{httptest.NewRequest("POST", "https://telemeter", nil)}. + WithForm("id", "test"). + WithHeaders("Authorization", "bearer invalid"). + Request, + clusterAuth: newTestClusterAuthorizer("", errors.New("invalid")), + check: responseCodeIs(500), + }, + { + name: "cluster auth returned error", + req: requestBuilder{httptest.NewRequest("POST", "https://telemeter", nil)}. + WithForm("id", "test"). + WithHeaders("Authorization", "bearer invalid"). + Request, + clusterAuth: newTestClusterAuthorizer("", newStatusCodeErr(666, "some error")), + check: responseCodeIs(666), + }, + { + name: "signing failed", + req: requestBuilder{httptest.NewRequest("POST", "https://telemeter", nil)}. + WithForm("id", "test"). + WithHeaders("Authorization", "bearer valid"). + Request, + clusterAuth: newTestClusterAuthorizer("sub123", nil), + signer: NewSigner("iss456", crypto.PrivateKey(nil)), + check: responseCodeIs(500), + }, + { + name: "cluster auth success", + req: requestBuilder{httptest.NewRequest("POST", "https://telemeter", nil)}. + WithForm("id", "test"). + WithHeaders("Authorization", "bearer valid"). + Request, + clusterAuth: newTestClusterAuthorizer("sub123", nil), + signer: NewSigner("iss456", pk), + check: responseCodeIs(200), + }, + { + name: "labels equal success", + req: requestBuilder{httptest.NewRequest("POST", "https://telemeter", nil)}. + WithForm("id", "test"). + WithHeaders("Authorization", "bearer valid"). + Request, + clusterAuth: newTestClusterAuthorizer("sub123", nil), + signer: NewSigner("iss456", pk), + check: labelsEqual(labels, "test"), + }, + } { + t.Run(tc.name, func(t *testing.T) { + h := NewAuthorizeClusterHandler(log.NewNopLogger(), clusterIDKey, 2, tc.signer, labels, tc.clusterAuth) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, tc.req) + if err := tc.check(rec); err != nil { + t.Error(err) + } + }) + } +} diff --git a/pkg/authorize/jwt/signer.go b/pkg/authorize/jwt/signer.go new file mode 100644 index 00000000..f337ebc2 --- /dev/null +++ b/pkg/authorize/jwt/signer.go @@ -0,0 +1,91 @@ +package jwt + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rsa" + "fmt" + "strings" + "time" + + jose "gopkg.in/square/go-jose.v2" + "gopkg.in/square/go-jose.v2/jwt" +) + +func NewSigner(issuer string, private crypto.PrivateKey) *Signer { + return &Signer{ + iss: issuer, + privateKey: private, + } +} + +type Signer struct { + iss string + privateKey crypto.PrivateKey +} + +func (j *Signer) GenerateToken(claims *jwt.Claims, privateClaims interface{}) (string, error) { + var alg jose.SignatureAlgorithm + switch privateKey := j.privateKey.(type) { + case *rsa.PrivateKey: + alg = jose.RS256 + case *ecdsa.PrivateKey: + switch privateKey.Curve { + case elliptic.P256(): + alg = jose.ES256 + case elliptic.P384(): + alg = jose.ES384 + case elliptic.P521(): + alg = jose.ES512 + default: + return "", fmt.Errorf("unknown private key curve, must be 256, 384, or 521") + } + default: + return "", fmt.Errorf("unknown private key type %T, must be *rsa.PrivateKey or *ecdsa.PrivateKey", j.privateKey) + } + + signer, err := jose.NewSigner( + jose.SigningKey{ + Algorithm: alg, + Key: j.privateKey, + }, + nil, + ) + if err != nil { + return "", err + } + + // claims are applied in reverse precedence + return jwt.Signed(signer). + Claims(privateClaims). + Claims(claims). + Claims(&jwt.Claims{ + Issuer: j.iss, + }). + CompactSerialize() +} + +func multipleErrors(errs []error) error { + if len(errs) > 1 { + return listErr(errs) + } + if len(errs) == 0 { + return nil + } + return errs[0] +} + +type listErr []error + +func (errs listErr) Error() string { + var messages []string + for _, err := range errs { + messages = append(messages, err.Error()) + } + return "multiple errors: " + strings.Join(messages, ", ") +} + +func now() time.Time { + return time.Now() +} diff --git a/pkg/authorize/jwt/validator.go b/pkg/authorize/jwt/validator.go new file mode 100644 index 00000000..d1c3a9ce --- /dev/null +++ b/pkg/authorize/jwt/validator.go @@ -0,0 +1,82 @@ +package jwt + +import ( + "errors" + + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + + "github.com/openshift/telemeter/pkg/authorize" + + "gopkg.in/square/go-jose.v2/jwt" +) + +// Validator is called by the JWT token authentictaor to apply domain specific +// validation to a token and extract user information. +type Validator interface { + // Validate validates a token and returns user information or an error. + // Validator can assume that the issuer and signature of a token are already + // verified when this function is called. + Validate(tokenData string, public *jwt.Claims, private interface{}) (*authorize.Client, error) + // NewPrivateClaims returns a struct that the authenticator should + // deserialize the JWT payload into. The authenticator may then pass this + // struct back to the Validator as the 'private' argument to a Validate() + // call. This struct should contain fields for any private claims that the + // Validator requires to validate the JWT. + NewPrivateClaims() interface{} +} + +func NewValidator(logger log.Logger, audiences []string) Validator { + return &validator{ + auds: audiences, + logger: log.With(logger, "component", "authorize/jwt"), + } +} + +type validator struct { + auds []string + logger log.Logger +} + +var _ = Validator(&validator{}) + +func (v *validator) Validate(_ string, public *jwt.Claims, privateObj interface{}) (*authorize.Client, error) { + private, ok := privateObj.(*privateClaims) + if !ok { + level.Info(v.logger).Log("msg", "jwt validator expected private claim of type *privateClaims", "got", privateObj) + return nil, errors.New("token could not be validated") + } + err := public.Validate(jwt.Expected{ + Time: now(), + }) + switch { + case err == nil: + case err == jwt.ErrExpired: + return nil, errors.New("token has expired") + default: + level.Info(v.logger).Log("msg", "unexpected validation", "err", err) + return nil, errors.New("token could not be validated") + } + + var audValid bool + + for _, aud := range v.auds { + audValid = public.Audience.Contains(aud) + if audValid { + break + } + } + + if !audValid { + return nil, errors.New("token is invalid for this audience") + } + + return &authorize.Client{ + ID: public.Subject, + Labels: private.Telemeter.Labels, + }, nil +} + +func (v *validator) NewPrivateClaims() interface{} { + return &privateClaims{} +} diff --git a/pkg/authorize/roundtripper.go b/pkg/authorize/roundtripper.go new file mode 100644 index 00000000..29dbc54c --- /dev/null +++ b/pkg/authorize/roundtripper.go @@ -0,0 +1,49 @@ +package authorize + +import ( + "fmt" + "net/http" + "net/url" +) + +type ServerRotatingRoundTripper struct { + endpoint *url.URL + initialToken string + tokenStore tokenStore + + wrapper http.RoundTripper +} + +func NewServerRotatingRoundTripper(initialToken string, endpoint *url.URL, rt http.RoundTripper) *ServerRotatingRoundTripper { + return &ServerRotatingRoundTripper{ + initialToken: initialToken, + endpoint: endpoint, + wrapper: rt, + } +} + +func (rt *ServerRotatingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + token, err := rt.tokenStore.Load(rt.endpoint, rt.initialToken, rt.wrapper) + if err != nil { + return nil, err + } + + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + resp, err := rt.wrapper.RoundTrip(req) + if resp != nil && resp.StatusCode == http.StatusUnauthorized { + rt.tokenStore.Invalidate(token) + } + return resp, err +} + +func (rt *ServerRotatingRoundTripper) Labels() (map[string]string, error) { + _, err := rt.tokenStore.Load(rt.endpoint, rt.initialToken, rt.wrapper) + if err != nil { + return nil, fmt.Errorf("unable to authorize to server: %v", err) + } + labels, ok := rt.tokenStore.Labels() + if !ok { + return nil, fmt.Errorf("labels from server have expired") + } + return labels, nil +} diff --git a/pkg/authorize/stub/stub.go b/pkg/authorize/stub/stub.go new file mode 100644 index 00000000..329b9b16 --- /dev/null +++ b/pkg/authorize/stub/stub.go @@ -0,0 +1,17 @@ +package stub + +import ( + "fmt" + "log" + + "github.com/openshift/telemeter/pkg/fnv" +) + +func Authorize(token, cluster string) (string, error) { + subject, err := fnv.Hash(token) + if err != nil { + return "", fmt.Errorf("hashing token failed: %v", err) + } + log.Printf("warning: Performing no-op authentication, subject will be %s with cluster %s", subject, cluster) + return subject, nil +} diff --git a/pkg/authorize/token_store.go b/pkg/authorize/token_store.go new file mode 100644 index 00000000..f5127638 --- /dev/null +++ b/pkg/authorize/token_store.go @@ -0,0 +1,107 @@ +package authorize + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "sync" + "time" +) + +type TokenResponse struct { + Version int `json:"version"` + + Token string `json:"token"` + ExpiresInSeconds int64 `json:"expiresInSeconds"` + + Labels map[string]string `json:"labels"` +} + +type tokenStore struct { + lock sync.Mutex + value string + expires time.Time + labels map[string]string +} + +func (t *tokenStore) Load(endpoint *url.URL, initialToken string, rt http.RoundTripper) (string, error) { + t.lock.Lock() + defer t.lock.Unlock() + if len(t.value) > 0 && (t.expires.IsZero() || t.expires.After(time.Now())) { + return t.value, nil + } + + c := http.Client{Transport: rt, Timeout: 30 * time.Second} + req, err := http.NewRequest("POST", endpoint.String(), nil) + if err != nil { + return "", fmt.Errorf("unable to create authentication request: %v", err) + } + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", initialToken)) + resp, err := c.Do(req) + if err != nil { + return "", fmt.Errorf("unable to perform authentication request: %v", err) + } + defer resp.Body.Close() + + switch resp.StatusCode { + case http.StatusOK, http.StatusCreated: + case http.StatusUnauthorized: + return "", fmt.Errorf("initial authentication token is expired or invalid") + default: + body, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 4*1024)) + return "", fmt.Errorf("unable to exchange initial token for a long lived token: %d:\n%s", resp.StatusCode, string(body)) + } + + response, parseErr := parseTokenFromBody(resp.Body, 16*1024) + if parseErr != nil { + return "", parseErr + } + + t.value = response.Token + t.labels = response.Labels + if response.ExpiresInSeconds >= 60 { + t.expires = time.Now().Add(time.Duration(response.ExpiresInSeconds-15) * time.Second) + } else { + t.expires = time.Time{} + } + + return t.value, nil +} + +func (t *tokenStore) Invalidate(token string) { + t.lock.Lock() + defer t.lock.Unlock() + if token == t.value { + t.value = "" + t.labels = nil + t.expires = time.Time{} + } +} + +func (t *tokenStore) Labels() (map[string]string, bool) { + t.lock.Lock() + defer t.lock.Unlock() + if len(t.value) == 0 { + return nil, false + } + labels := make(map[string]string) + for k, v := range t.labels { + labels[k] = v + } + return labels, true +} + +func parseTokenFromBody(r io.Reader, limitBytes int64) (*TokenResponse, error) { + data, err := ioutil.ReadAll(io.LimitReader(r, limitBytes)) + if err != nil { + return nil, fmt.Errorf("unable to read the authentication response: %v", err) + } + response := &TokenResponse{} + if err := json.Unmarshal(data, response); err != nil { + return nil, fmt.Errorf("unable to parse the authentication response: %v", err) + } + return response, nil +} diff --git a/pkg/authorize/tollbooth/mock.go b/pkg/authorize/tollbooth/mock.go new file mode 100644 index 00000000..ac1d6875 --- /dev/null +++ b/pkg/authorize/tollbooth/mock.go @@ -0,0 +1,100 @@ +package tollbooth + +import ( + "encoding/json" + "fmt" + "net/http" + "sync" + + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + + "github.com/openshift/telemeter/pkg/fnv" +) + +type Key struct { + Token string + Cluster string +} + +type mock struct { + mu sync.Mutex + Tokens map[string]struct{} + Responses map[Key]clusterRegistration + logger log.Logger +} + +func NewMock(logger log.Logger, tokenSet map[string]struct{}) *mock { + return &mock{ + Tokens: tokenSet, + Responses: make(map[Key]clusterRegistration), + logger: log.With(logger, "component", "authorize/toolbooth"), + } +} + +func (s *mock) ServeHTTP(w http.ResponseWriter, req *http.Request) { + defer req.Body.Close() + if req.Method != "POST" { + write(w, http.StatusMethodNotAllowed, ®istrationError{Name: "MethodNotAllowed", Reason: "Only requests of type 'POST' are accepted."}, s.logger) + return + } + if req.Header.Get("Content-Type") != "application/json" { + write(w, http.StatusBadRequest, ®istrationError{Name: "InvalidContentType", Reason: "Only requests with Content-Type application/json are accepted."}, s.logger) + return + } + regRequest := &clusterRegistration{} + if err := json.NewDecoder(req.Body).Decode(regRequest); err != nil { + write(w, http.StatusBadRequest, ®istrationError{Name: "InvalidBody", Reason: fmt.Sprintf("Unable to parse body as JSON: %v", err)}, s.logger) + return + } + + s.mu.Lock() + defer s.mu.Unlock() + + if regRequest.ClusterID == "" { + write(w, http.StatusBadRequest, ®istrationError{Name: "BadRequest", Reason: "No cluster ID provided."}, s.logger) + return + } + + if _, tokenFound := s.Tokens[regRequest.AuthorizationToken]; !tokenFound { + write(w, http.StatusUnauthorized, ®istrationError{Name: "NotAuthorized", Reason: "The provided token is not recognized."}, s.logger) + return + } + + key := Key{Token: regRequest.AuthorizationToken, Cluster: regRequest.ClusterID} + resp, clusterFound := s.Responses[key] + code := http.StatusOK + + accountID, err := fnv.Hash(regRequest.ClusterID) + if err != nil { + level.Warn(s.logger).Log("msg", "hashing cluster ID failed", "err", err) + write(w, http.StatusInternalServerError, ®istrationError{Name: "", Reason: "hashing cluster ID failed"}, s.logger) + return + } + + if !clusterFound { + resp = clusterRegistration{ + AccountID: accountID, + AuthorizationToken: regRequest.AuthorizationToken, + ClusterID: regRequest.ClusterID, + } + s.Responses[key] = resp + code = http.StatusCreated + } + + write(w, code, resp, s.logger) +} + +func write(w http.ResponseWriter, statusCode int, resp interface{}, logger log.Logger) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + data, err := json.MarshalIndent(resp, "", " ") + if err != nil { + level.Error(logger).Log("err", "marshaling response failed", "err", err) + return + } + if _, err := w.Write(data); err != nil { + level.Error(logger).Log("err", "writing response failed", "err", err) + return + } +} diff --git a/pkg/authorize/tollbooth/tollbooth.go b/pkg/authorize/tollbooth/tollbooth.go new file mode 100644 index 00000000..3cebb579 --- /dev/null +++ b/pkg/authorize/tollbooth/tollbooth.go @@ -0,0 +1,92 @@ +package tollbooth + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "mime" + "net/http" + "net/url" + + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/pkg/errors" + + "github.com/openshift/telemeter/pkg/authorize" +) + +type clusterRegistration struct { + ClusterID string `json:"cluster_id"` + AuthorizationToken string `json:"authorization_token"` + AccountID string `json:"account_id"` +} + +type registrationError struct { + Name string `json:"name"` + Reason string `json:"reason"` +} + +type authorizer struct { + to *url.URL + client *http.Client + logger log.Logger +} + +func NewAuthorizer(logger log.Logger, c *http.Client, to *url.URL) *authorizer { + return &authorizer{ + to: to, + client: c, + logger: log.With(logger, "component", "authorize/toolbooth"), + } +} + +func (a *authorizer) AuthorizeCluster(token, cluster string) (string, error) { + regReq := &clusterRegistration{ + ClusterID: cluster, + AuthorizationToken: token, + } + + data, err := json.Marshal(regReq) + if err != nil { + return "", err + } + + body, err := authorize.AgainstEndpoint(a.logger, a.client, a.to, data, cluster, func(res *http.Response) error { + contentType := res.Header.Get("Content-Type") + mediaType, _, err := mime.ParseMediaType(contentType) + if err != nil || mediaType != "application/json" { + level.Warn(a.logger).Log("msg", "upstream server responded with an unknown content type", "to", a.to, "contenttype", contentType) + return fmt.Errorf("unrecognized token response content-type %q", contentType) + } + return nil + }) + if err != nil { + return "", err + } + + response := &clusterRegistration{} + if err := json.Unmarshal(body, response); err != nil { + level.Warn(a.logger).Log("msg", "upstream server response could not be parsed", "to", a.to) + return "", fmt.Errorf("unable to parse response body: %v", err) + } + + if len(response.AccountID) == 0 { + level.Warn(a.logger).Log("msg", "upstream server responded with an empty user string", "to", a.to) + return "", fmt.Errorf("server responded with an empty user string") + } + + return response.AccountID, nil +} + +// ExtractToken extracts the token from an auth request. +// In the case of a request to Tollbooth, the token +// is the entire contents of the request body. +func ExtractToken(r *http.Request) (string, error) { + body, err := ioutil.ReadAll(r.Body) + if err := r.Body.Close(); err != nil { + return "", errors.Wrap(err, "failed to close body") + } + r.Body = ioutil.NopCloser(bytes.NewBuffer(body)) + return string(body), err +} diff --git a/pkg/benchmark/benchmark.go b/pkg/benchmark/benchmark.go new file mode 100644 index 00000000..dfd2724c --- /dev/null +++ b/pkg/benchmark/benchmark.go @@ -0,0 +1,356 @@ +package benchmark + +import ( + "context" + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "io" + "io/ioutil" + "math" + "math/rand" + "net/http" + "net/url" + "os" + "sort" + "strings" + "sync" + "time" + + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/prometheus/client_golang/prometheus" + clientmodel "github.com/prometheus/client_model/go" + "github.com/prometheus/common/expfmt" + uuid "github.com/satori/go.uuid" + + "github.com/openshift/telemeter/pkg/authorize" + "github.com/openshift/telemeter/pkg/metricfamily" + "github.com/openshift/telemeter/pkg/metricsclient" +) + +const ( + DefaultSyncPeriod = 4*time.Minute + 30*time.Second + LimitBytes = 200 * 1024 +) + +var ( + forwardErrors = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "forward_errors", + Help: "The number of times forwarding federated metrics has failed", + }) + forwardedSamples = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "forwarded_samples", + Help: "The total number of forwarded samples for all time series", + }) +) + +func init() { + prometheus.MustRegister( + forwardErrors, + forwardedSamples, + ) +} + +type Benchmark struct { + cancel context.CancelFunc + lock sync.Mutex + reconfigure chan struct{} + running bool + workers []*worker + logger log.Logger +} + +// Config defines the parameters that can be used to configure a worker. +// The only required field is `From`. +type Config struct { + ToAuthorize *url.URL + ToUpload *url.URL + ToCAFile string + ToToken string + ToTokenFile string + Interval time.Duration + MetricsFile string + Workers int + Logger log.Logger +} + +// worker represents a metrics forwarding agent. It collects metrics from a source URL and forwards them to a sink. +// A worker should be configured with a `Config` and instantiated with the `New` func. +// workers are thread safe; all access to shared fields is synchronized. +type worker struct { + client *metricsclient.Client + id string + interval time.Duration + metrics []*clientmodel.MetricFamily + to *url.URL + transformer metricfamily.Transformer + logger log.Logger +} + +// New creates a new Benchmark based on the provided Config. If the Config contains invalid +// values, then an error is returned. +func New(cfg *Config) (*Benchmark, error) { + logger := log.With(cfg.Logger, "component", "benchmark") + b := Benchmark{ + reconfigure: make(chan struct{}), + workers: make([]*worker, cfg.Workers), + logger: logger, + } + + interval := cfg.Interval + if interval == 0 { + interval = DefaultSyncPeriod + } + + if len(cfg.ToToken) == 0 && len(cfg.ToTokenFile) > 0 { + data, err := ioutil.ReadFile(cfg.ToTokenFile) + if err != nil { + return nil, fmt.Errorf("unable to read to-token-file: %v", err) + } + cfg.ToToken = strings.TrimSpace(string(data)) + } + if (len(cfg.ToToken) > 0) != (cfg.ToAuthorize != nil) { + return nil, errors.New("an authorization URL and authorization token must both specified or empty") + } + + f, err := os.Open(cfg.MetricsFile) + if err != nil { + return nil, fmt.Errorf("unable to read metrics-file: %v", err) + } + + var pool *x509.CertPool + if len(cfg.ToCAFile) > 0 { + pool, err = x509.SystemCertPool() + if err != nil { + return nil, fmt.Errorf("failed to read system certificates: %v", err) + } + data, err := ioutil.ReadFile(cfg.ToCAFile) + if err != nil { + return nil, fmt.Errorf("failed to read to-ca-file: %v", err) + } + if !pool.AppendCertsFromPEM(data) { + level.Warn(logger).Log("msg", "no certs found in to-ca-file") + } + } + + for i := range b.workers { + w := &worker{ + id: uuid.Must(uuid.NewV4()).String(), + interval: interval, + to: cfg.ToUpload, + logger: logger, + } + + if _, err := f.Seek(0, 0); err != nil { + return nil, fmt.Errorf("failed to rewind file: %v", err) + } + dec := expfmt.NewDecoder(f, expfmt.FmtText) + for { + var m clientmodel.MetricFamily + err := dec.Decode(&m) + if err == io.EOF { + break + } + if err != nil { + return nil, fmt.Errorf("unable to parse metrics: %v", err) + } + w.metrics = append(w.metrics, &m) + } + + transport := metricsclient.DefaultTransport(logger, false) + transport.Proxy = http.ProxyFromEnvironment + if pool != nil { + if transport.TLSClientConfig == nil { + transport.TLSClientConfig = &tls.Config{} + } + transport.TLSClientConfig.RootCAs = pool + } + client := &http.Client{Transport: transport} + transformer := metricfamily.MultiTransformer{} + if len(cfg.ToToken) > 0 { + u, err := url.Parse(cfg.ToAuthorize.String()) + if err != nil { + panic(err) + } + q := u.Query() + q.Add("id", w.id) + u.RawQuery = q.Encode() + + // Exchange our token for a token from the authorize endpoint, which also gives us a + // set of expected labels we must include. + rt := authorize.NewServerRotatingRoundTripper(cfg.ToToken, u, client.Transport) + client.Transport = rt + transformer.With(metricfamily.NewLabel(nil, rt)) + } + w.client = metricsclient.New(logger, client, LimitBytes, w.interval, "federate_to") + w.transformer = transformer + b.workers[i] = w + } + + if err := f.Close(); err != nil { + return nil, fmt.Errorf("failed to close file: %v", err) + } + + return &b, nil +} + +// Run starts a Benchmark instance. +func (b *Benchmark) Run() { + b.lock.Lock() + r := b.running + b.lock.Unlock() + if r { + return + } + + for { + var wg sync.WaitGroup + done := make(chan struct{}) + b.lock.Lock() + b.running = true + ctx, cancel := context.WithCancel(context.Background()) + b.cancel = cancel + for i, w := range b.workers { + wg.Add(1) + go func(i int, w *worker) { + level.Info(b.logger).Log("msg", "started worker", "index", i+1, "total", len(b.workers), "worker", w.id) + select { + case <-time.After(time.Duration(rand.Int63n(int64(w.interval)))): + w.run(ctx) + case <-ctx.Done(): + } + wg.Done() + }(i, w) + } + b.lock.Unlock() + go func() { + wg.Wait() + close(done) + }() + select { + case <-done: + return + case <-b.reconfigure: + level.Info(b.logger).Log("msg", "restarting workers...") + continue + } + } +} + +// Stop will pause a Benchmark instance. +func (b *Benchmark) Stop() { + b.lock.Lock() + defer b.lock.Unlock() + if b.running { + b.cancel() + b.running = false + } +} + +// Reconfigure reconfigures an existing Benchmark instnace. +func (b *Benchmark) Reconfigure(cfg *Config) error { + benchmark, err := New(cfg) + if err != nil { + return fmt.Errorf("failed to reconfigure: %v", err) + } + + b.lock.Lock() + defer b.lock.Unlock() + + if b.running { + b.reconfigure <- struct{}{} + b.cancel() + } + b.workers = benchmark.workers + return nil +} + +func (w *worker) run(ctx context.Context) { + for { + m := w.generate() + wait := w.interval + if err := w.forward(ctx, m); err != nil { + forwardErrors.Inc() + level.Error(w.logger).Log("msg", "unable to forward results", "worker", w.id, "err", err) + wait = time.Minute + } + var n int + for i := range m { + n += len(m[i].Metric) + } + forwardedSamples.Add(float64(n)) + select { + // If the context is cancelled, then we're done. + case <-ctx.Done(): + return + case <-time.After(wait): + } + } +} + +func (w *worker) generate() []*clientmodel.MetricFamily { + rand.Seed(time.Now().UnixNano()) + mfs := make([]*clientmodel.MetricFamily, len(w.metrics)) + now := time.Now().UnixNano() / int64(time.Millisecond) + for i := range w.metrics { + mf := *w.metrics[i] + mf.Metric = make([]*clientmodel.Metric, len(w.metrics[i].Metric)) + for j := range w.metrics[i].Metric { + m := randomize(w.metrics[i].Metric[j]) + ts := now - rand.Int63n(int64(w.interval/time.Millisecond)) + m.TimestampMs = &ts + mf.Metric[j] = m + } + // Sort the time series within the metric family by timestamp so Prometheus will accept them. + sort.Slice(mf.Metric, func(i, j int) bool { + return mf.Metric[i].GetTimestampMs() < mf.Metric[j].GetTimestampMs() + }) + mfs[i] = &mf + } + return mfs +} + +// randomize copies and randomizes the values of a metric. +func randomize(metric *clientmodel.Metric) *clientmodel.Metric { + m := *metric + if m.GetUntyped() != nil { + v := *m.GetUntyped() + f := math.Round(rand.Float64() * v.GetValue()) + v.Value = &f + m.Untyped = &v + } + if m.GetGauge() != nil { + v := *m.GetGauge() + f := math.Round(rand.Float64() * v.GetValue()) + v.Value = &f + m.Gauge = &v + } + if m.GetCounter() != nil { + if rand.Intn(2) == 1 { + v := *m.GetCounter() + f := v.GetValue() + 1 + v.Value = &f + m.Counter = &v + } + } + return &m +} + +func (w *worker) forward(ctx context.Context, metrics []*clientmodel.MetricFamily) error { + if w.to == nil { + level.Warn(w.logger).Log("msg", "no destination configured; doing nothing", "worker", w.id) + return nil + } + if err := metricfamily.Filter(metrics, w.transformer); err != nil { + return err + } + if len(metrics) == 0 { + level.Warn(w.logger).Log("msg", "no metrics to send; doing nothing", "worker", w.id) + return nil + } + + req := &http.Request{Method: "POST", URL: w.to} + return w.client.Send(ctx, req, metrics) +} diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go new file mode 100644 index 00000000..2195903f --- /dev/null +++ b/pkg/cache/cache.go @@ -0,0 +1,104 @@ +package cache + +import ( + "bufio" + "bytes" + "net/http" + "net/http/httputil" + + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" +) + +// Cacher is able to get and set key value pairs. +type Cacher interface { + Get(string) ([]byte, bool, error) + Set(string, []byte) error +} + +// KeyFunc generates a cache key from a http.Request. +type KeyFunc func(*http.Request) (string, error) + +// RoundTripper is a http.RoundTripper than can get and set responses from a cache. +type RoundTripper struct { + c Cacher + key KeyFunc + next http.RoundTripper + + l log.Logger + + // Metrics. + cacheReadsTotal *prometheus.CounterVec + cacheWritesTotal *prometheus.CounterVec +} + +// RoundTrip implements the RoundTripper interface. +func (r *RoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + key, err := r.key(req) + if err != nil { + return nil, errors.Wrap(err, "failed to generate key from request") + } + + raw, ok, err := r.c.Get(key) + if err != nil { + r.cacheReadsTotal.WithLabelValues("error").Inc() + return nil, errors.Wrap(err, "failed to retrieve value from cache") + } + + if ok { + r.cacheReadsTotal.WithLabelValues("hit").Inc() + resp, err := http.ReadResponse(bufio.NewReader(bytes.NewBuffer(raw)), req) + return resp, errors.Wrap(err, "failed to read response") + } + + r.cacheReadsTotal.WithLabelValues("miss").Inc() + resp, err := r.next.RoundTrip(req) + if err == nil && resp.StatusCode/200 == 1 { + // Try to cache the response but don't block. + defer func() { + raw, err := httputil.DumpResponse(resp, true) + if err != nil { + level.Error(r.l).Log("msg", "failed to dump response", "err", err) + return + } + if err := r.c.Set(key, raw); err != nil { + r.cacheWritesTotal.WithLabelValues("error").Inc() + level.Error(r.l).Log("msg", "failed to set value in cache", "err", err) + return + } + r.cacheWritesTotal.WithLabelValues("success").Inc() + }() + } + return resp, err +} + +// NewRoundTripper creates a new http.RoundTripper that returns http.Responses +// from a cache. +func NewRoundTripper(c Cacher, key KeyFunc, next http.RoundTripper, l log.Logger, reg prometheus.Registerer) http.RoundTripper { + rt := &RoundTripper{ + c: c, + key: key, + next: next, + l: l, + cacheReadsTotal: prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "cache_reads_total", + Help: "The number of read requests made to the cache.", + }, []string{"result"}, + ), + cacheWritesTotal: prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "cache_writes_total", + Help: "The number of write requests made to the cache.", + }, []string{"result"}, + ), + } + + if reg != nil { + reg.MustRegister(rt.cacheReadsTotal, rt.cacheWritesTotal) + } + + return rt +} diff --git a/pkg/cache/memcached/memcached.go b/pkg/cache/memcached/memcached.go new file mode 100644 index 00000000..2a3f6d2b --- /dev/null +++ b/pkg/cache/memcached/memcached.go @@ -0,0 +1,95 @@ +package memcached + +import ( + "context" + "crypto/sha256" + "fmt" + "sync" + "time" + + "github.com/bradfitz/gomemcache/memcache" + "github.com/pkg/errors" + + tcache "github.com/openshift/telemeter/pkg/cache" +) + +// cache is a Cacher implemented on top of Memcached. +type cache struct { + client *memcache.Client + expiration int32 + mu sync.RWMutex +} + +// New creates a new Cacher from a list of Memcached servers, +// a key expiration time given in seconds, a DNS refresh interval, +// and a context. The Cacher will continue to update the DNS entries +// for the Memcached servers every interval as long as the context is valid. +func New(ctx context.Context, interval, expiration int32, servers ...string) tcache.Cacher { + c := &cache{ + client: memcache.New(servers...), + expiration: expiration, + } + + if interval > 0 { + go func() { + t := time.NewTicker(time.Duration(interval) * time.Second) + for { + select { + case <-t.C: + c.mu.Lock() + c.client = memcache.New(servers...) + c.mu.Unlock() + case <-ctx.Done(): + t.Stop() + return + } + } + }() + } + return c +} + +// Get returns a value from Memcached. +func (c *cache) Get(key string) ([]byte, bool, error) { + key, err := hash(key) + if err != nil { + return nil, false, err + } + c.mu.RLock() + defer c.mu.RUnlock() + i, err := c.client.Get(key) + if err != nil { + if err == memcache.ErrCacheMiss { + return nil, false, nil + } + return nil, false, err + } + + return i.Value, true, nil +} + +// Set sets a value in Memcached. +func (c *cache) Set(key string, value []byte) error { + key, err := hash(key) + if err != nil { + return err + } + i := memcache.Item{ + Key: key, + Value: value, + Expiration: c.expiration, + } + c.mu.RLock() + defer c.mu.RUnlock() + return c.client.Set(&i) +} + +// hashKey hashes the given key to ensure that it is less than 250 bytes, +// as Memcached cannot handle longer keys. +func hash(key string) (string, error) { + h := sha256.New() + if _, err := h.Write([]byte(key)); err != nil { + return "", errors.Wrap(err, "failed to hash key") + } + return fmt.Sprintf("%x", (h.Sum(nil))), nil +} diff --git a/pkg/fnv/hash.go b/pkg/fnv/hash.go new file mode 100644 index 00000000..5a11ac50 --- /dev/null +++ b/pkg/fnv/hash.go @@ -0,0 +1,20 @@ +package fnv + +import ( + "fmt" + "hash" + "hash/fnv" + "strconv" +) + +// Hash hashes the given text using a 64-bit FNV-1a hash.Hash. +func Hash(text string) (string, error) { + return hashText(fnv.New64a(), text) +} + +func hashText(h hash.Hash64, text string) (string, error) { + if _, err := h.Write([]byte(text)); err != nil { + return "", fmt.Errorf("hashing failed: %v", err) + } + return strconv.FormatUint(h.Sum64(), 32), nil +} diff --git a/pkg/fnv/hash_test.go b/pkg/fnv/hash_test.go new file mode 100644 index 00000000..ff4ade4e --- /dev/null +++ b/pkg/fnv/hash_test.go @@ -0,0 +1,63 @@ +package fnv + +import ( + "errors" + "hash" + "testing" +) + +type testHasher struct { + n int + writeErr error + sum64 uint64 +} + +func (h *testHasher) Write(p []byte) (n int, err error) { return h.n, h.writeErr } +func (h *testHasher) Sum64() uint64 { return h.sum64 } +func (h *testHasher) Sum(b []byte) []byte { return nil } +func (h *testHasher) Reset() {} +func (h *testHasher) Size() int { return 0 } +func (h *testHasher) BlockSize() int { return 0 } + +func TestHashText(t *testing.T) { + for _, tc := range []struct { + name string + h hash.Hash64 + text string + want, wantErr string + }{ + { + name: "write success", + h: &testHasher{ + writeErr: nil, + sum64: 123, + }, + text: "foo", + want: "3r", + }, + { + name: "write err", + h: &testHasher{ + writeErr: errors.New("write error"), + }, + text: "foo", + wantErr: "hashing failed: write error", + }, + } { + t.Run(tc.name, func(t *testing.T) { + got, err := hashText(tc.h, tc.text) + if got != tc.want { + t.Errorf("want hashed text %q, got %q", tc.want, got) + } + + gotErr := "" + if err != nil { + gotErr = err.Error() + } + + if gotErr != tc.wantErr { + t.Errorf("want err %q, got %q", tc.wantErr, gotErr) + } + }) + } +} diff --git a/pkg/forwarder/forwarder.go b/pkg/forwarder/forwarder.go new file mode 100644 index 00000000..3c36f336 --- /dev/null +++ b/pkg/forwarder/forwarder.go @@ -0,0 +1,330 @@ +package forwarder + +import ( + "context" + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" + "sync" + "time" + + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + + "github.com/prometheus/client_golang/prometheus" + clientmodel "github.com/prometheus/client_model/go" + + "github.com/openshift/telemeter/pkg/authorize" + telemeterhttp "github.com/openshift/telemeter/pkg/http" + "github.com/openshift/telemeter/pkg/metricfamily" + "github.com/openshift/telemeter/pkg/metricsclient" +) + +type RuleMatcher interface { + MatchRules() []string +} + +var ( + gaugeFederateSamples = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "federate_samples", + Help: "Tracks the number of samples per federation", + }) + gaugeFederateFilteredSamples = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "federate_filtered_samples", + Help: "Tracks the number of samples filtered per federation", + }) + gaugeFederateErrors = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "federate_errors", + Help: "The number of times forwarding federated metrics has failed", + }) +) + +func init() { + prometheus.MustRegister( + gaugeFederateErrors, gaugeFederateSamples, gaugeFederateFilteredSamples, + ) +} + +// Config defines the parameters that can be used to configure a worker. +// The only required field is `From`. +type Config struct { + From *url.URL + ToAuthorize *url.URL + ToUpload *url.URL + FromToken string + ToToken string + FromTokenFile string + ToTokenFile string + FromCAFile string + + AnonymizeLabels []string + AnonymizeSalt string + AnonymizeSaltFile string + Debug bool + Interval time.Duration + LimitBytes int64 + Rules []string + RulesFile string + Transformer metricfamily.Transformer + + Logger log.Logger +} + +// Worker represents a metrics forwarding agent. It collects metrics from a source URL and forwards them to a sink. +// A Worker should be configured with a `Config` and instantiated with the `New` func. +// Workers are thread safe; all access to shared fields are synchronized. +type Worker struct { + fromClient *metricsclient.Client + toClient *metricsclient.Client + from *url.URL + to *url.URL + + interval time.Duration + transformer metricfamily.Transformer + rules []string + + lastMetrics []*clientmodel.MetricFamily + lock sync.Mutex + reconfigure chan struct{} + + logger log.Logger +} + +// New creates a new Worker based on the provided Config. If the Config contains invalid +// values, then an error is returned. +func New(cfg Config) (*Worker, error) { + if cfg.From == nil { + return nil, errors.New("a URL from which to scrape is required") + } + logger := log.With(cfg.Logger, "component", "forwarder") + level.Warn(logger).Log("msg", cfg.ToUpload) + w := Worker{ + from: cfg.From, + interval: cfg.Interval, + reconfigure: make(chan struct{}), + to: cfg.ToUpload, + logger: log.With(cfg.Logger, "component", "forwarder/worker"), + } + + if w.interval == 0 { + w.interval = 4*time.Minute + 30*time.Second + } + + // Configure the anonymization. + anonymizeSalt := cfg.AnonymizeSalt + if len(cfg.AnonymizeSalt) == 0 && len(cfg.AnonymizeSaltFile) > 0 { + data, err := ioutil.ReadFile(cfg.AnonymizeSaltFile) + if err != nil { + return nil, fmt.Errorf("failed to read anonymize-salt-file: %v", err) + } + anonymizeSalt = strings.TrimSpace(string(data)) + } + if len(cfg.AnonymizeLabels) != 0 && len(anonymizeSalt) == 0 { + return nil, fmt.Errorf("anonymize-salt must be specified if anonymize-labels is set") + } + if len(cfg.AnonymizeLabels) == 0 { + level.Warn(logger).Log("msg", "not anonymizing any labels") + } + + // Configure a transformer. + var transformer metricfamily.MultiTransformer + if cfg.Transformer != nil { + transformer.With(cfg.Transformer) + } + if len(cfg.AnonymizeLabels) > 0 { + transformer.With(metricfamily.NewMetricsAnonymizer(anonymizeSalt, cfg.AnonymizeLabels, nil)) + } + + // Create the `fromClient`. + fromTransport := metricsclient.DefaultTransport(logger, false) + if len(cfg.FromCAFile) > 0 { + if fromTransport.TLSClientConfig == nil { + fromTransport.TLSClientConfig = &tls.Config{} + } + pool, err := x509.SystemCertPool() + if err != nil { + return nil, fmt.Errorf("failed to read system certificates: %v", err) + } + data, err := ioutil.ReadFile(cfg.FromCAFile) + if err != nil { + return nil, fmt.Errorf("failed to read from-ca-file: %v", err) + } + if !pool.AppendCertsFromPEM(data) { + level.Warn(logger).Log("msg", "no certs found in from-ca-file") + } + fromTransport.TLSClientConfig.RootCAs = pool + } + fromClient := &http.Client{Transport: fromTransport} + if cfg.Debug { + fromClient.Transport = telemeterhttp.NewDebugRoundTripper(logger, fromClient.Transport) + } + if len(cfg.FromToken) == 0 && len(cfg.FromTokenFile) > 0 { + data, err := ioutil.ReadFile(cfg.FromTokenFile) + if err != nil { + return nil, fmt.Errorf("unable to read from-token-file: %v", err) + } + cfg.FromToken = strings.TrimSpace(string(data)) + } + if len(cfg.FromToken) > 0 { + fromClient.Transport = telemeterhttp.NewBearerRoundTripper(cfg.FromToken, fromClient.Transport) + } + w.fromClient = metricsclient.New(logger, fromClient, cfg.LimitBytes, w.interval, "federate_from") + + // Create the `toClient`. + toTransport := metricsclient.DefaultTransport(logger, true) + toTransport.Proxy = http.ProxyFromEnvironment + toClient := &http.Client{Transport: toTransport} + if cfg.Debug { + toClient.Transport = telemeterhttp.NewDebugRoundTripper(logger, toClient.Transport) + } + if len(cfg.ToToken) == 0 && len(cfg.ToTokenFile) > 0 { + data, err := ioutil.ReadFile(cfg.ToTokenFile) + if err != nil { + return nil, fmt.Errorf("unable to read to-token-file: %v", err) + } + cfg.ToToken = strings.TrimSpace(string(data)) + } + if (len(cfg.ToToken) > 0) != (cfg.ToAuthorize != nil) { + return nil, errors.New("an authorization URL and authorization token must both specified or empty") + } + if len(cfg.ToToken) > 0 { + // Exchange our token for a token from the authorize endpoint, which also gives us a + // set of expected labels we must include. + rt := authorize.NewServerRotatingRoundTripper(cfg.ToToken, cfg.ToAuthorize, toClient.Transport) + toClient.Transport = rt + transformer.With(metricfamily.NewLabel(nil, rt)) + } + w.toClient = metricsclient.New(logger, toClient, cfg.LimitBytes, w.interval, "federate_to") + w.transformer = transformer + + // Configure the matching rules. + rules := cfg.Rules + if len(cfg.RulesFile) > 0 { + data, err := ioutil.ReadFile(cfg.RulesFile) + if err != nil { + return nil, fmt.Errorf("unable to read match-file: %v", err) + } + rules = append(rules, strings.Split(string(data), "\n")...) + } + for i := 0; i < len(rules); { + s := strings.TrimSpace(rules[i]) + if len(s) == 0 { + rules = append(rules[:i], rules[i+1:]...) + continue + } + rules[i] = s + i++ + } + w.rules = rules + + return &w, nil +} + +// Reconfigure temporarily stops a worker and reconfigures is with the provided Config. +// Is thread safe and can run concurrently with `LastMetrics` and `Run`. +func (w *Worker) Reconfigure(cfg Config) error { + worker, err := New(cfg) + if err != nil { + return fmt.Errorf("failed to reconfigure: %v", err) + } + + w.lock.Lock() + defer w.lock.Unlock() + + w.fromClient = worker.fromClient + w.toClient = worker.toClient + w.interval = worker.interval + w.from = worker.from + w.to = worker.to + w.transformer = worker.transformer + w.rules = worker.rules + + // Signal a restart to Run func. + // Do this in a goroutine since we do not care if restarting the Run loop is asynchronous. + go func() { w.reconfigure <- struct{}{} }() + return nil +} + +func (w *Worker) LastMetrics() []*clientmodel.MetricFamily { + w.lock.Lock() + defer w.lock.Unlock() + return w.lastMetrics +} + +func (w *Worker) Run(ctx context.Context) { + for { + // Ensure that the Worker does not access critical configuration during a reconfiguration. + w.lock.Lock() + wait := w.interval + // The critical section ends here. + w.lock.Unlock() + + if err := w.forward(ctx); err != nil { + gaugeFederateErrors.Inc() + level.Error(w.logger).Log("msg", "unable to forward results", "err", err) + wait = time.Minute + } + + select { + // If the context is cancelled, then we're done. + case <-ctx.Done(): + return + case <-time.After(wait): + // We want to be able to interrupt a sleep to immediately apply a new configuration. + case <-w.reconfigure: + } + } +} + +func (w *Worker) forward(ctx context.Context) error { + w.lock.Lock() + defer w.lock.Unlock() + + // Load the match rules each time. + from := w.from + + // reset query from last invocation, otherwise match rules will be appended + w.from.RawQuery = "" + v := from.Query() + for _, rule := range w.rules { + v.Add("match[]", rule) + } + from.RawQuery = v.Encode() + + req := &http.Request{Method: "GET", URL: from} + families, err := w.fromClient.Retrieve(ctx, req) + if err != nil { + return err + } + + before := metricfamily.MetricsCount(families) + if err := metricfamily.Filter(families, w.transformer); err != nil { + return err + } + + families = metricfamily.Pack(families) + after := metricfamily.MetricsCount(families) + + gaugeFederateSamples.Set(float64(before)) + gaugeFederateFilteredSamples.Set(float64(before - after)) + + w.lastMetrics = families + + if len(families) == 0 { + level.Warn(w.logger).Log("msg", "no metrics to send, doing nothing") + return nil + } + + if w.to == nil { + level.Warn(w.logger).Log("msg", "to is nil, doing nothing") + return nil + } + + req = &http.Request{Method: "POST", URL: w.to} + return w.toClient.RemoteWrite(ctx, req, families) +} diff --git a/pkg/forwarder/forwarder_test.go b/pkg/forwarder/forwarder_test.go new file mode 100644 index 00000000..8385fc16 --- /dev/null +++ b/pkg/forwarder/forwarder_test.go @@ -0,0 +1,295 @@ +package forwarder + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "sync" + "testing" + + "github.com/go-kit/kit/log" +) + +func TestNew(t *testing.T) { + from, err := url.Parse("https://redhat.com") + if err != nil { + t.Fatalf("failed to parse `from` URL: %v", err) + } + toAuthorize, err := url.Parse("https://openshift.com") + if err != nil { + t.Fatalf("failed to parse `toAuthorize` URL: %v", err) + } + toUpload, err := url.Parse("https://k8s.io") + if err != nil { + t.Fatalf("failed to parse `toUpload` URL: %v", err) + } + + tc := []struct { + c Config + err bool + }{ + { + // Empty configuration should error. + c: Config{Logger: log.NewNopLogger()}, + err: true, + }, + { + // Only providing a `From` should not error. + c: Config{ + From: from, + Logger: log.NewNopLogger(), + }, + err: false, + }, + { + // Providing `From` and `ToUpload` should not error. + c: Config{ + From: from, + ToUpload: toUpload, + Logger: log.NewNopLogger(), + }, + err: false, + }, + { + // Providing `ToAuthorize` without `ToToken` should error. + c: Config{ + From: from, + ToAuthorize: toAuthorize, + Logger: log.NewNopLogger(), + }, + err: true, + }, + { + // Providing `ToToken` without `ToAuthorize` should error. + c: Config{ + From: from, + ToToken: "foo", + Logger: log.NewNopLogger(), + }, + err: true, + }, + { + // Providing `ToAuthorize` and `ToToken` should not error. + c: Config{ + From: from, + ToAuthorize: toAuthorize, + ToToken: "foo", + Logger: log.NewNopLogger(), + }, + err: false, + }, + { + // Providing an invalid `FromTokenFile` file should error. + c: Config{ + From: from, + FromTokenFile: "/this/path/does/not/exist", + Logger: log.NewNopLogger(), + }, + err: true, + }, + { + // Providing an invalid `ToTokenFile` file should error. + c: Config{ + From: from, + ToTokenFile: "/this/path/does/not/exist", + Logger: log.NewNopLogger(), + }, + err: true, + }, + { + // Providing only `AnonymizeSalt` should not error. + c: Config{ + From: from, + AnonymizeSalt: "1", + Logger: log.NewNopLogger(), + }, + err: false, + }, + { + // Providing only `AnonymizeLabels` should error. + c: Config{ + From: from, + AnonymizeLabels: []string{"foo"}, + Logger: log.NewNopLogger(), + }, + err: true, + }, + { + // Providing only `AnonymizeSalt` and `AnonymizeLabels should not error. + c: Config{ + From: from, + AnonymizeLabels: []string{"foo"}, + AnonymizeSalt: "1", + Logger: log.NewNopLogger(), + }, + err: false, + }, + { + // Providing an invalid `AnonymizeSaltFile` should error. + c: Config{ + From: from, + AnonymizeLabels: []string{"foo"}, + AnonymizeSaltFile: "/this/path/does/not/exist", + Logger: log.NewNopLogger(), + }, + err: true, + }, + { + // Providing `AnonymizeSalt` takes preference over an invalid `AnonymizeSaltFile` and should not error. + c: Config{ + From: from, + AnonymizeLabels: []string{"foo"}, + AnonymizeSalt: "1", + AnonymizeSaltFile: "/this/path/does/not/exist", + Logger: log.NewNopLogger(), + }, + err: false, + }, + { + // Providing an invalid `FromCAFile` should error. + c: Config{ + From: from, + FromCAFile: "/this/path/does/not/exist", + Logger: log.NewNopLogger(), + }, + err: true, + }, + } + + for i := range tc { + if _, err := New(tc[i].c); (err != nil) != tc[i].err { + no := "no" + if tc[i].err { + no = "an" + } + t.Errorf("test case %d: got '%v', expected %s error", i, err, no) + } + } +} + +func TestReconfigure(t *testing.T) { + from, err := url.Parse("https://redhat.com") + if err != nil { + t.Fatalf("failed to parse `from` URL: %v", err) + } + c := Config{ + From: from, + Logger: log.NewNopLogger(), + } + w, err := New(c) + if err != nil { + t.Fatalf("failed to create new worker: %v", err) + } + + from2, err := url.Parse("https://redhat.com") + if err != nil { + t.Fatalf("failed to parse `from2` URL: %v", err) + } + + tc := []struct { + c Config + err bool + }{ + { + // Empty configuration should error. + c: Config{Logger: log.NewNopLogger()}, + err: true, + }, + { + // Configuration with new `From` should not error. + c: Config{ + From: from2, + Logger: log.NewNopLogger(), + }, + err: false, + }, + { + // Configuration with new invalid field should error. + c: Config{ + From: from, + FromTokenFile: "/this/path/does/not/exist", + Logger: log.NewNopLogger(), + }, + err: true, + }, + } + + for i := range tc { + if err := w.Reconfigure(tc[i].c); (err != nil) != tc[i].err { + no := "no" + if tc[i].err { + no = "an" + } + t.Errorf("test case %d: got %q, expected %s error", i, err, no) + } + } +} + +// TestRun tests the Run method of the Worker type. +// This test will: +// * instantiate a worker +// * configure the worker to make requests against a test server +// * in that test server, reconfigure the worker to make requests against a second test server +// * in the second test server, cancel the worker's context. +// This test will only succeed if the worker is able to be correctly reconfigured and canceled +// such that the Run method returns. +func TestRun(t *testing.T) { + c := Config{ + // Use a dummy URL. + From: &url.URL{}, + Logger: log.NewNopLogger(), + } + w, err := New(c) + if err != nil { + t.Fatalf("failed to create new worker: %v", err) + } + + ctx, cancel := context.WithCancel(context.Background()) + var once sync.Once + var wg sync.WaitGroup + + wg.Add(1) + // This is the second test server. We need to define it early so we can use its URL in the + // handler for the first test server. + // In this handler, we decrement the wait group and cancel the worker's context. + ts2 := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { + cancel() + once.Do(wg.Done) + })) + defer ts2.Close() + + // This is the first test server. + // In this handler, we test the Reconfigure method of the worker and point it to the second + // test server. + ts1 := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { + go func() { + from, err := url.Parse(ts2.URL) + if err != nil { + t.Fatalf("failed to parse second test server URL: %v", err) + } + if err := w.Reconfigure(Config{From: from, Logger: log.NewNopLogger()}); err != nil { + t.Fatalf("failed to reconfigure worker with second test server url: %v", err) + } + }() + })) + defer ts1.Close() + + from, err := url.Parse(ts1.URL) + if err != nil { + t.Fatalf("failed to parse first test server URL: %v", err) + } + if err := w.Reconfigure(Config{From: from, Logger: log.NewNopLogger()}); err != nil { + t.Fatalf("failed to reconfigure worker with first test server url: %v", err) + } + + wg.Add(1) + // In this goroutine we run the worker and only decrement + // the wait group when the worker finishes running. + go func() { + w.Run(ctx) + wg.Done() + }() + + wg.Wait() +} diff --git a/pkg/http/client.go b/pkg/http/client.go new file mode 100644 index 00000000..63492902 --- /dev/null +++ b/pkg/http/client.go @@ -0,0 +1,101 @@ +package http + +import ( + "net/http" + + "github.com/prometheus/client_golang/prometheus/promhttp" + + "github.com/prometheus/client_golang/prometheus" +) + +var ( + inFlightGauge = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "client_in_flight_requests", + Help: "A gauge of in-flight requests for the wrapped client.", + }, + []string{"client"}, + ) + + counter = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "client_api_requests_total", + Help: "A counter for requests from the wrapped client.", + }, + []string{"code", "method", "client"}, + ) + + dnsLatencyVec = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "dns_duration_seconds", + Help: "Trace dns latency histogram.", + Buckets: []float64{.005, .01, .025, .05}, + }, + []string{"event", "client"}, + ) + + tlsLatencyVec = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "tls_duration_seconds", + Help: "Trace tls latency histogram.", + Buckets: []float64{.05, .1, .25, .5}, + }, + []string{"event", "client"}, + ) + + histVec = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "request_duration_seconds", + Help: "A histogram of request latencies.", + Buckets: prometheus.DefBuckets, + }, + []string{"method", "client"}, + ) +) + +func init() { + prometheus.MustRegister(counter, tlsLatencyVec, dnsLatencyVec, histVec, inFlightGauge) +} + +func NewInstrumentedRoundTripper(clientName string, next http.RoundTripper) http.RoundTripper { + trace := &promhttp.InstrumentTrace{ + DNSStart: func(t float64) { + dnsLatencyVec. + WithLabelValues("dns_start", clientName). + Observe(t) + }, + DNSDone: func(t float64) { + dnsLatencyVec. + WithLabelValues("dns_done", clientName). + Observe(t) + }, + TLSHandshakeStart: func(t float64) { + tlsLatencyVec. + WithLabelValues("tls_handshake_start", clientName). + Observe(t) + }, + TLSHandshakeDone: func(t float64) { + tlsLatencyVec. + WithLabelValues("tls_handshake_done", clientName). + Observe(t) + }, + } + + inFlightGauge := inFlightGauge.WithLabelValues(clientName) + + counter := counter.MustCurryWith(prometheus.Labels{ + "client": clientName, + }) + + histVec := histVec.MustCurryWith(prometheus.Labels{ + "client": clientName, + }) + + return promhttp.InstrumentRoundTripperInFlight(inFlightGauge, + promhttp.InstrumentRoundTripperCounter(counter, + promhttp.InstrumentRoundTripperTrace(trace, + promhttp.InstrumentRoundTripperDuration(histVec, next), + ), + ), + ) +} diff --git a/pkg/http/roundtripper.go b/pkg/http/roundtripper.go new file mode 100644 index 00000000..5fa7ac04 --- /dev/null +++ b/pkg/http/roundtripper.go @@ -0,0 +1,78 @@ +package http + +import ( + "bytes" + "encoding/hex" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/http/httputil" + "unicode/utf8" + + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" +) + +type bearerRoundTripper struct { + token string + wrapper http.RoundTripper +} + +func NewBearerRoundTripper(token string, rt http.RoundTripper) http.RoundTripper { + return &bearerRoundTripper{token: token, wrapper: rt} +} + +func (rt *bearerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", rt.token)) + return rt.wrapper.RoundTrip(req) +} + +type debugRoundTripper struct { + next http.RoundTripper + logger log.Logger +} + +func NewDebugRoundTripper(logger log.Logger, next http.RoundTripper) *debugRoundTripper { + return &debugRoundTripper{next, log.With(logger, "component", "http/debugroundtripper")} +} + +func (rt *debugRoundTripper) RoundTrip(req *http.Request) (res *http.Response, err error) { + reqd, _ := httputil.DumpRequest(req, false) + reqBody := bodyToString(&req.Body) + + res, err = rt.next.RoundTrip(req) + if err != nil { + level.Error(rt.logger).Log("err", err) + return + } + + resd, _ := httputil.DumpResponse(res, false) + resBody := bodyToString(&res.Body) + + level.Debug(rt.logger).Log("msg", "round trip", "url", req.URL, "requestdump", string(reqd), "requestbody", reqBody, "responsedump", string(resd), "responsebody", resBody) + return +} + +func bodyToString(body *io.ReadCloser) string { + if *body == nil { + return "" + } + + var b bytes.Buffer + _, err := b.ReadFrom(*body) + if err != nil { + panic(err) + } + if err = (*body).Close(); err != nil { + panic(err) + } + *body = ioutil.NopCloser(&b) + + s := b.String() + if utf8.ValidString(s) { + return s + } + + return hex.Dump(b.Bytes()) +} diff --git a/pkg/http/routes.go b/pkg/http/routes.go new file mode 100644 index 00000000..85f0cf0e --- /dev/null +++ b/pkg/http/routes.go @@ -0,0 +1,50 @@ +package http + +import ( + "fmt" + "net/http" + "net/http/pprof" + + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +// DebugRoutes adds the debug handlers to a mux. +func DebugRoutes(mux *http.ServeMux) *http.ServeMux { + mux.HandleFunc("/debug/pprof/", pprof.Index) + mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + mux.HandleFunc("/debug/pprof/profile", pprof.Profile) + mux.Handle("/debug/pprof/block", pprof.Handler("block")) + mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + mux.HandleFunc("/debug/pprof/trace", pprof.Trace) + return mux +} + +// HealthRoutes adds the health checks to a mux. +func HealthRoutes(mux *http.ServeMux) *http.ServeMux { + mux.HandleFunc("/healthz", func(w http.ResponseWriter, req *http.Request) { fmt.Fprintln(w, "ok") }) + mux.HandleFunc("/healthz/ready", func(w http.ResponseWriter, req *http.Request) { fmt.Fprintln(w, "ok") }) + return mux +} + +// MetricRoutes adds the metrics endpoint to a mux. +func MetricRoutes(mux *http.ServeMux) *http.ServeMux { + mux.Handle("/metrics", promhttp.Handler()) + return mux +} + +// ReloadRoutes adds the reload endpoint to a mux. +func ReloadRoutes(mux *http.ServeMux, reload func() error) *http.ServeMux { + mux.HandleFunc("/-/reload", func(w http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + if err := reload(); err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + }) + return mux +} diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go new file mode 100644 index 00000000..5f5c5dff --- /dev/null +++ b/pkg/logger/logger.go @@ -0,0 +1,21 @@ +package logger + +import ( + "github.com/go-kit/kit/log/level" +) + +// LogLevelFromString determines log level to string, defaults to all, +func LogLevelFromString(l string) level.Option { + switch l { + case "debug": + return level.AllowDebug() + case "info": + return level.AllowInfo() + case "warn": + return level.AllowWarn() + case "error": + return level.AllowError() + default: + return level.AllowAll() + } +} diff --git a/pkg/metricfamily/anonymize.go b/pkg/metricfamily/anonymize.go new file mode 100644 index 00000000..a9e5f5ab --- /dev/null +++ b/pkg/metricfamily/anonymize.go @@ -0,0 +1,82 @@ +package metricfamily + +import ( + "crypto/sha256" + "encoding/base64" + + clientmodel "github.com/prometheus/client_model/go" +) + +type AnonymizeMetrics struct { + salt string + global map[string]struct{} + byMetric map[string]map[string]struct{} +} + +// NewMetricsAnonymizer hashes label values on the incoming metrics using a cryptographic hash. +// Because the cardinality of most label values is low, only a portion of the hash is returned. +// To prevent rainbow tables from being used to recover the label value, each client should use +// a salt value. Because label values are expected to remain stable over many sessions, the salt +// must also be stable over the same time period. The salt should not be shared with the remote +// agent. This type is not thread-safe. +func NewMetricsAnonymizer(salt string, labels []string, metricsLabels map[string][]string) *AnonymizeMetrics { + global := make(map[string]struct{}) + for _, label := range labels { + global[label] = struct{}{} + } + byMetric := make(map[string]map[string]struct{}) + for name, labels := range metricsLabels { + l := make(map[string]struct{}) + for _, label := range labels { + l[label] = struct{}{} + } + byMetric[name] = l + } + return &AnonymizeMetrics{ + salt: salt, + global: global, + byMetric: byMetric, + } +} + +func (a *AnonymizeMetrics) Transform(family *clientmodel.MetricFamily) (bool, error) { + if family == nil { + return false, nil + } + if set, ok := a.byMetric[family.GetName()]; ok { + transformMetricLabelValues(a.salt, family.Metric, a.global, set) + } else { + transformMetricLabelValues(a.salt, family.Metric, a.global) + } + return true, nil +} + +func transformMetricLabelValues(salt string, metrics []*clientmodel.Metric, sets ...map[string]struct{}) { + for _, m := range metrics { + if m == nil { + continue + } + for _, pair := range m.Label { + if pair.Value == nil || *pair.Value == "" { + continue + } + name := pair.GetName() + for _, set := range sets { + _, ok := set[name] + if !ok { + continue + } + v := secureValueHash(salt, pair.GetValue()) + pair.Value = &v + break + } + } + } +} + +// secureValueHash hashes the input value for moderately low cardinality (< 1 million unique inputs) +// and converts it to a base64 string suitable for use as a label value in Prometheus. +func secureValueHash(salt, value string) string { + hash := sha256.Sum256([]byte(salt + value)) + return base64.RawURLEncoding.EncodeToString(hash[:9]) +} diff --git a/pkg/metricfamily/count.go b/pkg/metricfamily/count.go new file mode 100644 index 00000000..9e04a9ff --- /dev/null +++ b/pkg/metricfamily/count.go @@ -0,0 +1,16 @@ +package metricfamily + +import clientmodel "github.com/prometheus/client_model/go" + +type Count struct { + families int + metrics int +} + +func (t *Count) Metrics() int { return t.metrics } + +func (t *Count) Transform(family *clientmodel.MetricFamily) (bool, error) { + t.families++ + t.metrics += len(family.Metric) + return true, nil +} diff --git a/pkg/metricfamily/drop_timestamp.go b/pkg/metricfamily/drop_timestamp.go new file mode 100644 index 00000000..c1b76d24 --- /dev/null +++ b/pkg/metricfamily/drop_timestamp.go @@ -0,0 +1,19 @@ +package metricfamily + +import clientmodel "github.com/prometheus/client_model/go" + +// DropTimestamp is a transformer that removes timestamps from metrics. +func DropTimestamp(family *clientmodel.MetricFamily) (bool, error) { + if family == nil { + return true, nil + } + + for _, m := range family.Metric { + if m == nil { + continue + } + m.TimestampMs = nil + } + + return true, nil +} diff --git a/pkg/metricfamily/drop_timestamp_test.go b/pkg/metricfamily/drop_timestamp_test.go new file mode 100644 index 00000000..ecc064b9 --- /dev/null +++ b/pkg/metricfamily/drop_timestamp_test.go @@ -0,0 +1,113 @@ +package metricfamily + +import ( + "fmt" + "testing" + + clientmodel "github.com/prometheus/client_model/go" +) + +func TestDropTimestamp(t *testing.T) { + + family := func(name string, metrics ...*clientmodel.Metric) *clientmodel.MetricFamily { + families := &clientmodel.MetricFamily{Name: &name} + families.Metric = append(families.Metric, metrics...) + return families + } + + metric := func(timestamp *int64) *clientmodel.Metric { return &clientmodel.Metric{TimestampMs: timestamp} } + + timestamp := func(timestamp int64) *int64 { return ×tamp } + + type checkFunc func(family *clientmodel.MetricFamily, ok bool, err error) error + + isOK := func(want bool) checkFunc { + return func(_ *clientmodel.MetricFamily, got bool, _ error) error { + if want != got { + return fmt.Errorf("want ok %t, got %t", want, got) + } + return nil + } + } + + hasErr := func(want error) checkFunc { + return func(_ *clientmodel.MetricFamily, _ bool, got error) error { + if want != got { + return fmt.Errorf("want err %v, got %v", want, got) + } + return nil + } + } + + hasMetrics := func(want int) checkFunc { + return func(m *clientmodel.MetricFamily, _ bool, _ error) error { + if got := len(m.Metric); want != got { + return fmt.Errorf("want len(m.Metric)=%v, got %v", want, got) + } + return nil + } + } + + metricsHaveTimestamps := func(want bool) checkFunc { + return func(m *clientmodel.MetricFamily, _ bool, _ error) error { + for _, metric := range m.Metric { + if got := metric.TimestampMs != nil; want != got { + return fmt.Errorf("want metrics to have timestamp %t, got %t", want, got) + } + } + return nil + } + } + + for _, tc := range []struct { + family *clientmodel.MetricFamily + name string + checks []checkFunc + }{ + { + name: "nil family", + checks: []checkFunc{ + isOK(true), + hasErr(nil), + }, + }, + { + name: "family without timestamp", + family: family("foo"), + checks: []checkFunc{ + isOK(true), + hasErr(nil), + }, + }, + { + name: "family without timestamp", + family: family("foo", metric(nil)), + checks: []checkFunc{ + isOK(true), + hasErr(nil), + hasMetrics(1), + metricsHaveTimestamps(false), + }, + }, + { + name: "family with timestamp", + family: family("foo", metric(nil), metric(timestamp(1))), + checks: []checkFunc{ + isOK(true), + hasErr(nil), + hasMetrics(2), + metricsHaveTimestamps(false), + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + ok, err := DropTimestamp(tc.family) + + for _, check := range tc.checks { + if err := check(tc.family, ok, err); err != nil { + t.Error(err) + } + } + }) + } +} diff --git a/pkg/metricfamily/drop_unsorted.go b/pkg/metricfamily/drop_unsorted.go new file mode 100644 index 00000000..67cbc88d --- /dev/null +++ b/pkg/metricfamily/drop_unsorted.go @@ -0,0 +1,26 @@ +package metricfamily + +import clientmodel "github.com/prometheus/client_model/go" + +type DropUnsorted struct { + timestamp int64 +} + +func (o *DropUnsorted) Transform(family *clientmodel.MetricFamily) (bool, error) { + for i, m := range family.Metric { + if m == nil { + continue + } + var ts int64 + if m.TimestampMs != nil { + ts = *m.TimestampMs + } + if ts < o.timestamp { + family.Metric[i] = nil + continue + } + o.timestamp = ts + } + o.timestamp = 0 + return true, nil +} diff --git a/pkg/metricfamily/elide.go b/pkg/metricfamily/elide.go new file mode 100644 index 00000000..c8e7f83b --- /dev/null +++ b/pkg/metricfamily/elide.go @@ -0,0 +1,40 @@ +package metricfamily + +import ( + prom "github.com/prometheus/client_model/go" +) + +type elide struct { + labelSet map[string]struct{} +} + +// NewElide creates a new elide transformer for the given metrics. +func NewElide(labels ...string) *elide { + labelSet := make(map[string]struct{}) + for i := range labels { + labelSet[labels[i]] = struct{}{} + } + + return &elide{labelSet} +} + +// Transform filters label pairs in the given metrics family, +// eliding labels. +func (t *elide) Transform(family *prom.MetricFamily) (bool, error) { + if family == nil || len(family.Metric) == 0 { + return true, nil + } + + for i := range family.Metric { + var filtered []*prom.LabelPair + for j := range family.Metric[i].Label { + if _, elide := t.labelSet[family.Metric[i].Label[j].GetName()]; elide { + continue + } + filtered = append(filtered, family.Metric[i].Label[j]) + } + family.Metric[i].Label = filtered + } + + return true, nil +} diff --git a/pkg/metricfamily/elide_test.go b/pkg/metricfamily/elide_test.go new file mode 100644 index 00000000..236caec4 --- /dev/null +++ b/pkg/metricfamily/elide_test.go @@ -0,0 +1,216 @@ +package metricfamily + +import ( + "fmt" + "testing" + + "github.com/golang/protobuf/proto" + clientmodel "github.com/prometheus/client_model/go" +) + +func TestElide(t *testing.T) { + family := func(metrics ...*clientmodel.Metric) *clientmodel.MetricFamily { + families := &clientmodel.MetricFamily{Name: proto.String("test")} + families.Metric = append(families.Metric, metrics...) + return families + } + + type checkFunc func(family *clientmodel.MetricFamily, ok bool, err error) error + + isOK := func(want bool) checkFunc { + return func(_ *clientmodel.MetricFamily, got bool, _ error) error { + if want != got { + return fmt.Errorf("want ok %t, got %t", want, got) + } + return nil + } + } + + hasErr := func(want error) checkFunc { + return func(_ *clientmodel.MetricFamily, _ bool, got error) error { + if want != got { + return fmt.Errorf("want err %v, got %v", want, got) + } + return nil + } + } + + metricIsNil := func(want bool) checkFunc { + return func(m *clientmodel.MetricFamily, _ bool, _ error) error { + if got := m == nil; want != got { + return fmt.Errorf("want metric to be nil=%t, got %t", want, got) + } + return nil + } + } + + hasMetricCount := func(want int) checkFunc { + return func(m *clientmodel.MetricFamily, _ bool, _ error) error { + if got := len(m.Metric); want != got { + return fmt.Errorf("want len(m.Metric)=%v, got %v", want, got) + } + return nil + } + } + + hasLabelCount := func(want ...int) checkFunc { + return func(family *clientmodel.MetricFamily, _ bool, _ error) error { + for i := range family.Metric { + if got := len(family.Metric[i].Label); got != want[i] { + return fmt.Errorf( + "want len(m.Metric[%v].Label)=%v, got %v", + i, want[i], got) + } + } + return nil + } + } + + hasLabels := func(want bool, labels ...string) checkFunc { + return func(family *clientmodel.MetricFamily, _ bool, _ error) error { + labelSet := make(map[string]struct{}) + for i := range family.Metric { + for j := range family.Metric[i].Label { + labelSet[family.Metric[i].Label[j].GetName()] = struct{}{} + } + } + + for _, label := range labels { + if _, got := labelSet[label]; want != got { + wants := "present" + if !want { + wants = "not present" + } + + gots := "is" + if !got { + gots = "isn't" + } + + return fmt.Errorf( + "want label %q be %s in metrics, but it %s", + label, wants, gots, + ) + } + } + + return nil + } + } + + metricWithLabels := func(labels ...string) *clientmodel.Metric { + var labelPairs []*clientmodel.LabelPair + for _, l := range labels { + labelPairs = append(labelPairs, &clientmodel.LabelPair{Name: proto.String(l)}) + } + return &clientmodel.Metric{Label: labelPairs} + } + + for _, tc := range []struct { + family *clientmodel.MetricFamily + elide *elide + name string + checks []checkFunc + }{ + { + name: "nil family", + family: nil, + elide: NewElide("elide"), + checks: []checkFunc{ + isOK(true), + hasErr(nil), + metricIsNil(true), + }, + }, + { + name: "empty family", + family: family(), + elide: NewElide("elide"), + checks: []checkFunc{ + isOK(true), + hasErr(nil), + hasMetricCount(0), + }, + }, + { + name: "one elide one retain", + family: family(metricWithLabels("retain", "elide")), + elide: NewElide("elide"), + checks: []checkFunc{ + isOK(true), + hasErr(nil), + hasMetricCount(1), + hasLabelCount(1), + hasLabels(false, "elide"), + hasLabels(true, "retain"), + }, + }, + { + name: "no match", + family: family(metricWithLabels("retain")), + elide: NewElide("elide"), + checks: []checkFunc{ + isOK(true), + hasErr(nil), + hasMetricCount(1), + hasLabelCount(1), + hasLabels(false, "elide"), + hasLabels(true, "retain"), + }, + }, + { + name: "single match", + family: family(metricWithLabels("elide")), + elide: NewElide("elide"), + checks: []checkFunc{ + isOK(true), + hasErr(nil), + hasMetricCount(1), + hasLabelCount(0), + hasLabels(false, "elide"), + }, + }, + { + name: "multiple retains, multiple elides", + family: family( + metricWithLabels("elide1", "elide2", "retain1", "retain2"), + ), + elide: NewElide("elide1", "elide2"), + checks: []checkFunc{ + isOK(true), + hasErr(nil), + hasMetricCount(1), + hasLabelCount(2), + hasLabels(false, "elide1"), + hasLabels(false, "elide2"), + hasLabels(true, "retain1"), + hasLabels(true, "retain2"), + }, + }, + { + name: "empty elider", + family: family( + metricWithLabels("retain1", "retain2"), + ), + elide: NewElide(), + checks: []checkFunc{ + isOK(true), + hasErr(nil), + hasMetricCount(1), + hasLabelCount(2), + hasLabels(true, "retain1"), + hasLabels(true, "retain2"), + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + ok, err := tc.elide.Transform(tc.family) + + for _, check := range tc.checks { + if err := check(tc.family, ok, err); err != nil { + t.Error(err) + } + } + }) + } +} diff --git a/pkg/metricfamily/empty.go b/pkg/metricfamily/empty.go new file mode 100644 index 00000000..8879e293 --- /dev/null +++ b/pkg/metricfamily/empty.go @@ -0,0 +1,12 @@ +package metricfamily + +import clientmodel "github.com/prometheus/client_model/go" + +func DropEmptyFamilies(family *clientmodel.MetricFamily) (bool, error) { + for _, m := range family.Metric { + if m != nil { + return true, nil + } + } + return false, nil +} diff --git a/pkg/metricfamily/expired.go b/pkg/metricfamily/expired.go new file mode 100644 index 00000000..62ba50ec --- /dev/null +++ b/pkg/metricfamily/expired.go @@ -0,0 +1,30 @@ +package metricfamily + +import ( + "time" + + clientmodel "github.com/prometheus/client_model/go" +) + +type dropExpiredSamples struct { + min int64 +} + +func NewDropExpiredSamples(min time.Time) Transformer { + return &dropExpiredSamples{ + min: min.Unix() * 1000, + } +} + +func (t *dropExpiredSamples) Transform(family *clientmodel.MetricFamily) (bool, error) { + for i, m := range family.Metric { + if m == nil { + continue + } + if ts := m.GetTimestampMs(); ts < t.min { + family.Metric[i] = nil + continue + } + } + return true, nil +} diff --git a/pkg/metricfamily/invalid.go b/pkg/metricfamily/invalid.go new file mode 100644 index 00000000..99d2c2f7 --- /dev/null +++ b/pkg/metricfamily/invalid.go @@ -0,0 +1,192 @@ +package metricfamily + +import ( + "fmt" + "time" + + clientmodel "github.com/prometheus/client_model/go" +) + +type errorInvalidFederateSamples struct { + min int64 +} + +func NewErrorInvalidFederateSamples(min time.Time) Transformer { + return &errorInvalidFederateSamples{ + min: min.Unix() * 1000, + } +} + +func (t *errorInvalidFederateSamples) Transform(family *clientmodel.MetricFamily) (bool, error) { + name := family.GetName() + if len(name) == 0 { + return false, nil + } + if len(name) > 255 { + return false, fmt.Errorf("metrics_name cannot be longer than 255 characters") + } + if family.Type == nil { + return false, nil + } + switch t := *family.Type; t { + case clientmodel.MetricType_COUNTER: + case clientmodel.MetricType_GAUGE: + case clientmodel.MetricType_HISTOGRAM: + case clientmodel.MetricType_SUMMARY: + case clientmodel.MetricType_UNTYPED: + default: + return false, fmt.Errorf("unknown metric type %s", t) + } + + for _, m := range family.Metric { + if m == nil { + continue + } + for _, label := range m.Label { + if label.Name == nil || len(*label.Name) == 0 || len(*label.Name) > 255 { + return false, fmt.Errorf("label_name cannot be longer than 255 characters") + } + if label.Value == nil || len(*label.Value) > 255 { + return false, fmt.Errorf("label_value cannot be longer than 255 characters") + } + } + if m.TimestampMs == nil { + return false, ErrNoTimestamp + } + if *m.TimestampMs < t.min { + return false, ErrTimestampTooOld + } + switch t := *family.Type; t { + case clientmodel.MetricType_COUNTER: + if m.Counter == nil || m.Gauge != nil || m.Histogram != nil || m.Summary != nil || m.Untyped != nil { + return false, fmt.Errorf("metric type %s must have counter field set", t) + } + case clientmodel.MetricType_GAUGE: + if m.Counter != nil || m.Gauge == nil || m.Histogram != nil || m.Summary != nil || m.Untyped != nil { + return false, fmt.Errorf("metric type %s must have gauge field set", t) + } + case clientmodel.MetricType_HISTOGRAM: + if m.Counter != nil || m.Gauge != nil || m.Histogram == nil || m.Summary != nil || m.Untyped != nil { + return false, fmt.Errorf("metric type %s must have histogram field set", t) + } + case clientmodel.MetricType_SUMMARY: + if m.Counter != nil || m.Gauge != nil || m.Histogram != nil || m.Summary == nil || m.Untyped != nil { + return false, fmt.Errorf("metric type %s must have summary field set", t) + } + case clientmodel.MetricType_UNTYPED: + if m.Counter != nil || m.Gauge != nil || m.Histogram != nil || m.Summary != nil || m.Untyped == nil { + return false, fmt.Errorf("metric type %s must have untyped field set", t) + } + } + } + return true, nil +} + +type dropInvalidFederateSamples struct { + min int64 +} + +func NewDropInvalidFederateSamples(min time.Time) Transformer { + return &dropInvalidFederateSamples{ + min: min.Unix() * 1000, + } +} + +func (t *dropInvalidFederateSamples) Transform(family *clientmodel.MetricFamily) (bool, error) { + name := family.GetName() + if len(name) == 0 { + return false, nil + } + if len(name) > 255 { + return false, nil + } + if family.Type == nil { + return false, nil + } + switch t := *family.Type; t { + case clientmodel.MetricType_COUNTER: + case clientmodel.MetricType_GAUGE: + case clientmodel.MetricType_HISTOGRAM: + case clientmodel.MetricType_SUMMARY: + case clientmodel.MetricType_UNTYPED: + default: + return false, nil + } + + for i, m := range family.Metric { + if m == nil { + continue + } + packLabels := false + for j, label := range m.Label { + if label.Name == nil || len(*label.Name) == 0 || len(*label.Name) > 255 { + m.Label[j] = nil + packLabels = true + } + if label.Value == nil || len(*label.Value) > 255 { + m.Label[j] = nil + packLabels = true + } + } + if packLabels { + m.Label = PackLabels(m.Label) + } + if m.TimestampMs == nil || *m.TimestampMs < t.min { + family.Metric[i] = nil + continue + } + switch t := *family.Type; t { + case clientmodel.MetricType_COUNTER: + if m.Counter == nil || m.Gauge != nil || m.Histogram != nil || m.Summary != nil || m.Untyped != nil { + family.Metric[i] = nil + } + case clientmodel.MetricType_GAUGE: + if m.Counter != nil || m.Gauge == nil || m.Histogram != nil || m.Summary != nil || m.Untyped != nil { + family.Metric[i] = nil + } + case clientmodel.MetricType_HISTOGRAM: + if m.Counter != nil || m.Gauge != nil || m.Histogram == nil || m.Summary != nil || m.Untyped != nil { + family.Metric[i] = nil + } + case clientmodel.MetricType_SUMMARY: + if m.Counter != nil || m.Gauge != nil || m.Histogram != nil || m.Summary == nil || m.Untyped != nil { + family.Metric[i] = nil + } + case clientmodel.MetricType_UNTYPED: + if m.Counter != nil || m.Gauge != nil || m.Histogram != nil || m.Summary != nil || m.Untyped == nil { + family.Metric[i] = nil + } + } + } + return true, nil +} + +// PackLabels fills holes in the label slice by shifting items towards the zero index. +// It will modify the slice in place. +func PackLabels(labels []*clientmodel.LabelPair) []*clientmodel.LabelPair { + j := len(labels) + next := 0 +Found: + for i := 0; i < j; i++ { + if labels[i] != nil { + continue + } + // scan for the next non-nil metric + if next <= i { + next = i + 1 + } + for k := next; k < j; k++ { + if labels[k] == nil { + continue + } + // fill the current i with a non-nil metric + labels[i], labels[k] = labels[k], nil + next = k + 1 + continue Found + } + // no more valid metrics + labels = labels[:i] + break + } + return labels +} diff --git a/pkg/metricfamily/label.go b/pkg/metricfamily/label.go new file mode 100644 index 00000000..c39bf978 --- /dev/null +++ b/pkg/metricfamily/label.go @@ -0,0 +1,76 @@ +package metricfamily + +import ( + "sync" + + clientmodel "github.com/prometheus/client_model/go" +) + +type LabelRetriever interface { + Labels() (map[string]string, error) +} + +type label struct { + labels map[string]*clientmodel.LabelPair + retriever LabelRetriever + mu sync.Mutex +} + +func NewLabel(labels map[string]string, retriever LabelRetriever) Transformer { + pairs := make(map[string]*clientmodel.LabelPair) + for k, v := range labels { + name, value := k, v + pairs[k] = &clientmodel.LabelPair{Name: &name, Value: &value} + } + return &label{ + labels: pairs, + retriever: retriever, + } +} + +func (t *label) Transform(family *clientmodel.MetricFamily) (bool, error) { + t.mu.Lock() + defer t.mu.Unlock() + // lazily resolve the label retriever as needed + if t.retriever != nil && len(family.Metric) > 0 { + added, err := t.retriever.Labels() + if err != nil { + return false, err + } + t.retriever = nil + for k, v := range added { + name, value := k, v + t.labels[k] = &clientmodel.LabelPair{Name: &name, Value: &value} + } + } + for _, m := range family.Metric { + m.Label = appendLabels(m.Label, t.labels) + } + return true, nil +} + +func appendLabels(existing []*clientmodel.LabelPair, overrides map[string]*clientmodel.LabelPair) []*clientmodel.LabelPair { + var found []string + for i, pair := range existing { + name := pair.GetName() + if value, ok := overrides[name]; ok { + existing[i] = value + found = append(found, name) + } + } + for k, v := range overrides { + if !contains(found, k) { + existing = append(existing, v) + } + } + return existing +} + +func contains(values []string, s string) bool { + for _, v := range values { + if s == v { + return true + } + } + return false +} diff --git a/pkg/metricfamily/multi_transformer.go b/pkg/metricfamily/multi_transformer.go new file mode 100644 index 00000000..df0e103d --- /dev/null +++ b/pkg/metricfamily/multi_transformer.go @@ -0,0 +1,42 @@ +package metricfamily + +import ( + clientmodel "github.com/prometheus/client_model/go" +) + +type MultiTransformer struct { + transformers []Transformer + builderFuncs []func() Transformer +} + +func (a *MultiTransformer) With(t Transformer) { + if t != nil { + a.transformers = append(a.transformers, t) + } +} + +func (a *MultiTransformer) WithFunc(f func() Transformer) { + a.builderFuncs = append(a.builderFuncs, f) +} + +func (a MultiTransformer) Transform(family *clientmodel.MetricFamily) (bool, error) { + var ts []Transformer + + for _, f := range a.builderFuncs { + ts = append(ts, f()) + } + + ts = append(ts, a.transformers...) + + for _, t := range ts { + ok, err := t.Transform(family) + if err != nil { + return false, err + } + if !ok { + return false, nil + } + } + + return true, nil +} diff --git a/pkg/metricfamily/none.go b/pkg/metricfamily/none.go new file mode 100644 index 00000000..c815d186 --- /dev/null +++ b/pkg/metricfamily/none.go @@ -0,0 +1,5 @@ +package metricfamily + +import clientmodel "github.com/prometheus/client_model/go" + +func None(*clientmodel.MetricFamily) (bool, error) { return true, nil } diff --git a/pkg/metricfamily/overwrite.go b/pkg/metricfamily/overwrite.go new file mode 100644 index 00000000..ae68fdd2 --- /dev/null +++ b/pkg/metricfamily/overwrite.go @@ -0,0 +1,53 @@ +package metricfamily + +import ( + "time" + + "github.com/prometheus/client_golang/prometheus" + client "github.com/prometheus/client_model/go" +) + +// driftRange is used to observe timestamps being older than 5min, newer than 5min, +// or within the present (+-5min) +const driftRange = 5 * time.Minute + +var ( + overwrittenMetrics = prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "telemeter_overwritten_timestamps_total", + Help: "Number of timestamps that were in the past, present or future", + }, []string{"tense"}) +) + +func init() { + prometheus.MustRegister(overwrittenMetrics) +} + +// OverwriteTimestamps sets all timestamps to the current time. +// We essentially already do this in Telemeter v1 by dropping all timestamps on Telemeter Servers +// and then when federating Telemeter Prometheus sets its own current timestamp. +// For v2 we want to be consistent when using remote-write and thus we overwrite the timestamps +// on Telemeter Server already to forward the same timestamps to both systems. +func OverwriteTimestamps(now func() time.Time) TransformerFunc { + return func(family *client.MetricFamily) (bool, error) { + timestamp := now().Unix() * 1000 + for i, m := range family.Metric { + observeDrift(now, m.GetTimestampMs()) + + family.Metric[i].TimestampMs = ×tamp + } + return true, nil + } +} + +func observeDrift(now func() time.Time, ms int64) { + timestamp := time.Unix(ms/1000, 0) + + if timestamp.Before(now().Add(-driftRange)) { + overwrittenMetrics.WithLabelValues("past").Inc() + } else if timestamp.After(now().Add(driftRange)) { + overwrittenMetrics.WithLabelValues("future").Inc() + } else { + overwrittenMetrics.WithLabelValues("present").Inc() + } + +} diff --git a/pkg/metricfamily/pack.go b/pkg/metricfamily/pack.go new file mode 100644 index 00000000..52e6b3b8 --- /dev/null +++ b/pkg/metricfamily/pack.go @@ -0,0 +1,62 @@ +package metricfamily + +import clientmodel "github.com/prometheus/client_model/go" + +func PackMetrics(family *clientmodel.MetricFamily) (bool, error) { + metrics := family.Metric + j := len(metrics) + next := 0 +Found: + for i := 0; i < j; i++ { + if metrics[i] != nil { + continue + } + // scan for the next non-nil metric + if next <= i { + next = i + 1 + } + for k := next; k < j; k++ { + if metrics[k] == nil { + continue + } + // fill the current i with a non-nil metric + metrics[i], metrics[k] = metrics[k], nil + next = k + 1 + continue Found + } + // no more valid metrics + family.Metric = metrics[:i] + break + } + return len(family.Metric) > 0, nil +} + +// Pack returns only families with metrics in the returned array, preserving the +// order of the original slice. Nil entries are removed from the slice. The returned +// slice may be empty. +func Pack(families []*clientmodel.MetricFamily) []*clientmodel.MetricFamily { + j := len(families) + next := 0 +Found: + for i := 0; i < j; i++ { + if families[i] != nil && len(families[i].Metric) > 0 { + continue + } + // scan for the next non-nil family + if next <= i { + next = i + 1 + } + for k := next; k < j; k++ { + if families[k] == nil || len(families[k].Metric) == 0 { + continue + } + // fill the current i with a non-nil family + families[i], families[k] = families[k], nil + next = k + 1 + continue Found + } + // no more valid families + return families[:i] + } + return families +} diff --git a/pkg/metricfamily/rename.go b/pkg/metricfamily/rename.go new file mode 100644 index 00000000..3e9b2532 --- /dev/null +++ b/pkg/metricfamily/rename.go @@ -0,0 +1,17 @@ +package metricfamily + +import clientmodel "github.com/prometheus/client_model/go" + +type RenameMetrics struct { + Names map[string]string +} + +func (m RenameMetrics) Transform(family *clientmodel.MetricFamily) (bool, error) { + if family == nil || family.Name == nil { + return true, nil + } + if replace, ok := m.Names[*family.Name]; ok { + family.Name = &replace + } + return true, nil +} diff --git a/pkg/metricfamily/required.go b/pkg/metricfamily/required.go new file mode 100644 index 00000000..94afa61b --- /dev/null +++ b/pkg/metricfamily/required.go @@ -0,0 +1,43 @@ +package metricfamily + +import ( + "fmt" + + clientmodel "github.com/prometheus/client_model/go" +) + +type requireLabel struct { + labels map[string]string +} + +func NewRequiredLabels(labels map[string]string) Transformer { + return requireLabel{labels: labels} +} + +var ( + ErrRequiredLabelMissing = fmt.Errorf("a required label is missing from the metric") +) + +func (t requireLabel) Transform(family *clientmodel.MetricFamily) (bool, error) { + for k, v := range t.labels { + Metrics: + for _, m := range family.Metric { + if m == nil { + continue + } + for _, label := range m.Label { + if label == nil { + continue + } + if label.GetName() == k { + if label.GetValue() != v { + return false, fmt.Errorf("expected label %s to have value %s instead of %s", label.GetName(), v, label.GetValue()) + } + continue Metrics + } + } + return false, ErrRequiredLabelMissing + } + } + return true, nil +} diff --git a/pkg/metricfamily/sort.go b/pkg/metricfamily/sort.go new file mode 100644 index 00000000..87ee9ce6 --- /dev/null +++ b/pkg/metricfamily/sort.go @@ -0,0 +1,128 @@ +package metricfamily + +import ( + "sort" + + clientmodel "github.com/prometheus/client_model/go" +) + +func SortMetrics(family *clientmodel.MetricFamily) (bool, error) { + sort.Sort(MetricsByTimestamp(family.Metric)) + return true, nil +} + +type MetricsByTimestamp []*clientmodel.Metric + +func (m MetricsByTimestamp) Len() int { + return len(m) +} + +func (m MetricsByTimestamp) Less(i int, j int) bool { + a, b := m[i], m[j] + if a == nil { + return b != nil + } + if b == nil { + return false + } + if a.TimestampMs == nil { + return b.TimestampMs != nil + } + if b.TimestampMs == nil { + return false + } + return *a.TimestampMs < *b.TimestampMs +} + +func (m MetricsByTimestamp) Swap(i int, j int) { + m[i], m[j] = m[j], m[i] +} + +// MergeSortedWithTimestamps collapses metrics families with the same name into a single family, +// preserving the order of the metrics. Families must be dense (no nils for families or metrics), +// all metrics must be sorted, and all metrics must have timestamps. +func MergeSortedWithTimestamps(families []*clientmodel.MetricFamily) []*clientmodel.MetricFamily { + var dst *clientmodel.MetricFamily + for pos, src := range families { + if dst == nil { + dst = src + continue + } + if dst.GetName() != src.GetName() { + dst = nil + continue + } + + lenI, lenJ := len(dst.Metric), len(src.Metric) + + // if the ranges don't overlap, we can block merge + dstBegin, dstEnd := *dst.Metric[0].TimestampMs, *dst.Metric[lenI-1].TimestampMs + srcBegin, srcEnd := *src.Metric[0].TimestampMs, *src.Metric[lenJ-1].TimestampMs + if dstEnd < srcBegin { + dst.Metric = append(dst.Metric, src.Metric...) + families[pos] = nil + continue + } + if srcEnd < dstBegin { + dst.Metric = append(src.Metric, dst.Metric...) + families[pos] = nil + continue + } + + // zip merge + i, j := 0, 0 + result := make([]*clientmodel.Metric, 0, lenI+lenJ) + Merge: + for { + switch { + case j >= lenJ: + for ; i < lenI; i++ { + result = append(result, dst.Metric[i]) + } + break Merge + case i >= lenI: + for ; j < lenJ; j++ { + result = append(result, src.Metric[j]) + } + break Merge + default: + a, b := *dst.Metric[i].TimestampMs, *src.Metric[j].TimestampMs + if a <= b { + result = append(result, dst.Metric[i]) + i++ + } else { + result = append(result, src.Metric[j]) + j++ + } + } + } + dst.Metric = result + families[pos] = nil + } + return Pack(families) +} + +// PackedFamilyWithTimestampsByName sorts a packed slice of metrics +// (no nils, all families have at least one metric, and all metrics +// have timestamps) in order of metric name and then oldest sample +type PackedFamilyWithTimestampsByName []*clientmodel.MetricFamily + +func (families PackedFamilyWithTimestampsByName) Len() int { + return len(families) +} + +func (families PackedFamilyWithTimestampsByName) Less(i int, j int) bool { + a, b := families[i].GetName(), families[j].GetName() + if a < b { + return true + } + if a > b { + return false + } + tA, tB := *families[i].Metric[0].TimestampMs, *families[j].Metric[0].TimestampMs + return tA < tB +} + +func (families PackedFamilyWithTimestampsByName) Swap(i int, j int) { + families[i], families[j] = families[j], families[i] +} diff --git a/pkg/metricfamily/transform.go b/pkg/metricfamily/transform.go new file mode 100644 index 00000000..9d585425 --- /dev/null +++ b/pkg/metricfamily/transform.go @@ -0,0 +1,41 @@ +package metricfamily + +import ( + clientmodel "github.com/prometheus/client_model/go" +) + +type Transformer interface { + Transform(*clientmodel.MetricFamily) (ok bool, err error) +} + +type TransformerFunc func(*clientmodel.MetricFamily) (ok bool, err error) + +func (f TransformerFunc) Transform(family *clientmodel.MetricFamily) (ok bool, err error) { + return f(family) +} + +// MetricsCount returns the number of unique metrics in the given families. It skips +// nil families but does not skip nil metrics. +func MetricsCount(families []*clientmodel.MetricFamily) int { + count := 0 + for _, family := range families { + if family == nil { + continue + } + count += len(family.Metric) + } + return count +} + +func Filter(families []*clientmodel.MetricFamily, filter Transformer) error { + for i, family := range families { + ok, err := filter.Transform(family) + if err != nil { + return err + } + if !ok { + families[i] = nil + } + } + return nil +} diff --git a/pkg/metricfamily/transform_test.go b/pkg/metricfamily/transform_test.go new file mode 100644 index 00000000..270d03f4 --- /dev/null +++ b/pkg/metricfamily/transform_test.go @@ -0,0 +1,117 @@ +package metricfamily + +import ( + "reflect" + "testing" + + clientmodel "github.com/prometheus/client_model/go" +) + +func family(name string, timestamps ...int64) *clientmodel.MetricFamily { + families := &clientmodel.MetricFamily{Name: &name} + for i := range timestamps { + families.Metric = append(families.Metric, &clientmodel.Metric{TimestampMs: ×tamps[i]}) + } + return families +} + +func metric(timestamp int64) *clientmodel.Metric { + return &clientmodel.Metric{ + TimestampMs: ×tamp, + } +} + +func TestPack(t *testing.T) { + a := family("A", 0) + b := family("B", 1) + c := family("C", 2) + d := family("D") + + tests := []struct { + name string + args []*clientmodel.MetricFamily + want []*clientmodel.MetricFamily + }{ + {name: "empty", args: []*clientmodel.MetricFamily{nil, nil, nil}, want: []*clientmodel.MetricFamily{}}, + {name: "begin", args: []*clientmodel.MetricFamily{nil, a, b}, want: []*clientmodel.MetricFamily{a, b}}, + {name: "middle", args: []*clientmodel.MetricFamily{a, nil, b}, want: []*clientmodel.MetricFamily{a, b}}, + {name: "end", args: []*clientmodel.MetricFamily{a, b, nil}, want: []*clientmodel.MetricFamily{a, b}}, + {name: "skip", args: []*clientmodel.MetricFamily{a, nil, b, nil, c}, want: []*clientmodel.MetricFamily{a, b, c}}, + {name: "removes empty", args: []*clientmodel.MetricFamily{d, d}, want: []*clientmodel.MetricFamily{}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Pack(tt.args); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Pack() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPackMetrics(t *testing.T) { + tests := []struct { + name string + args *clientmodel.MetricFamily + want *clientmodel.MetricFamily + wantOk bool + wantErr bool + }{ + {name: "empty", args: &clientmodel.MetricFamily{}, want: &clientmodel.MetricFamily{}}, + { + name: "all nil", + args: &clientmodel.MetricFamily{Metric: []*clientmodel.Metric{nil, nil}}, + want: &clientmodel.MetricFamily{Metric: []*clientmodel.Metric{}}, + }, + { + name: "leading nil", + args: &clientmodel.MetricFamily{Metric: []*clientmodel.Metric{nil, metric(1)}}, + want: &clientmodel.MetricFamily{Metric: []*clientmodel.Metric{metric(1)}}, + wantOk: true, + }, + { + name: "trailing nil", + args: &clientmodel.MetricFamily{Metric: []*clientmodel.Metric{metric(1), nil}}, + want: &clientmodel.MetricFamily{Metric: []*clientmodel.Metric{metric(1)}}, + wantOk: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, gotErr := PackMetrics(tt.args) + if got != tt.wantOk { + t.Errorf("PackMetrics() = %t, want %t", got, tt.wantOk) + } + if (gotErr != nil) != tt.wantErr { + t.Errorf("PackMetrics() = %v, want %t", gotErr, tt.wantErr) + } + if !reflect.DeepEqual(tt.args, tt.want) { + t.Errorf("PackMetrics() = %v, want %v", tt.args, tt.want) + } + }) + } +} + +func TestMergeSort(t *testing.T) { + tests := []struct { + name string + args []*clientmodel.MetricFamily + want []*clientmodel.MetricFamily + }{ + {name: "empty", args: []*clientmodel.MetricFamily{}, want: []*clientmodel.MetricFamily{}}, + {name: "single", args: []*clientmodel.MetricFamily{family("A", 1)}, want: []*clientmodel.MetricFamily{family("A", 1)}}, + {name: "merge", args: []*clientmodel.MetricFamily{family("A", 1), family("A", 2)}, want: []*clientmodel.MetricFamily{family("A", 1, 2)}}, + {name: "reverse merge", args: []*clientmodel.MetricFamily{family("A", 2), family("A", 1)}, want: []*clientmodel.MetricFamily{family("A", 1, 2)}}, + {name: "differ", args: []*clientmodel.MetricFamily{family("A", 2), family("B", 1)}, want: []*clientmodel.MetricFamily{family("A", 2), family("B", 1)}}, + {name: "zip merge", args: []*clientmodel.MetricFamily{family("A", 2, 4, 6), family("A", 1, 3, 5)}, want: []*clientmodel.MetricFamily{family("A", 1, 2, 3, 4, 5, 6)}}, + {name: "zip merge - dst longer", args: []*clientmodel.MetricFamily{family("A", 2, 4, 6), family("A", 3)}, want: []*clientmodel.MetricFamily{family("A", 2, 3, 4, 6)}}, + {name: "zip merge - src longer", args: []*clientmodel.MetricFamily{family("A", 4), family("A", 1, 3, 5)}, want: []*clientmodel.MetricFamily{family("A", 1, 3, 4, 5)}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := MergeSortedWithTimestamps(tt.args); !reflect.DeepEqual(got, tt.want) { + t.Errorf("MergeSortedWithTimestamps() = %v, want %v", got, tt.want) + } + }) + } + +} diff --git a/pkg/metricfamily/unsorted.go b/pkg/metricfamily/unsorted.go new file mode 100644 index 00000000..4a2bf1ce --- /dev/null +++ b/pkg/metricfamily/unsorted.go @@ -0,0 +1,44 @@ +package metricfamily + +import ( + "fmt" + + clientmodel "github.com/prometheus/client_model/go" +) + +var ( + ErrUnsorted = fmt.Errorf("metrics in provided family are not in increasing timestamp order") + ErrNoTimestamp = fmt.Errorf("metrics in provided family do not have a timestamp") + ErrTimestampTooOld = fmt.Errorf("metrics in provided family have a timestamp that is too old, check clock skew") +) + +type errorOnUnsorted struct { + timestamp int64 + require bool +} + +func NewErrorOnUnsorted(requireTimestamp bool) Transformer { + return &errorOnUnsorted{ + require: requireTimestamp, + } +} + +func (t *errorOnUnsorted) Transform(family *clientmodel.MetricFamily) (bool, error) { + t.timestamp = 0 + for _, m := range family.Metric { + if m == nil { + continue + } + var ts int64 + if m.TimestampMs != nil { + ts = *m.TimestampMs + } else if t.require { + return false, ErrNoTimestamp + } + if ts < t.timestamp { + return false, ErrUnsorted + } + t.timestamp = ts + } + return true, nil +} diff --git a/pkg/metricfamily/whitelist.go b/pkg/metricfamily/whitelist.go new file mode 100644 index 00000000..3cd9ed42 --- /dev/null +++ b/pkg/metricfamily/whitelist.go @@ -0,0 +1,63 @@ +package metricfamily + +import ( + clientmodel "github.com/prometheus/client_model/go" + "github.com/prometheus/prometheus/pkg/labels" + "github.com/prometheus/prometheus/promql" +) + +type whitelist [][]*labels.Matcher + +// NewWhitelist returns a Transformer that checks if at least one +// rule in the whitelist is true. +// This Transformer will nil metrics within a metric family that do not match a rule. +// Each given rule is transformed into a matchset. Matchsets are OR-ed. +// Individual matchers within a matchset are AND-ed, as in PromQL. +func NewWhitelist(rules []string) (Transformer, error) { + var ms [][]*labels.Matcher + for i := range rules { + matchers, err := promql.ParseMetricSelector(rules[i]) + if err != nil { + return nil, err + } + ms = append(ms, matchers) + } + return whitelist(ms), nil +} + +// Transform implements the Transformer interface. +func (t whitelist) Transform(family *clientmodel.MetricFamily) (bool, error) { + var ok bool +Metric: + for i, m := range family.Metric { + if m == nil { + continue + } + for _, matchset := range t { + if match(family.GetName(), m, matchset...) { + ok = true + continue Metric + } + } + family.Metric[i] = nil + } + return ok, nil +} + +// match checks whether every Matcher matches a given metric. +func match(name string, metric *clientmodel.Metric, matchers ...*labels.Matcher) bool { +Matcher: + for _, m := range matchers { + if m.Name == "__name__" && m.Matches(name) { + continue + } + for _, label := range metric.Label { + if label == nil || m.Name != label.GetName() || !m.Matches(label.GetValue()) { + continue + } + continue Matcher + } + return false + } + return true +} diff --git a/pkg/metricfamily/whitelist_test.go b/pkg/metricfamily/whitelist_test.go new file mode 100644 index 00000000..831c0d8a --- /dev/null +++ b/pkg/metricfamily/whitelist_test.go @@ -0,0 +1,206 @@ +package metricfamily + +import ( + "fmt" + "reflect" + "testing" + + clientmodel "github.com/prometheus/client_model/go" +) + +func familyWithLabels(name string, labels ...[]*clientmodel.LabelPair) *clientmodel.MetricFamily { + family := &clientmodel.MetricFamily{Name: &name} + time := int64(0) + for i := range labels { + family.Metric = append(family.Metric, &clientmodel.Metric{TimestampMs: &time, Label: labels[i]}) + } + return family +} + +func copyMetric(family *clientmodel.MetricFamily) *clientmodel.MetricFamily { + metric := make([]*clientmodel.Metric, len(family.Metric)) + copy(metric, family.Metric) + f := *family + f.Metric = metric + return &f +} + +func setNilMetric(family *clientmodel.MetricFamily, positions ...int) *clientmodel.MetricFamily { + f := copyMetric(family) + for _, position := range positions { + f.Metric[position] = nil + } + return f +} + +func TestWhitelist(t *testing.T) { + type checkFunc func(family *clientmodel.MetricFamily, ok bool, err error) error + + isOK := func(want bool) checkFunc { + return func(_ *clientmodel.MetricFamily, got bool, _ error) error { + if want != got { + return fmt.Errorf("want ok %t, got %t", want, got) + } + return nil + } + } + + hasErr := func(want error) checkFunc { + return func(_ *clientmodel.MetricFamily, _ bool, got error) error { + if want != got { + return fmt.Errorf("want err %v, got %v", want, got) + } + return nil + } + } + + deepEqual := func(want *clientmodel.MetricFamily) checkFunc { + return func(got *clientmodel.MetricFamily, _ bool, _ error) error { + if !reflect.DeepEqual(want, got) { + return fmt.Errorf("want metricfamily %v, got %v", want, got) + } + return nil + } + } + + strPnt := func(str string) *string { + return &str + } + + a := familyWithLabels("A", []*clientmodel.LabelPair{ + &clientmodel.LabelPair{ + Name: strPnt("method"), + Value: strPnt("POST"), + }, + }) + + b := familyWithLabels("B", []*clientmodel.LabelPair{ + &clientmodel.LabelPair{ + Name: strPnt("method"), + Value: strPnt("GET"), + }, + }) + + c := familyWithLabels("C", + []*clientmodel.LabelPair{ + &clientmodel.LabelPair{ + Name: strPnt("method"), + Value: strPnt("POST"), + }, + &clientmodel.LabelPair{ + Name: strPnt("status"), + Value: strPnt("200"), + }, + }, + []*clientmodel.LabelPair{ + &clientmodel.LabelPair{ + Name: strPnt("method"), + Value: strPnt("GET"), + }, + &clientmodel.LabelPair{ + Name: strPnt("status"), + Value: strPnt("200"), + }, + }, + []*clientmodel.LabelPair{ + &clientmodel.LabelPair{ + Name: strPnt("method"), + Value: strPnt("POST"), + }, + &clientmodel.LabelPair{ + Name: strPnt("status"), + Value: strPnt("500"), + }, + }, + []*clientmodel.LabelPair{ + &clientmodel.LabelPair{ + Name: strPnt("method"), + Value: strPnt("DELETE"), + }, + &clientmodel.LabelPair{ + Name: strPnt("status"), + Value: strPnt("200"), + }, + }, + ) + + for _, tc := range []struct { + name string + checks []checkFunc + family *clientmodel.MetricFamily + whitelister Transformer + }{ + { + name: "accept A", + family: a, + checks: []checkFunc{isOK(true), hasErr(nil), deepEqual(a)}, + whitelister: mustMakeWhitelist(t, []string{"{__name__=\"A\"}"}), + }, + { + name: "reject B", + family: b, + checks: []checkFunc{isOK(false), hasErr(nil), deepEqual(setNilMetric(b, 0))}, + whitelister: mustMakeWhitelist(t, []string{"{__name__=\"A\"}"}), + }, + { + name: "accept C", + family: c, + checks: []checkFunc{isOK(true), hasErr(nil), deepEqual(c)}, + whitelister: mustMakeWhitelist(t, []string{"{__name__=\"C\"}"}), + }, + { + name: "reject C", + family: c, + checks: []checkFunc{isOK(false), hasErr(nil), deepEqual(setNilMetric(c, 0, 1, 2, 3))}, + whitelister: mustMakeWhitelist(t, []string{"{method=\"PUT\"}"}), + }, + { + name: "reject parts of C", + family: c, + checks: []checkFunc{isOK(true), hasErr(nil), deepEqual(setNilMetric(c, 0, 2, 3))}, + whitelister: mustMakeWhitelist(t, []string{"{__name__=\"C\",method=\"GET\"}"}), + }, + { + name: "reject different parts of C", + family: c, + checks: []checkFunc{isOK(true), hasErr(nil), deepEqual(setNilMetric(c, 2))}, + whitelister: mustMakeWhitelist(t, []string{"{status=\"200\"}"}), + }, + { + name: "multiple rules", + family: c, + checks: []checkFunc{isOK(true), hasErr(nil), deepEqual(setNilMetric(c, 0, 3))}, + whitelister: mustMakeWhitelist(t, []string{"{method=\"GET\"}", "{status=\"500\"}"}), + }, + { + name: "multiple rules complex", + family: c, + checks: []checkFunc{isOK(true), hasErr(nil), deepEqual(setNilMetric(c, 0, 1, 3))}, + whitelister: mustMakeWhitelist(t, []string{"{method=\"GET\",status=\"400\"}", "{status=\"500\"}"}), + }, + { + name: "multiple rules complex with rejection", + family: c, + checks: []checkFunc{isOK(true), hasErr(nil), deepEqual(setNilMetric(c, 1, 2))}, + whitelister: mustMakeWhitelist(t, []string{"{method=\"POST\",status=\"200\"}", "{method=\"DELETE\"}"}), + }, + } { + t.Run(tc.name, func(t *testing.T) { + f := copyMetric(tc.family) + ok, err := tc.whitelister.Transform(f) + for _, check := range tc.checks { + if err := check(f, ok, err); err != nil { + t.Error(err) + } + } + }) + } +} + +func mustMakeWhitelist(t *testing.T, rules []string) Transformer { + w, err := NewWhitelist(rules) + if err != nil { + t.Fatalf("failed to create new whitelist transformer: %v", err) + } + return w +} diff --git a/pkg/metricsclient/metricsclient.go b/pkg/metricsclient/metricsclient.go new file mode 100644 index 00000000..4289fb66 --- /dev/null +++ b/pkg/metricsclient/metricsclient.go @@ -0,0 +1,400 @@ +package metricsclient + +import ( + "bytes" + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "strconv" + "strings" + "time" + + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/gogo/protobuf/proto" + "github.com/golang/snappy" + "github.com/prometheus/client_golang/prometheus" + clientmodel "github.com/prometheus/client_model/go" + "github.com/prometheus/common/expfmt" + "github.com/prometheus/prometheus/prompb" + + "github.com/openshift/telemeter/pkg/reader" +) + +const ( + nameLabelName = "__name__" +) + +var ( + gaugeRequestRetrieve = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "metricsclient_request_retrieve", + Help: "Tracks the number of metrics retrievals", + }, []string{"client", "status_code"}) + gaugeRequestSend = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "metricsclient_request_send", + Help: "Tracks the number of metrics sends", + }, []string{"client", "status_code"}) +) + +func init() { + prometheus.MustRegister( + gaugeRequestRetrieve, gaugeRequestSend, + ) +} + +type Client struct { + client *http.Client + maxBytes int64 + timeout time.Duration + metricsName string + logger log.Logger +} + +type PartitionedMetrics struct { + ClusterID string + Families []*clientmodel.MetricFamily +} + +func New(logger log.Logger, client *http.Client, maxBytes int64, timeout time.Duration, metricsName string) *Client { + return &Client{ + client: client, + maxBytes: maxBytes, + timeout: timeout, + metricsName: metricsName, + logger: log.With(logger, "component", "metricsclient"), + } +} + +func (c *Client) Retrieve(ctx context.Context, req *http.Request) ([]*clientmodel.MetricFamily, error) { + if req.Header == nil { + req.Header = make(http.Header) + } + req.Header.Set("Accept", strings.Join([]string{string(expfmt.FmtProtoDelim), string(expfmt.FmtText)}, " , ")) + + ctx, cancel := context.WithTimeout(ctx, c.timeout) + req = req.WithContext(ctx) + defer cancel() + + families := make([]*clientmodel.MetricFamily, 0, 100) + err := withCancel(ctx, c.client, req, func(resp *http.Response) error { + switch resp.StatusCode { + case http.StatusOK: + gaugeRequestRetrieve.WithLabelValues(c.metricsName, "200").Inc() + case http.StatusUnauthorized: + gaugeRequestRetrieve.WithLabelValues(c.metricsName, "401").Inc() + return fmt.Errorf("Prometheus server requires authentication: %s", resp.Request.URL) + case http.StatusForbidden: + gaugeRequestRetrieve.WithLabelValues(c.metricsName, "403").Inc() + return fmt.Errorf("Prometheus server forbidden: %s", resp.Request.URL) + case http.StatusBadRequest: + gaugeRequestRetrieve.WithLabelValues(c.metricsName, "400").Inc() + return fmt.Errorf("bad request: %s", resp.Request.URL) + default: + gaugeRequestRetrieve.WithLabelValues(c.metricsName, strconv.Itoa(resp.StatusCode)).Inc() + return fmt.Errorf("Prometheus server reported unexpected error code: %d", resp.StatusCode) + } + + // read the response into memory + format := expfmt.ResponseFormat(resp.Header) + r := &reader.LimitedReader{R: resp.Body, N: c.maxBytes} + decoder := expfmt.NewDecoder(r, format) + for { + family := &clientmodel.MetricFamily{} + families = append(families, family) + if err := decoder.Decode(family); err != nil { + if err == io.EOF { + break + } + return err + } + } + + return nil + }) + if err != nil { + return nil, err + } + return families, nil +} + +func (c *Client) Send(ctx context.Context, req *http.Request, families []*clientmodel.MetricFamily) error { + buf := &bytes.Buffer{} + if err := Write(buf, families); err != nil { + return err + } + + if req.Header == nil { + req.Header = make(http.Header) + } + req.Header.Set("Content-Type", string(expfmt.FmtProtoDelim)) + req.Header.Set("Content-Encoding", "snappy") + req.Body = ioutil.NopCloser(buf) + + ctx, cancel := context.WithTimeout(ctx, c.timeout) + req = req.WithContext(ctx) + defer cancel() + level.Debug(c.logger).Log("msg", "start to send") + return withCancel(ctx, c.client, req, func(resp *http.Response) error { + defer func() { + if _, err := io.Copy(ioutil.Discard, resp.Body); err != nil { + level.Error(c.logger).Log("msg", "error copying body", "err", err) + } + resp.Body.Close() + }() + level.Debug(c.logger).Log("msg", resp.StatusCode) + switch resp.StatusCode { + case http.StatusOK: + gaugeRequestSend.WithLabelValues(c.metricsName, "200").Inc() + case http.StatusUnauthorized: + gaugeRequestSend.WithLabelValues(c.metricsName, "401").Inc() + return fmt.Errorf("gateway server requires authentication: %s", resp.Request.URL) + case http.StatusForbidden: + gaugeRequestSend.WithLabelValues(c.metricsName, "403").Inc() + return fmt.Errorf("gateway server forbidden: %s", resp.Request.URL) + case http.StatusBadRequest: + gaugeRequestSend.WithLabelValues(c.metricsName, "400").Inc() + level.Debug(c.logger).Log("msg", resp.Body) + return fmt.Errorf("gateway server bad request: %s", resp.Request.URL) + default: + gaugeRequestSend.WithLabelValues(c.metricsName, strconv.Itoa(resp.StatusCode)).Inc() + body, _ := ioutil.ReadAll(resp.Body) + if len(body) > 1024 { + body = body[:1024] + } + return fmt.Errorf("gateway server reported unexpected error code: %d: %s", resp.StatusCode, string(body)) + } + + return nil + }) +} + +func Read(r io.Reader) ([]*clientmodel.MetricFamily, error) { + decompress := snappy.NewReader(r) + decoder := expfmt.NewDecoder(decompress, expfmt.FmtProtoDelim) + families := make([]*clientmodel.MetricFamily, 0, 100) + for { + family := &clientmodel.MetricFamily{} + if err := decoder.Decode(family); err != nil { + if err == io.EOF { + break + } + return nil, err + } + families = append(families, family) + } + return families, nil +} + +func Write(w io.Writer, families []*clientmodel.MetricFamily) error { + // output the filtered set + compress := snappy.NewBufferedWriter(w) + encoder := expfmt.NewEncoder(compress, expfmt.FmtProtoDelim) + for _, family := range families { + if family == nil { + continue + } + if err := encoder.Encode(family); err != nil { + return err + } + } + if err := compress.Flush(); err != nil { + return err + } + return nil +} + +func withCancel(ctx context.Context, client *http.Client, req *http.Request, fn func(*http.Response) error) error { + resp, err := client.Do(req) + defer func() { + if resp != nil { + resp.Body.Close() + } + }() + if err != nil { + return err + } + + done := make(chan struct{}) + go func() { + err = fn(resp) + close(done) + }() + + select { + case <-ctx.Done(): + closeErr := resp.Body.Close() + + // wait for the goroutine to finish. + <-done + + // err is propagated from the goroutine above + // if it is nil, we bubble up the close err, if any. + if err == nil { + err = closeErr + } + + // if there is no close err, + // we propagate the context context error. + if err == nil { + err = ctx.Err() + } + case <-done: + // propagate the err from the spawned goroutine, if any. + } + + return err +} + +func DefaultTransport(logger log.Logger, isTLS bool) *http.Transport { + // Load client cert + cert, err := tls.LoadX509KeyPair("/etc/certs/client.pem", "/etc/certs/client.key") + if err != nil { + level.Error(logger).Log("msg", "failed to load certs", err) + return nil + } + level.Info(logger).Log("msg", "certs loaded") + // Load CA cert + caCert, err := ioutil.ReadFile("/etc/certs/ca.pem") + if err != nil { + level.Error(logger).Log("msg", "failed to load ca", err) + return nil + } + level.Info(logger).Log("msg", "ca loaded") + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) + // Setup HTTPS client + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: caCertPool, + } + tlsConfig.BuildNameToCertificate() + + if isTLS { + return &http.Transport{ + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, + DisableKeepAlives: true, + TLSClientConfig: tlsConfig, + } + } else { + return &http.Transport{ + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, + DisableKeepAlives: true, + } + } +} +func convertToTimeseries(p *PartitionedMetrics, now time.Time) ([]prompb.TimeSeries, error) { + var timeseries []prompb.TimeSeries + + for _, f := range p.Families { + for _, m := range f.Metric { + var ts prompb.TimeSeries + + labelpairs := []prompb.Label{{ + Name: nameLabelName, + Value: *f.Name, + }} + + for _, l := range m.Label { + labelpairs = append(labelpairs, prompb.Label{ + Name: *l.Name, + Value: *l.Value, + }) + } + + s := prompb.Sample{ + Timestamp: *m.TimestampMs, + } + + switch *f.Type { + case clientmodel.MetricType_COUNTER: + s.Value = *m.Counter.Value + case clientmodel.MetricType_GAUGE: + s.Value = *m.Gauge.Value + case clientmodel.MetricType_UNTYPED: + s.Value = *m.Untyped.Value + default: + return nil, fmt.Errorf("metric type %s not supported", f.Type.String()) + } + + ts.Labels = append(ts.Labels, labelpairs...) + ts.Samples = append(ts.Samples, s) + + timeseries = append(timeseries, ts) + } + } + + return timeseries, nil +} + +type clusterIDCtxType int + +const ( + clusterIDCtx clusterIDCtxType = iota +) + +func (c *Client) RemoteWrite(ctx context.Context, req *http.Request, families []*clientmodel.MetricFamily) error { + clusterID := "1234567890" //ctx.Value(clusterIDCtx).(string) + timeseries, err := convertToTimeseries(&PartitionedMetrics{ClusterID: clusterID, Families: families}, time.Now()) + if err != nil { + msg := "failed to convert timeseries" + level.Warn(c.logger).Log("msg", msg, "err", err) + return fmt.Errorf(msg) + } + + if len(timeseries) == 0 { + level.Info(c.logger).Log("msg", "no time series to forward to receive endpoint") + return nil + } + + wreq := &prompb.WriteRequest{Timeseries: timeseries} + + data, err := proto.Marshal(wreq) + if err != nil { + msg := "failed to marshal proto" + level.Warn(c.logger).Log("msg", msg, "err", err) + return fmt.Errorf(msg) + } + + compressed := snappy.Encode(nil, data) + + req1, err := http.NewRequest(http.MethodPost, "https://test-open-cluster-management-monitoring.apps.marco.dev05.red-chesterfield.com/api/metrics/v1/test/api/v1/receive", bytes.NewBuffer(compressed)) + if err != nil { + msg := "failed to create forwarding request" + level.Warn(c.logger).Log("msg", msg, "err", err) + return fmt.Errorf(msg) + } + //req.Header.Add("THANOS-TENANT", tenantID) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + req1 = req1.WithContext(ctx) + + resp, err := c.client.Do(req1) + if err != nil { + msg := "failed to forward request" + level.Warn(c.logger).Log("msg", msg, "err", err) + return fmt.Errorf(msg) + } + + if resp.StatusCode/100 != 2 { + // surfacing upstreams error to our users too + msg := fmt.Sprintf("response status code is %s", resp.Status) + level.Warn(c.logger).Log("msg", msg) + return fmt.Errorf(msg) + } + return nil +} diff --git a/pkg/reader/reader.go b/pkg/reader/reader.go new file mode 100644 index 00000000..f7f8c558 --- /dev/null +++ b/pkg/reader/reader.go @@ -0,0 +1,50 @@ +package reader + +import ( + "fmt" + "io" +) + +type limitReadCloser struct { + io.Reader + closer io.ReadCloser +} + +func NewLimitReadCloser(r io.ReadCloser, n int64) io.ReadCloser { + return limitReadCloser{ + Reader: LimitReader(r, n), + closer: r, + } +} + +func (c limitReadCloser) Close() error { + return c.closer.Close() +} + +var ErrTooLong = fmt.Errorf("the incoming sample data is too long") + +// LimitReader returns a Reader that reads from r +// but stops with ErrTooLong after n bytes. +// The underlying implementation is a *LimitedReader. +func LimitReader(r io.Reader, n int64) io.Reader { return &LimitedReader{r, n} } + +// A LimitedReader reads from R but limits the amount of +// data returned to just N bytes. Each call to Read +// updates N to reflect the new amount remaining. +// Read returns ErrTooLong when N <= 0 or when the underlying R returns EOF. +type LimitedReader struct { + R io.Reader // underlying reader + N int64 // max bytes remaining +} + +func (l *LimitedReader) Read(p []byte) (n int, err error) { + if l.N <= 0 { + return 0, ErrTooLong + } + if int64(len(p)) > l.N { + p = p[0:l.N] + } + n, err = l.R.Read(p) + l.N -= int64(n) + return +} diff --git a/pkg/receive/handler.go b/pkg/receive/handler.go new file mode 100644 index 00000000..54f77c39 --- /dev/null +++ b/pkg/receive/handler.go @@ -0,0 +1,188 @@ +package receive + +import ( + "bytes" + "context" + "fmt" + "io/ioutil" + "net/http" + "time" + + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/gogo/protobuf/proto" + "github.com/golang/snappy" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/prometheus/prompb" +) + +const forwardTimeout = 5 * time.Second + +// DefaultRequestLimit is the size limit of a request body coming in +const DefaultRequestLimit = 15 * 1024 // based on historic Prometheus data with 6KB at most + +// ClusterAuthorizer authorizes a cluster by its token and id, returning a subject or error +type ClusterAuthorizer interface { + AuthorizeCluster(token, cluster string) (subject string, err error) +} + +// Handler knows the forwardURL for all requests +type Handler struct { + ForwardURL string + tenantID string + client *http.Client + logger log.Logger + + // Metrics. + forwardRequestsTotal *prometheus.CounterVec +} + +// NewHandler returns a new Handler with a http client +func NewHandler(logger log.Logger, forwardURL string, reg prometheus.Registerer, tenantID string) *Handler { + h := &Handler{ + ForwardURL: forwardURL, + tenantID: tenantID, + client: &http.Client{ + Timeout: forwardTimeout, + }, + logger: log.With(logger, "component", "receive/handler"), + forwardRequestsTotal: prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "telemeter_forward_requests_total", + Help: "The number of forwarded remote-write requests.", + }, []string{"result"}, + ), + } + + if reg != nil { + reg.MustRegister(h.forwardRequestsTotal) + } + + return h +} + +// Receive a remote-write request after it has been authenticated and forward it to Thanos +func (h *Handler) Receive(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + defer r.Body.Close() + + ctx, cancel := context.WithTimeout(r.Context(), forwardTimeout) + defer cancel() + + req, err := http.NewRequest(http.MethodPost, h.ForwardURL, r.Body) + if err != nil { + level.Error(h.logger).Log("msg", "failed to create forward request", "err", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + req = req.WithContext(ctx) + req.Header.Add("THANOS-TENANT", h.tenantID) + + resp, err := h.client.Do(req) + if err != nil { + h.forwardRequestsTotal.WithLabelValues("error").Inc() + level.Error(h.logger).Log("msg", "failed to forward request", "err", err) + http.Error(w, err.Error(), http.StatusBadGateway) + return + } + + if resp.StatusCode/100 != 2 { + msg := "upstream response status is not 200 OK" + h.forwardRequestsTotal.WithLabelValues("error").Inc() + level.Error(h.logger).Log("msg", msg, "statuscode", resp.Status) + http.Error(w, msg, resp.StatusCode) + return + } + h.forwardRequestsTotal.WithLabelValues("success").Inc() + w.WriteHeader(resp.StatusCode) +} + +// LimitBodySize is a middleware that check that the request body is not bigger than the limit +func LimitBodySize(limit int64, next http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer r.Body.Close() + + // Set body to this buffer for other handlers to read + r.Body = ioutil.NopCloser(bytes.NewBuffer(body)) + + if len(body) >= int(limit) { + http.Error(w, "request too big", http.StatusRequestEntityTooLarge) + return + } + + next.ServeHTTP(w, r) + } +} + +// ErrRequiredLabelMissing is returned if a required label is missing from a metric +var ErrRequiredLabelMissing = fmt.Errorf("a required label is missing from the metric") + +// ValidateLabels by checking each enforced label to be present in every time series +func ValidateLabels(logger log.Logger, next http.Handler, labels ...string) http.HandlerFunc { + logger = log.With(logger, "component", "receive", "middleware", "validateLabels") + + labelmap := make(map[string]struct{}) + for _, label := range labels { + labelmap[label] = struct{}{} + } + + return func(w http.ResponseWriter, r *http.Request) { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + level.Error(logger).Log("msg", "failed to read body", "err", err) + http.Error(w, "failed to read body", http.StatusInternalServerError) + return + } + defer r.Body.Close() + + // Set body to this buffer for other handlers to read + r.Body = ioutil.NopCloser(bytes.NewBuffer(body)) + + content, err := snappy.Decode(nil, body) + if err != nil { + level.Warn(logger).Log("msg", "failed to decode request body", "err", err) + http.Error(w, "failed to decode request body", http.StatusBadRequest) + return + } + + var wreq prompb.WriteRequest + if err := proto.Unmarshal(content, &wreq); err != nil { + level.Warn(logger).Log("msg", "failed to decode protobuf from body", "err", err) + http.Error(w, "failed to decode protobuf from body", http.StatusBadRequest) + return + } + + for _, ts := range wreq.GetTimeseries() { + // exit early if not enough labels anyway + if len(ts.GetLabels()) < len(labels) { + level.Warn(logger).Log("msg", "request is missing required labels", "err", ErrRequiredLabelMissing) + http.Error(w, ErrRequiredLabelMissing.Error(), http.StatusBadRequest) + return + } + + found := 0 + + for _, l := range ts.GetLabels() { + if _, ok := labelmap[l.GetName()]; ok { + found++ + } + } + + if len(labels) != found { + level.Warn(logger).Log("msg", "request is missing required labels", "err", ErrRequiredLabelMissing) + http.Error(w, ErrRequiredLabelMissing.Error(), http.StatusBadRequest) + return + } + } + + next.ServeHTTP(w, r) + } +} diff --git a/pkg/server/forward.go b/pkg/server/forward.go new file mode 100644 index 00000000..8afdc340 --- /dev/null +++ b/pkg/server/forward.go @@ -0,0 +1,226 @@ +package server + +import ( + "bytes" + "context" + "fmt" + "io" + "math" + "net/http" + "net/url" + "time" + + "github.com/go-chi/chi/middleware" + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/gogo/protobuf/proto" + "github.com/golang/snappy" + "github.com/prometheus/client_golang/prometheus" + clientmodel "github.com/prometheus/client_model/go" + "github.com/prometheus/common/expfmt" + "github.com/prometheus/prometheus/prompb" + + "github.com/openshift/telemeter/pkg/metricfamily" +) + +const ( + nameLabelName = "__name__" +) + +var ( + forwardSamples = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "telemeter_v1_forward_samples_total", + Help: "Total amount of successfully forwarded samples from v1 requests.", + }) + forwardRequests = prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "telemeter_v1_forward_requests_total", + Help: "Total amount of forwarded v1 requests.", + }, []string{"result"}) + forwardDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Name: "telemeter_v1_forward_request_duration_seconds", + Help: "Tracks the duration of all requests forwarded v1.", + Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5}, // max = timeout + }, []string{"status_code"}) + overwrittenTimestamps = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "telemeter_v1_forward_overwritten_timestamps_total", + Help: "Total number of timestamps from v1 requests that were overwritten.", + }) +) + +func init() { + prometheus.MustRegister(forwardSamples) + prometheus.MustRegister(forwardRequests) + prometheus.MustRegister(forwardDuration) + prometheus.MustRegister(overwrittenTimestamps) +} + +// ForwardHandler gets a request containing metric families and +// converts it to a remote write request forwarding it to the upstream at fowardURL. +func ForwardHandler(logger log.Logger, forwardURL *url.URL, tenantID string, client *http.Client) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + rlogger := log.With(logger, "request", middleware.GetReqID(r.Context())) + + clusterID, ok := ClusterIDFromContext(r.Context()) + if !ok { + msg := "failed to retrieve clusterID" + level.Warn(rlogger).Log("msg", msg) + http.Error(w, msg, http.StatusInternalServerError) + return + } + + decoder := expfmt.NewDecoder(r.Body, expfmt.ResponseFormat(r.Header)) + defer r.Body.Close() + + families := make([]*clientmodel.MetricFamily, 0, 100) + for { + family := &clientmodel.MetricFamily{} + if err := decoder.Decode(family); err != nil { + if err == io.EOF { + break + } + msg := err.Error() + level.Warn(rlogger).Log("msg", msg, "err", err) + http.Error(w, msg, http.StatusInternalServerError) + return + } + + families = append(families, family) + } + families = metricfamily.Pack(families) + + timeseries, err := convertToTimeseries(&PartitionedMetrics{ClusterID: clusterID, Families: families}, time.Now()) + if err != nil { + msg := "failed to convert timeseries" + level.Warn(rlogger).Log("msg", msg, "err", err) + http.Error(w, msg, http.StatusInternalServerError) + return + } + + if len(timeseries) == 0 { + level.Info(rlogger).Log("msg", "no time series to forward to receive endpoint") + return + } + + wreq := &prompb.WriteRequest{Timeseries: timeseries} + + data, err := proto.Marshal(wreq) + if err != nil { + msg := "failed to marshal proto" + level.Warn(rlogger).Log("msg", msg, "err", err) + http.Error(w, msg, http.StatusInternalServerError) + return + } + + compressed := snappy.Encode(nil, data) + + req, err := http.NewRequest(http.MethodPost, forwardURL.String(), bytes.NewBuffer(compressed)) + if err != nil { + msg := "failed to create forwarding request" + level.Warn(rlogger).Log("msg", msg, "err", err) + http.Error(w, msg, http.StatusInternalServerError) + return + } + req.Header.Add("THANOS-TENANT", tenantID) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + req = req.WithContext(ctx) + + begin := time.Now() + resp, err := client.Do(req) + if err != nil { + msg := "failed to forward request" + level.Warn(rlogger).Log("msg", msg, "err", err) + http.Error(w, msg, http.StatusBadGateway) + return + } + + forwardDuration. + WithLabelValues(fmt.Sprintf("%d", resp.StatusCode)). + Observe(time.Since(begin).Seconds()) + + meanDrift := timeseriesMeanDrift(timeseries, time.Now().Unix()) + if math.Abs(meanDrift) > 10 { + level.Info(rlogger).Log("msg", "mean drift from now for clusters", "clusterID", clusterID, "drift", fmt.Sprintf("%.3fs", meanDrift)) + } + + if resp.StatusCode/100 != 2 { + // surfacing upstreams error to our users too + msg := fmt.Sprintf("response status code is %s", resp.Status) + level.Warn(rlogger).Log("msg", msg) + http.Error(w, msg, resp.StatusCode) + return + } + + s := 0 + for _, ts := range wreq.Timeseries { + s = s + len(ts.Samples) + } + forwardSamples.Add(float64(s)) + } +} + +func convertToTimeseries(p *PartitionedMetrics, now time.Time) ([]prompb.TimeSeries, error) { + var timeseries []prompb.TimeSeries + + timestamp := now.UnixNano() / int64(time.Millisecond) + for _, f := range p.Families { + for _, m := range f.Metric { + var ts prompb.TimeSeries + + labelpairs := []prompb.Label{{ + Name: nameLabelName, + Value: *f.Name, + }} + + for _, l := range m.Label { + labelpairs = append(labelpairs, prompb.Label{ + Name: *l.Name, + Value: *l.Value, + }) + } + + s := prompb.Sample{ + Timestamp: *m.TimestampMs, + } + // If the sample is in the future, overwrite it. + if *m.TimestampMs > timestamp { + s.Timestamp = timestamp + overwrittenTimestamps.Inc() + } + + switch *f.Type { + case clientmodel.MetricType_COUNTER: + s.Value = *m.Counter.Value + case clientmodel.MetricType_GAUGE: + s.Value = *m.Gauge.Value + case clientmodel.MetricType_UNTYPED: + s.Value = *m.Untyped.Value + default: + return nil, fmt.Errorf("metric type %s not supported", f.Type.String()) + } + + ts.Labels = append(ts.Labels, labelpairs...) + ts.Samples = append(ts.Samples, s) + + timeseries = append(timeseries, ts) + } + } + + return timeseries, nil +} + +func timeseriesMeanDrift(ts []prompb.TimeSeries, timestampSeconds int64) float64 { + var count float64 + var sum float64 + + for _, t := range ts { + for _, s := range t.Samples { + sum = sum + (float64(timestampSeconds) - float64(s.Timestamp/1000)) + count++ + } + } + + return sum / count +} diff --git a/pkg/server/forward_test.go b/pkg/server/forward_test.go new file mode 100644 index 00000000..71cfafca --- /dev/null +++ b/pkg/server/forward_test.go @@ -0,0 +1,215 @@ +package server + +import ( + "fmt" + "testing" + "time" + + clientmodel "github.com/prometheus/client_model/go" + "github.com/prometheus/prometheus/prompb" +) + +func Test_convertToTimeseries(t *testing.T) { + counter := clientmodel.MetricType_COUNTER + untyped := clientmodel.MetricType_UNTYPED + gauge := clientmodel.MetricType_GAUGE + + fooMetricName := "foo_metric" + fooHelp := "foo help text" + fooLabelName := "foo" + fooLabelValue1 := "bar" + fooLabelValue2 := "baz" + + barMetricName := "bar_metric" + barHelp := "bar help text" + barLabelName := "bar" + barLabelValue1 := "baz" + + value42 := 42.0 + value50 := 50.0 + timestamp := int64(15615582020000) + now := time.Now() + nowTimestamp := now.UnixNano() / int64(time.Millisecond) + + tests := []struct { + name string + in *PartitionedMetrics + want []prompb.TimeSeries + }{{ + name: "counter", + in: &PartitionedMetrics{ + ClusterID: "foo", + Families: []*clientmodel.MetricFamily{{ + Name: &fooMetricName, + Help: &fooHelp, + Type: &counter, + Metric: []*clientmodel.Metric{{ + Label: []*clientmodel.LabelPair{{Name: &fooLabelName, Value: &fooLabelValue1}}, + Counter: &clientmodel.Counter{Value: &value42}, + TimestampMs: ×tamp, + }, { + Label: []*clientmodel.LabelPair{{Name: &fooLabelName, Value: &fooLabelValue2}}, + Counter: &clientmodel.Counter{Value: &value50}, + TimestampMs: ×tamp, + }}, + }, { + Name: &barMetricName, + Help: &barHelp, + Type: &counter, + Metric: []*clientmodel.Metric{{ + Label: []*clientmodel.LabelPair{{Name: &barLabelName, Value: &barLabelValue1}}, + Counter: &clientmodel.Counter{Value: &value42}, + TimestampMs: ×tamp, + }}, + }}, + }, + want: []prompb.TimeSeries{{ + Labels: []prompb.Label{{Name: nameLabelName, Value: fooMetricName}, {Name: fooLabelName, Value: fooLabelValue1}}, + Samples: []prompb.Sample{{Value: value42, Timestamp: nowTimestamp}}, + }, { + Labels: []prompb.Label{{Name: nameLabelName, Value: fooMetricName}, {Name: fooLabelName, Value: fooLabelValue2}}, + Samples: []prompb.Sample{{Value: value50, Timestamp: nowTimestamp}}, + }, { + Labels: []prompb.Label{{Name: nameLabelName, Value: barMetricName}, {Name: barLabelName, Value: barLabelValue1}}, + Samples: []prompb.Sample{{Value: value42, Timestamp: nowTimestamp}}, + }}, + }, { + name: "gauge", + in: &PartitionedMetrics{ + ClusterID: "foo", + Families: []*clientmodel.MetricFamily{{ + Name: &fooMetricName, + Help: &fooHelp, + Type: &gauge, + Metric: []*clientmodel.Metric{{ + Label: []*clientmodel.LabelPair{{Name: &fooLabelName, Value: &fooLabelValue1}}, + Gauge: &clientmodel.Gauge{Value: &value42}, + TimestampMs: ×tamp, + }, { + Label: []*clientmodel.LabelPair{{Name: &fooLabelName, Value: &fooLabelValue2}}, + Gauge: &clientmodel.Gauge{Value: &value50}, + TimestampMs: ×tamp, + }}, + }, { + Name: &barMetricName, + Help: &barHelp, + Type: &gauge, + Metric: []*clientmodel.Metric{{ + Label: []*clientmodel.LabelPair{{Name: &barLabelName, Value: &barLabelValue1}}, + Gauge: &clientmodel.Gauge{Value: &value42}, + TimestampMs: ×tamp, + }}, + }}, + }, + want: []prompb.TimeSeries{{ + Labels: []prompb.Label{{Name: nameLabelName, Value: fooMetricName}, {Name: fooLabelName, Value: fooLabelValue1}}, + Samples: []prompb.Sample{{Value: value42, Timestamp: nowTimestamp}}, + }, { + Labels: []prompb.Label{{Name: nameLabelName, Value: fooMetricName}, {Name: fooLabelName, Value: fooLabelValue2}}, + Samples: []prompb.Sample{{Value: value50, Timestamp: nowTimestamp}}, + }, { + Labels: []prompb.Label{{Name: nameLabelName, Value: barMetricName}, {Name: barLabelName, Value: barLabelValue1}}, + Samples: []prompb.Sample{{Value: value42, Timestamp: nowTimestamp}}, + }}, + }, { + name: "untyped", + in: &PartitionedMetrics{ + ClusterID: "foo", + Families: []*clientmodel.MetricFamily{{ + Name: &fooMetricName, + Help: &fooHelp, + Type: &untyped, + Metric: []*clientmodel.Metric{{ + Label: []*clientmodel.LabelPair{{Name: &fooLabelName, Value: &fooLabelValue1}}, + Untyped: &clientmodel.Untyped{Value: &value42}, + TimestampMs: ×tamp, + }, { + Label: []*clientmodel.LabelPair{{Name: &fooLabelName, Value: &fooLabelValue2}}, + Untyped: &clientmodel.Untyped{Value: &value50}, + TimestampMs: ×tamp, + }}, + }, { + Name: &barMetricName, + Help: &barHelp, + Type: &untyped, + Metric: []*clientmodel.Metric{{ + Label: []*clientmodel.LabelPair{{Name: &barLabelName, Value: &barLabelValue1}}, + Untyped: &clientmodel.Untyped{Value: &value42}, + TimestampMs: ×tamp, + }}, + }}, + }, + want: []prompb.TimeSeries{{ + Labels: []prompb.Label{{Name: nameLabelName, Value: fooMetricName}, {Name: fooLabelName, Value: fooLabelValue1}}, + Samples: []prompb.Sample{{Value: value42, Timestamp: nowTimestamp}}, + }, { + Labels: []prompb.Label{{Name: nameLabelName, Value: fooMetricName}, {Name: fooLabelName, Value: fooLabelValue2}}, + Samples: []prompb.Sample{{Value: value50, Timestamp: nowTimestamp}}, + }, { + Labels: []prompb.Label{{Name: nameLabelName, Value: barMetricName}, {Name: barLabelName, Value: barLabelValue1}}, + Samples: []prompb.Sample{{Value: value42, Timestamp: nowTimestamp}}, + }}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + out, err := convertToTimeseries(tt.in, now) + if err != nil { + t.Errorf("converting timeseries errored: %v", err) + } + if ok, err := timeseriesEqual(tt.want, out); !ok { + t.Errorf("timeseries don't match: %v", err) + } + }) + } +} + +func timeseriesEqual(t1 []prompb.TimeSeries, t2 []prompb.TimeSeries) (bool, error) { + if len(t1) != len(t2) { + return false, fmt.Errorf("timeseries don't match amount of series: %d != %d", len(t1), len(t2)) + } + + for i, t := range t1 { + for j, l := range t.Labels { + if t2[i].Labels[j].Name != l.Name { + return false, fmt.Errorf("label names don't match: %s != %s", t2[i].Labels[j].Name, l.Name) + } + if t2[i].Labels[j].Value != l.Value { + return false, fmt.Errorf("label values don't match: %s != %s", t2[i].Labels[j].Value, l.Value) + } + } + + for j, s := range t.Samples { + if t2[i].Samples[j].Timestamp != s.Timestamp { + return false, fmt.Errorf("sample timestamps don't match: %d != %d", t2[i].Samples[j].Timestamp, s.Timestamp) + } + if t2[i].Samples[j].Value != s.Value { + return false, fmt.Errorf("sample values don't match: %f != %f", t2[i].Samples[j].Value, s.Value) + } + } + } + + return true, nil +} + +func Test_timeseriesMean(t *testing.T) { + ts := []prompb.TimeSeries{{ + Samples: []prompb.Sample{ + {Value: 0, Timestamp: 15615582010000}, + {Value: 0, Timestamp: 15615582020000}, + {Value: 0, Timestamp: 15615582030000}, + {Value: 0, Timestamp: 15615582040000}, + {Value: 0, Timestamp: 15615582050000}, + }, + }, { + Samples: []prompb.Sample{ + {Value: 0, Timestamp: 15615582000000}, + {Value: 0, Timestamp: 15615582010000}, + {Value: 0, Timestamp: 15615582020000}, + }, + }} + + mean := timeseriesMeanDrift(ts, 15615582050) + if mean != 27.50 { + t.Errorf("expected mean to be 2.75, but got: %.3f", mean) + } +} diff --git a/pkg/server/instrument.go b/pkg/server/instrument.go new file mode 100644 index 00000000..cc31c412 --- /dev/null +++ b/pkg/server/instrument.go @@ -0,0 +1,51 @@ +package server + +import ( + "net/http" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +var ( + requestDuration = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "http_request_duration_seconds", + Help: "Tracks the latencies for HTTP requests.", + }, + []string{"code", "handler", "method"}, + ) + + requestSize = prometheus.NewSummaryVec( + prometheus.SummaryOpts{ + Name: "http_request_size_bytes", + Help: "Tracks the size of HTTP requests.", + }, + []string{"code", "handler", "method"}, + ) + + requestsTotal = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "http_requests_total", + Help: "Tracks the number of HTTP requests.", + }, []string{"code", "handler", "method"}, + ) +) + +func init() { + prometheus.MustRegister(requestDuration, requestSize, requestsTotal) +} + +// InstrumentedHandler is an HTTP middleware that monitors HTTP requests and responses. +func InstrumentedHandler(handlerName string, next http.Handler) http.Handler { + return promhttp.InstrumentHandlerDuration( + requestDuration.MustCurryWith(prometheus.Labels{"handler": handlerName}), + promhttp.InstrumentHandlerRequestSize( + requestSize.MustCurryWith(prometheus.Labels{"handler": handlerName}), + promhttp.InstrumentHandlerCounter( + requestsTotal.MustCurryWith(prometheus.Labels{"handler": handlerName}), + next, + ), + ), + ) +} diff --git a/pkg/server/ratelimited.go b/pkg/server/ratelimited.go new file mode 100644 index 00000000..b9647027 --- /dev/null +++ b/pkg/server/ratelimited.go @@ -0,0 +1,70 @@ +package server + +import ( + "fmt" + "net/http" + "sync" + "time" + + "github.com/go-chi/chi/middleware" + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "golang.org/x/time/rate" +) + +// ErrWriteLimitReached is an error that is returned when a cluster has sent too many requests. +type ErrWriteLimitReached string + +func (e ErrWriteLimitReached) Error() string { + return fmt.Sprintf("write limit reached for cluster with id %q", string(e)) +} + +// Ratelimit is a middleware that rate limits requests based on a cluster ID. +func Ratelimit(logger log.Logger, limit time.Duration, now func() time.Time, next http.HandlerFunc) http.HandlerFunc { + s := ratelimitStore{ + limits: make(map[string]*rate.Limiter), + mu: sync.Mutex{}, + } + + return func(w http.ResponseWriter, r *http.Request) { + rlogger := log.With(logger, "request", middleware.GetReqID(r.Context())) + + clusterID, ok := ClusterIDFromContext(r.Context()) + if !ok { + msg := "failed to get cluster ID from request" + level.Warn(rlogger).Log("msg", msg) + http.Error(w, msg, http.StatusInternalServerError) + return + } + + if err := s.limit(limit, now(), clusterID); err != nil { + level.Debug(rlogger).Log("msg", "rate limited", "err", err) + http.Error(w, err.Error(), http.StatusTooManyRequests) + return + } + + next.ServeHTTP(w, r) + } +} + +type ratelimitStore struct { + limits map[string]*rate.Limiter + mu sync.Mutex +} + +func (s *ratelimitStore) limit(limit time.Duration, now time.Time, key string) error { + s.mu.Lock() + defer s.mu.Unlock() + + limiter, ok := s.limits[key] + if !ok { + limiter = rate.NewLimiter(rate.Every(limit), 1) + s.limits[key] = limiter + } + + if !limiter.AllowN(now, 1) { + return ErrWriteLimitReached(key) + } + + return nil +} diff --git a/pkg/server/ratelimited_test.go b/pkg/server/ratelimited_test.go new file mode 100644 index 00000000..e7b474f2 --- /dev/null +++ b/pkg/server/ratelimited_test.go @@ -0,0 +1,170 @@ +package server + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "sync" + "testing" + "time" + + "github.com/go-kit/kit/log" + "golang.org/x/time/rate" + + "github.com/openshift/telemeter/pkg/authorize" +) + +func TestRatelimit(t *testing.T) { + clientID := func() string { return "" } + + fakeAuthorizeHandler := func(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + r = r.WithContext(authorize.WithClient(r.Context(), &authorize.Client{ + Labels: map[string]string{"_id": clientID()}, + })) + next.ServeHTTP(w, r) + } + } + server := httptest.NewServer( + fakeAuthorizeHandler( + ClusterID(log.NewNopLogger(), "_id", + Ratelimit(log.NewNopLogger(), time.Minute, time.Now, + func(w http.ResponseWriter, r *http.Request) {}, + ), + ), + ), + ) + + defer server.Close() + + for _, tc := range []struct { + name string + advance time.Duration + clientID string + expectedStatus int + expectedErr error + }{ + { + name: "WriteSuccessForA", + advance: 0, + clientID: "a", + expectedStatus: http.StatusOK, + expectedErr: nil, + }, + { + name: "WriteSuccessForB", + advance: 0, + clientID: "b", + expectedStatus: http.StatusOK, + expectedErr: nil, + }, + { + name: "FailAfter1sForA", + advance: time.Second, + clientID: "a", + expectedStatus: http.StatusTooManyRequests, + expectedErr: ErrWriteLimitReached("a"), + }, + } { + t.Run(tc.name, func(t *testing.T) { + clientID = func() string { return tc.clientID } + + time.Sleep(tc.advance) + + req, err := http.NewRequest(http.MethodPost, server.URL, nil) + if err != nil { + t.Error("failed to create request") + return + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Errorf("failed to do request: %v", err) + return + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + + if resp.StatusCode != tc.expectedStatus { + t.Errorf("expected status %d and got %d: %s", tc.expectedStatus, resp.StatusCode, string(body)) + } + + if tc.expectedErr != nil { + if strings.TrimSpace(string(body)) != tc.expectedErr.Error() { + t.Errorf("expcted body '%s', and got '%s'", tc.expectedErr.Error(), strings.TrimSpace(string(body))) + } + } + }) + } +} + +func TestRatelimitStore_Limit(t *testing.T) { + s := &ratelimitStore{ + limits: map[string]*rate.Limiter{}, + mu: sync.Mutex{}, + } + + now := time.Time{}.Add(time.Hour) + + for _, tc := range []struct { + name string + advance time.Duration + key string + err error + }{ + { + name: "first", + advance: 0, + key: "a", + err: nil, + }, + { + name: "1sfails", + advance: time.Second, + key: "a", + err: ErrWriteLimitReached("a"), + }, + { + name: "10sfails", + advance: 10 * time.Second, + key: "a", + err: ErrWriteLimitReached("a"), + }, + { + name: "10sSuccessForB", + advance: 10 * time.Second, + key: "b", + err: nil, + }, + { + name: "1minSuccess", + advance: time.Minute, + key: "a", + err: nil, + }, + { + name: "2minSuccess", + advance: 2 * time.Minute, + key: "a", + err: nil, + }, + { + name: "2minSuccessForB", + advance: 2 * time.Minute, + key: "b", + err: nil, + }, + } { + t.Run(tc.name, func(t *testing.T) { + err := s.limit(time.Minute, now.Add(tc.advance), tc.key) + if err != tc.err { + t.Errorf("expected '%s' got '%s'", tc.err, err) + } + }) + } +} diff --git a/pkg/server/snappy.go b/pkg/server/snappy.go new file mode 100644 index 00000000..0a19cfba --- /dev/null +++ b/pkg/server/snappy.go @@ -0,0 +1,20 @@ +package server + +import ( + "io/ioutil" + "net/http" + "strings" + + "github.com/golang/snappy" +) + +// Snappy checks HTTP headers and if Content-Ecoding is snappy it decodes the request body. +func Snappy(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if strings.EqualFold(r.Header.Get("Content-Encoding"), "snappy") { + r.Body = ioutil.NopCloser(snappy.NewReader(r.Body)) + } + + next.ServeHTTP(w, r) + } +} diff --git a/pkg/server/snappy_test.go b/pkg/server/snappy_test.go new file mode 100644 index 00000000..4a0913c3 --- /dev/null +++ b/pkg/server/snappy_test.go @@ -0,0 +1,118 @@ +package server + +import ( + "bytes" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/golang/snappy" + clientmodel "github.com/prometheus/client_model/go" + "github.com/prometheus/common/expfmt" +) + +func TestSnappy(t *testing.T) { + fooMetricName := "foo_metric" + fooLabelName := "foo" + fooLabelValue1 := "bar" + value42 := 42.0 + timestamp := int64(15615582020000) + + families := []*clientmodel.MetricFamily{{ + Name: &fooMetricName, + Metric: []*clientmodel.Metric{{ + Label: []*clientmodel.LabelPair{ + {Name: &fooLabelName, Value: &fooLabelValue1}, + }, + Counter: &clientmodel.Counter{Value: &value42}, + TimestampMs: ×tamp, + }}, + }} + + s := httptest.NewServer(Snappy(func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + + decoder := expfmt.NewDecoder(r.Body, expfmt.FmtProtoDelim) + families := make([]*clientmodel.MetricFamily, 0, 1) + for { + family := &clientmodel.MetricFamily{} + if err := decoder.Decode(family); err != nil { + if err == io.EOF { + break + } + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + families = append(families, family) + } + + if len(families) != 1 { + t.Errorf("expected 1 metric family got: %d", len(families)) + } + if *families[0].Name != fooMetricName { + t.Errorf("metric name is not as expected, is: %s", *families[0].Name) + } + })) + + { + payload := bytes.Buffer{} + + compress := snappy.NewBufferedWriter(&payload) + encoder := expfmt.NewEncoder(compress, expfmt.FmtProtoDelim) + for _, family := range families { + if family == nil { + continue + } + if err := encoder.Encode(family); err != nil { + t.Fatal(err) + } + } + if err := compress.Flush(); err != nil { + t.Fatal(err) + } + + req, err := http.NewRequest(http.MethodPost, s.URL, &payload) + if err != nil { + t.Fatal(err) + } + + req.Header.Set("Content-Encoding", "snappy") + + resp, err := s.Client().Do(req) + if err != nil { + t.Fatal(err) + } + + if resp.StatusCode != http.StatusOK { + t.Errorf("expected %d and got %d", http.StatusOK, resp.StatusCode) + } + } + { + payload := bytes.Buffer{} + + encoder := expfmt.NewEncoder(&payload, expfmt.FmtProtoDelim) + for _, family := range families { + if family == nil { + continue + } + if err := encoder.Encode(family); err != nil { + t.Fatal(err) + } + } + + req, err := http.NewRequest(http.MethodPost, s.URL, &payload) + if err != nil { + t.Fatal(err) + } + + resp, err := s.Client().Do(req) + if err != nil { + t.Fatal(err) + } + + if resp.StatusCode != http.StatusOK { + t.Errorf("expected %d and got %d", resp.StatusCode, http.StatusOK) + } + } +} diff --git a/pkg/server/store.go b/pkg/server/store.go new file mode 100644 index 00000000..33e3887f --- /dev/null +++ b/pkg/server/store.go @@ -0,0 +1,10 @@ +package server + +import ( + clientmodel "github.com/prometheus/client_model/go" +) + +type PartitionedMetrics struct { + ClusterID string + Families []*clientmodel.MetricFamily +} diff --git a/pkg/server/validator.go b/pkg/server/validator.go new file mode 100644 index 00000000..dea75335 --- /dev/null +++ b/pkg/server/validator.go @@ -0,0 +1,175 @@ +package server + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "time" + + "github.com/go-chi/chi/middleware" + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + clientmodel "github.com/prometheus/client_model/go" + "github.com/prometheus/common/expfmt" + + "github.com/openshift/telemeter/pkg/authorize" + "github.com/openshift/telemeter/pkg/metricfamily" + "github.com/openshift/telemeter/pkg/reader" +) + +type clusterIDCtxType int + +const ( + clusterIDCtx clusterIDCtxType = iota +) + +// WithClusterID puts the clusterID into the given context. +func WithClusterID(ctx context.Context, clusterID string) context.Context { + return context.WithValue(ctx, clusterIDCtx, clusterID) +} + +// ClusterIDFromContext returns the clusterID from the context. +func ClusterIDFromContext(ctx context.Context) (string, bool) { + p, ok := ctx.Value(clusterIDCtx).(string) + return p, ok +} + +// ClusterID is a HTTP middleware that extracts the cluster's ID and passes it on via context. +func ClusterID(logger log.Logger, key string, next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + rlogger := log.With(logger, "request", middleware.GetReqID(r.Context())) + + client, ok := authorize.FromContext(r.Context()) + if !ok { + msg := "unable to find user info" + level.Warn(rlogger).Log("msg", msg) + http.Error(w, msg, http.StatusInternalServerError) + return + } + if len(client.Labels[key]) == 0 { + msg := fmt.Sprintf("user data must contain a '%s' label", key) + level.Warn(rlogger).Log("msg", msg) + http.Error(w, msg, http.StatusInternalServerError) + return + } + + r = r.WithContext(WithClusterID(r.Context(), client.Labels[key])) + + next.ServeHTTP(w, r) + } +} + +// Validate the payload of a request against given and required rules. +func Validate(logger log.Logger, baseTransforms metricfamily.Transformer, maxAge time.Duration, limitBytes int64, now func() time.Time, next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + rlogger := log.With(logger, "request", middleware.GetReqID(r.Context())) + + client, ok := authorize.FromContext(r.Context()) + if !ok { + msg := "unable to find user info" + level.Warn(rlogger).Log("msg", msg) + http.Error(w, msg, http.StatusInternalServerError) + return + } + + if limitBytes > 0 { + r.Body = reader.NewLimitReadCloser(r.Body, limitBytes) + } + + body, err := ioutil.ReadAll(r.Body) + if err != nil { + msg := "failed to read request body" + if errors.Is(err, reader.ErrTooLong) { + level.Warn(rlogger).Log("msg", msg, "err", err) + http.Error(w, msg, http.StatusRequestEntityTooLarge) + return + } + level.Warn(rlogger).Log("msg", msg, "err", err) + http.Error(w, msg, http.StatusInternalServerError) + return + } + defer r.Body.Close() + + var transforms metricfamily.MultiTransformer + transforms.With(metricfamily.NewErrorOnUnsorted(true)) + transforms.With(metricfamily.NewRequiredLabels(client.Labels)) + transforms.With(metricfamily.TransformerFunc(metricfamily.DropEmptyFamilies)) + + // these transformers need to be created for every request + if maxAge > 0 { + transforms.With(metricfamily.NewErrorInvalidFederateSamples(now().Add(-maxAge))) + } + + transforms.With(metricfamily.OverwriteTimestamps(now)) + transforms.With(baseTransforms) + + decoder := expfmt.NewDecoder(bytes.NewBuffer(body), expfmt.ResponseFormat(r.Header)) + + families := make([]*clientmodel.MetricFamily, 0, 100) + for { + family := &clientmodel.MetricFamily{} + if err := decoder.Decode(family); err != nil { + if err == io.EOF { + break + } + msg := "failed to decode metrics" + level.Warn(rlogger).Log("msg", msg, "err", err) + http.Error(w, msg, http.StatusInternalServerError) + return + } + families = append(families, family) + } + + families = metricfamily.Pack(families) + + if err := metricfamily.Filter(families, transforms); err != nil { + if errors.Is(err, metricfamily.ErrNoTimestamp) { + level.Debug(rlogger).Log("msg", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if errors.Is(err, metricfamily.ErrUnsorted) { + level.Debug(rlogger).Log("msg", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if errors.Is(err, metricfamily.ErrTimestampTooOld) { + level.Debug(rlogger).Log("msg", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if errors.Is(err, metricfamily.ErrRequiredLabelMissing) { + level.Debug(rlogger).Log("msg", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + msg := "unexpected error during metrics transforming" + level.Warn(rlogger).Log("msg", msg, "err", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + buf := &bytes.Buffer{} + encoder := expfmt.NewEncoder(buf, expfmt.ResponseFormat(r.Header)) + for _, f := range families { + if f == nil { + continue + } + if err := encoder.Encode(f); err != nil { + msg := "failed to encode transformed metrics again" + level.Warn(rlogger).Log("msg", msg, "err", err) + http.Error(w, msg, http.StatusInternalServerError) + return + } + } + + r.Body = ioutil.NopCloser(buf) + + next.ServeHTTP(w, r) + } +} diff --git a/pkg/server/validator_test.go b/pkg/server/validator_test.go new file mode 100644 index 00000000..a1462fe9 --- /dev/null +++ b/pkg/server/validator_test.go @@ -0,0 +1,183 @@ +package server + +import ( + "bytes" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/go-kit/kit/log" + clientmodel "github.com/prometheus/client_model/go" + "github.com/prometheus/common/expfmt" + + "github.com/openshift/telemeter/pkg/authorize" + "github.com/openshift/telemeter/pkg/metricfamily" +) + +func TestValidate(t *testing.T) { + fooMetricName := "foo_metric" + fooLabelName := "foo" + fooLabelValue1 := "bar" + clientIDLabelName := "_id" + clientIDLabelValue := "test" + value42 := 42.0 + timestamp := int64(15615582020000) + timestampNewer := int64(15615582020000 + 10000) + timestampTooOld := int64(1234) + + fakeAuthorizeHandler := func(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + r = r.WithContext(authorize.WithClient(r.Context(), &authorize.Client{ + Labels: map[string]string{"_id": "test"}, + })) + next.ServeHTTP(w, r) + } + } + + now := func() time.Time { return time.Date(2020, 04, 20, 20, 20, 20, 0, time.UTC) } + + s := httptest.NewServer( + fakeAuthorizeHandler( + Validate(log.NewNopLogger(), metricfamily.MultiTransformer{}, time.Hour, 512*1024, now, + func(w http.ResponseWriter, r *http.Request) { + // TODO: Make the check proper to changing timestamps? + body, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Fatalf("failed to read body: %v", err) + } + + expectBody := "# TYPE foo_metric counter\nfoo_metric{_id=\"test\",foo=\"bar\"} 42 1587414020000\n" + + if strings.TrimSpace(string(body)) != strings.TrimSpace(expectBody) { + t.Errorf("expected '%s', got: %s", expectBody, string(body)) + } + }, + ), + ), + ) + defer s.Close() + + testcases := []struct { + name string + families []*clientmodel.MetricFamily + expectStatus int + expectBody string + }{{ + name: "valid", + families: []*clientmodel.MetricFamily{{ + Name: &fooMetricName, + Metric: []*clientmodel.Metric{{ + Label: []*clientmodel.LabelPair{ + {Name: &clientIDLabelName, Value: &clientIDLabelValue}, + {Name: &fooLabelName, Value: &fooLabelValue1}, + }, + Counter: &clientmodel.Counter{Value: &value42}, + TimestampMs: ×tamp, + }}, + }}, + expectStatus: http.StatusOK, + expectBody: "", + }, { + name: "noTimestamp", + families: []*clientmodel.MetricFamily{{ + Name: &fooMetricName, + Metric: []*clientmodel.Metric{{ + Label: []*clientmodel.LabelPair{ + {Name: &clientIDLabelName, Value: &clientIDLabelValue}, + {Name: &fooLabelName, Value: &fooLabelValue1}, + }, + Counter: &clientmodel.Counter{Value: &value42}, + }}, + }}, + expectStatus: http.StatusBadRequest, + expectBody: "metrics in provided family do not have a timestamp", + }, { + name: "unsorted", + families: []*clientmodel.MetricFamily{{ + Name: &fooMetricName, + Metric: []*clientmodel.Metric{{ + Label: []*clientmodel.LabelPair{ + {Name: &clientIDLabelName, Value: &clientIDLabelValue}, + {Name: &fooLabelName, Value: &fooLabelValue1}, + }, + Counter: &clientmodel.Counter{Value: &value42}, + TimestampMs: ×tampNewer, + }, { + Label: []*clientmodel.LabelPair{ + {Name: &clientIDLabelName, Value: &clientIDLabelValue}, + {Name: &fooLabelName, Value: &fooLabelValue1}, + }, + Counter: &clientmodel.Counter{Value: &value42}, + TimestampMs: ×tamp, + }}, + }}, + expectStatus: http.StatusBadRequest, + expectBody: "metrics in provided family are not in increasing timestamp order", + }, { + name: "tooOld", + families: []*clientmodel.MetricFamily{{ + Name: &fooMetricName, + Metric: []*clientmodel.Metric{{ + Label: []*clientmodel.LabelPair{ + {Name: &clientIDLabelName, Value: &clientIDLabelValue}, + {Name: &fooLabelName, Value: &fooLabelValue1}, + }, + Counter: &clientmodel.Counter{Value: &value42}, + TimestampMs: ×tampTooOld, + }}, + }}, + expectStatus: http.StatusBadRequest, + expectBody: "metrics in provided family have a timestamp that is too old, check clock skew", + }, { + name: "missingRequiredLabel", + families: []*clientmodel.MetricFamily{{ + Name: &fooMetricName, + Metric: []*clientmodel.Metric{{ + Label: []*clientmodel.LabelPair{ + {Name: &fooLabelName, Value: &fooLabelValue1}, + }, + Counter: &clientmodel.Counter{Value: &value42}, + TimestampMs: ×tampTooOld, + }}, + }}, + expectStatus: http.StatusBadRequest, + expectBody: "a required label is missing from the metric", + }} + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + buf := &bytes.Buffer{} + encoder := expfmt.NewEncoder(buf, expfmt.FmtText) + for _, f := range tc.families { + if err := encoder.Encode(f); err != nil { + t.Fatal(err) + } + } + + req, err := http.NewRequest(http.MethodPost, s.URL, buf) + if err != nil { + t.Fatal(err) + } + req.Header.Set("Content-Type", string(expfmt.FmtText)) + + resp, err := s.Client().Do(req) + if err != nil { + t.Fatal(err) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + + if resp.StatusCode != tc.expectStatus { + t.Errorf("expected status code %d but got %d: %s", tc.expectStatus, resp.StatusCode, string(body)) + } + if strings.TrimSpace(string(body)) != tc.expectBody { + t.Errorf("expected body to be '%s' but got '%s'", tc.expectBody, strings.TrimSpace(string(body))) + } + }) + } +} diff --git a/temp/certs/ca.key b/temp/certs/ca.key new file mode 100644 index 00000000..6478b619 --- /dev/null +++ b/temp/certs/ca.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIJPGaoMOIpbgVLQRXeHR0C/vCj5llLzcX+S6YSH249vFoAoGCCqGSM49 +AwEHoUQDQgAE5cw8FL5xJLnjGj283ixGM8gmhgwBA/jIgHz/wByrHA7zH6cEGDiu +hFTSMViSzsAmJohxVPCcyLeWnn3GkgypAw== +-----END EC PRIVATE KEY----- diff --git a/temp/certs/ca.pem b/temp/certs/ca.pem new file mode 100644 index 00000000..cc2e885b --- /dev/null +++ b/temp/certs/ca.pem @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIBdTCCARqgAwIBAgIUczfpTU1j+4voaWCqvuFBJXqagZwwCgYIKoZIzj0EAwIw +GDEWMBQGA1UEAxMNb2JzZXJ2YXRvcml1bTAeFw0yMDA3MjQxNjU1MDBaFw0yNTA3 +MjMxNjU1MDBaMBgxFjAUBgNVBAMTDW9ic2VydmF0b3JpdW0wWTATBgcqhkjOPQIB +BggqhkjOPQMBBwNCAATlzDwUvnEkueMaPbzeLEYzyCaGDAED+MiAfP/AHKscDvMf +pwQYOK6EVNIxWJLOwCYmiHFU8JzIt5aefcaSDKkDo0IwQDAOBgNVHQ8BAf8EBAMC +AQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUC+PLg8jIigT/IR7kZdO8SsoC +lL4wCgYIKoZIzj0EAwIDSQAwRgIhAN4yFnAq6qstwTSq/IXAeCW6fJwEjHvpic6h +lU9mW1eHAiEApwCx4U+hEzna65/MYvJm8hvgwCvzQx752rJR6thVql4= +-----END CERTIFICATE----- diff --git a/temp/certs/client.key b/temp/certs/client.key new file mode 100644 index 00000000..cc08ac8a --- /dev/null +++ b/temp/certs/client.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIF6E5OS+KsIBbzn/nIlN5rPw88xpdZcU4m2WNQEpXMH3oAoGCCqGSM49 +AwEHoUQDQgAEwnmc+PBGgOAOS2hQvX+YzA7r2Mg3Afsc+Kw0qiopxHiEwm9sdZ0s +UYIiZVWhVG8oo0llMX8EyjANtwxaGnMV6w== +-----END EC PRIVATE KEY----- diff --git a/temp/certs/client.pem b/temp/certs/client.pem new file mode 100644 index 00000000..7c4e2b76 --- /dev/null +++ b/temp/certs/client.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICYzCCAgmgAwIBAgIUCLOsv7M3vgvr0RpuTw8owGH7KogwCgYIKoZIzj0EAwIw +GDEWMBQGA1UEAxMNb2JzZXJ2YXRvcml1bTAeFw0yMDA3MjQxNjU1MDBaFw0yMTA3 +MjQxNjU1MDBaMGcxDTALBgNVBAsTBHRlc3QxVjBUBgNVBAMTTXRlc3Qtb3Blbi1j +bHVzdGVyLW1hbmFnZW1lbnQtbW9uaXRvcmluZy5hcHBzLm1hcmNvLmRldjA1LnJl +ZC1jaGVzdGVyZmllbGQuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwnmc ++PBGgOAOS2hQvX+YzA7r2Mg3Afsc+Kw0qiopxHiEwm9sdZ0sUYIiZVWhVG8oo0ll +MX8EyjANtwxaGnMV66OB4TCB3jAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYI +KwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUrxhZgIDDv+sbN8Wpu/AQ +lFenNwMwHwYDVR0jBBgwFoAUC+PLg8jIigT/IR7kZdO8SsoClL4waQYDVR0RBGIw +YIIJbG9jYWxob3N0gk10ZXN0LW9wZW4tY2x1c3Rlci1tYW5hZ2VtZW50LW1vbml0 +b3JpbmcuYXBwcy5tYXJjby5kZXYwNS5yZWQtY2hlc3RlcmZpZWxkLmNvbYcEfwAA +ATAKBggqhkjOPQQDAgNIADBFAiAb0X++YPPZnF5zJqPRIyhUKz+h8P7uFQbSVl5j +HaF8dgIhAOHanPJNqhZ1Xd4+8a8NVSRKd5gyOBGafKuJUAlYOLH0 +-----END CERTIFICATE----- diff --git a/temp/certs/server.key b/temp/certs/server.key new file mode 100644 index 00000000..5703adba --- /dev/null +++ b/temp/certs/server.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIAStnFsdQVfXdyMYfipcxS3h1xfrcBBBUStEb1YKE6FToAoGCCqGSM49 +AwEHoUQDQgAE2c4/INPf81tdbbjECIS5/KHWH9/c3DEN+Sq8GEqLdXlfOW7Rg7h3 +8YKA6cPnXfBcj7JGVZkgyVp4CuaynNs57w== +-----END EC PRIVATE KEY----- diff --git a/temp/certs/server.pem b/temp/certs/server.pem new file mode 100644 index 00000000..bc8ef8f6 --- /dev/null +++ b/temp/certs/server.pem @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICEDCCAbagAwIBAgIUZRjYYo/TfmjS2KEAjeQuFlynVqMwCgYIKoZIzj0EAwIw +GDEWMBQGA1UEAxMNb2JzZXJ2YXRvcml1bTAeFw0yMDA3MjQxNjU1MDBaFw0yMTA3 +MjQxNjU1MDBaMBQxEjAQBgNVBAMTCWxvY2FsaG9zdDBZMBMGByqGSM49AgEGCCqG +SM49AwEHA0IABNnOPyDT3/NbXW24xAiEufyh1h/f3NwxDfkqvBhKi3V5Xzlu0YO4 +d/GCgOnD513wXI+yRlWZIMlaeArmspzbOe+jgeEwgd4wDgYDVR0PAQH/BAQDAgWg +MBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFMtq +g24YUSz4WxeneDLBUXSBXgflMB8GA1UdIwQYMBaAFAvjy4PIyIoE/yEe5GXTvErK +ApS+MGkGA1UdEQRiMGCCCWxvY2FsaG9zdIJNdGVzdC1vcGVuLWNsdXN0ZXItbWFu +YWdlbWVudC1tb25pdG9yaW5nLmFwcHMubWFyY28uZGV2MDUucmVkLWNoZXN0ZXJm +aWVsZC5jb22HBH8AAAEwCgYIKoZIzj0EAwIDSAAwRQIgGj7BcOI1jTA3B+/gGUzk +6WD63T68IYzQSqJn76qZqCICIQCyOgcud/ThqDtxXzf29d/uvT/1HkT87JWaVi/Z +AYI3fA== +-----END CERTIFICATE----- diff --git a/temp/client_secret.yaml b/temp/client_secret.yaml new file mode 100755 index 00000000..661bf900 --- /dev/null +++ b/temp/client_secret.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +data: + ca.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZwekNDQTQrZ0F3SUJBZ0lVVHd2Kzl2Q2YrV241YzhCd1RQTlJqdVYwQTBvd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1l6RUxNQWtHQTFVRUJoTUNWVk14RVRBUEJnTlZCQWdNQ0U1bGR5QlpiM0pyTVE4d0RRWURWUVFIREFaQgpjbTF2Ym1zeEdqQVlCZ05WQkFvTUVVbENUU0JEYkc5MVpDQlFjbWwyWVhSbE1SUXdFZ1lEVlFRRERBdDNkM2N1CmFXSnRMbU52YlRBZUZ3MHlNREF5TURjd05EVXpNVEphRncwek1EQXlNRFF3TkRVek1USmFNR014Q3pBSkJnTlYKQkFZVEFsVlRNUkV3RHdZRFZRUUlEQWhPWlhjZ1dXOXlhekVQTUEwR0ExVUVCd3dHUVhKdGIyNXJNUm93R0FZRApWUVFLREJGSlFrMGdRMnh2ZFdRZ1VISnBkbUYwWlRFVU1CSUdBMVVFQXd3TGQzZDNMbWxpYlM1amIyMHdnZ0lpCk1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQ0R3QXdnZ0lLQW9JQ0FRQ2lyQ0RGeGkwUENhUVM3MlJNcXpmSDYrRVEKeDNZQ0RPSVNkQnpOR0FWVkhrOUdTRVVRNDFNcGxlZFIrdlkxckw4NUdaWkRETW8rK0QvV2YrK2l5VUdJeWZKbgpEQmhmYWlnNlhYbG5FUm93dUR2Q1Z0ejJTTXR0elh6dVdwQTBvQ3NXQ2p5Nmc2dHdIUk1oZVoxYmV0S3RIdElzCjhnNG5wNTZ4dE81ZzhZOG5oRVRRbDJLM2ZubFRwbmcrNE9jekhvMWJqemgzdnNENzF1MGF4M3NPa2tZeHpEOWEKcGhOZW44Wm9qK0pvc2FhSXNyZDJzaklKUEJUZUlUQ3IrMFhEVnRFWGQxWFQyekNXV2VveU52OHVtbDNkZG5EVQpOL2hoQitOV3pGd2c1ak9NKzZqR212cTczYityMGY5d0l4QkNtWWNKQUs5QmtuZmdEUm5LTDVEK0NoQm1zbEZxClpFZy9rLytEVEp4R2hPdmxwdS94MFFsdkZTc1VsNUwrUU94cVdtaDhkaVhidkJ5Rml6QkMzamFFQ2F2MlNlY0QKOExRbk92VytvWWEzN1dsNWVUellscHNEK0ZIbzRVeUNFQWNSVnJFbWhBcG5Pdmk5M2JIYi94a28rSjB6dVJYdgphdzhQMGI1Ym5RS1dqdWxNbVZNalpZQlNGazhXYjExekNsZ3d6SHFkUiszaXRPRHdZTi9pL1ltZE5LbG1ybzhCCmVKd09uVkV0WC8wY3grS2Q2MVBVTDlnY0VHeXB3Slczb2tYZTFtOEdvRFB1M1VxVER5ek5UclUrdjFVZmc4OEwKNERwUVIzeGkxcEppOVBqSHBaSDl0RHdGMWpQbGdBdlIvKzV4TnBuaHhlbWxOZUFxQjBVZzdtSi9RblhUR24xZwpON0tOUUx4OTZmNVUxdW9uTVFJREFRQUJvMU13VVRBZEJnTlZIUTRFRmdRVWE3d2lHdFY5ODFCR05PY21PcDR4Cmk4RXpaM013SHdZRFZSMGpCQmd3Rm9BVWE3d2lHdFY5ODFCR05PY21PcDR4aThFelozTXdEd1lEVlIwVEFRSC8KQkFVd0F3RUIvekFOQmdrcWhraUc5dzBCQVFzRkFBT0NBZ0VBbVdxWDVCM0NhTEpHR3FzWmpBVkdLTHVUVzhkQQpKbTFNekJacThCcXFTaE5tSHBFeUJLOFBhL0dnZW5xMklyQkNpRnZkL2lKalBQN2tGczg5eEd5aUdsV0NNOWEwCkxtTnhkdjlqSXZMMlFOWFZnVng3VW5QQjc2SzFxTW1qaWVrSytzK1I1N0U5R3VyaEp2K3M0MWNObzNrbWkySHIKdFc2dVNIUXRrVU5ob1EvbnUrV0lmZDlhUWFEUWVNSTd6UHFHRDk5OHJhazNXaTNzYWt2ZDFCUEpueDcwczVmTQpRTmlQbE95blJhdXovUG9rcE8vMS9xY0NDV3JlTnVJUExpWVQ5cU1kZXp2WGVMVWhjVmtYd21XR1dTNWpQQk1NClRuUlhwa2lNVXhUSjZYSGFuMEdyVVVaelFtb2EvSnBhMm4zUjlhaUtRek83TFUxSVFGdXVsT0VNZVZQbUpreTYKblJZeENwbzNzNEJaeWVMSG92Y0g1RDJ2Tlo2b3IxdHFRRkhFUHBuYkdrR3FsNFl4S1lFVkpiOTNtZ3IvRHZBcQorQ3JyT0w5d2ZpVStQWnVVdWFHbzEzczJTRk5DV1NWRlpPRUp6eDVtMzhJcWcvb1Q4ZE9qYTZwRytUWDVpdDMvCnNBS0pjWTZndU84ZS9PNWFQclJEZlgzN0g3MHlZNDdUcFFuaTM5K24xUlFMSWdvU2s2MG1oZHQwYUplN0NTMWwKZ1NvQWM5R2pKU1QyaG5jaU56N1dWeEpFclVKZnh1S2FETFl3UEpya0k0ekhLcGpJb2JxN3NZUlltdStRVUdsKwo4KzlYbkRjYWtTZG1OMTVZTVBuWkRzT0paSmZUSWVEVnB3TWxCbTROcHp5M1djS0ZtRlVrMEN5TWRhanJqQmFrCnh0M1MzcWZ5NmExS3FPdz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVmekNDQW1lZ0F3SUJBZ0lRVDRSaVEzZnpKQlUrV2ZlQ1Z0SUpQREFOQmdrcWhraUc5dzBCQVFzRkFEQmoKTVFzd0NRWURWUVFHRXdKVlV6RVJNQThHQTFVRUNBd0lUbVYzSUZsdmNtc3hEekFOQmdOVkJBY01Ca0Z5Ylc5dQphekVhTUJnR0ExVUVDZ3dSU1VKTklFTnNiM1ZrSUZCeWFYWmhkR1V4RkRBU0JnTlZCQU1NQzNkM2R5NXBZbTB1ClkyOXRNQjRYRFRJd01ETXhOakl3TlRNME1Gb1hEVEl3TURZeE5ESXdOVE0wTUZvd016RVZNQk1HQTFVRUNoTU0KWTJWeWRDMXRZVzVoWjJWeU1Sb3dHQVlEVlFRREV4RnRZVzVoWjJWa0xXTnNkWE4wWlhJdE1UQ0NBU0l3RFFZSgpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFMaVhhbWtoTytLQXZScXZrNjNqYTZ0YTZ5RXpoRkw1CjI1a3Y2WkFudFVHK2k3UkNGYTUwMFB1cHk4V1VYUGlHN014ZmxHZlJFb01zbndWd1hMeHl6bmoyWCtlMWR5R3cKTDVHMEIralpKZ0lxSjVzWHBNMzM4NGVnM0paWnBlNDd1NFh5R0tuSW1INXVHOVpWSU1iWUFaZmRkclI5bnNzZQo1S0N3ODdETmJ0ZllLam1XUzJYVFJpbVNwRXNBb09WK3ptdzlwSCszaUR5cFlTa2Y1OFByTmNVMmtvWkJIK2p3Clk2SlNFa09oYWZ6VGI0VGNFQVU0VG9yY1JZZm1TREY3RytjMTRFZVN6alFmNm9kd0l2NEJMemZzQWdtbk1UZjQKakRXaFpkSk9QNUtDKzJtdjdFL2VLMnRFMVZCYjBLdkNNbVBFSGpvc25TUms3VmZzRXVvUDJLY0NBd0VBQWFOZgpNRjB3RGdZRFZSMFBBUUgvQkFRREFnV2dNQXdHQTFVZEV3RUIvd1FDTUFBd0h3WURWUjBqQkJnd0ZvQVVhN3dpCkd0Vjk4MUJHTk9jbU9wNHhpOEV6WjNNd0hBWURWUjBSQkJVd0U0SVJiV0Z1WVdkbFpDMWpiSFZ6ZEdWeUxURXcKRFFZSktvWklodmNOQVFFTEJRQURnZ0lCQUJpbFR2ZHN3MDNKbkdZcEM5dnZNN29RZk4xY2hOd2dRR0xNNVRNOQo4K3lndGhNaHZvSjFHSXJsRWJqS2MrWmdFZzFZMUowZy8rV0I4dW05ZzhMc3BrNmp1RWtNWFlOQmFNYWpvN21TCllhMmlPcTQ5QWVZL0g3WC9yTnNxbVNPcGV3NWNXcG51TUVEMXhDS2tBVmZQZks4ZEttelRsdXVYSVhWNUpQd0QKK3dKQ0NMbHEydjdnUGZBVWpDVGR0KzBjeTBjalUyMGlpcUZha2ExWEtjL0dDZDdTSmhWRjAwN3JlSHdyZGVaLwoxR1RMdWdodm5QY20vek91NjVPOC93NlZLeVlGZ0JWS054UjBwMzZVemRJVGJtdERZYjNaYkVockNqd2UyUHJ6CkpFZVlzQ1JBZDR2eTJOcjJhQVBjeEd3QzRDR0g5SmFhMGk4SU94bnRHN1NnNW5oYWlDSU0rQmRGNDlrOFdGNHkKSDd2STlKcUVuMkVLYnN1SmVsMXlyemxSLzNXZ2o4Ym9zWEZYaXVTQmhWalNHd3h5MU1EMVZ5enZmMDZSWTY0YQpDMERDWnlhQmZXRkxqdVVDeVc0L0dBajlEbTB2WU0ra0xUbzlsY24reEN4WmlKQlcvaHF4bnFBM0UzOFFIbVV3CmxXcVp5Wno4UW5QUWozWWZncjNlVWYwV0N5V2NtSWZ5Y0xEUGpKczZ3clJHYWMyQXZjamRSRGpza0FCd0o2QjkKR3FicTZjRGRUbU5MTGtCdS9ENWI4eVl5RGR1T25SYjA3V2dUNFd1UEErQUdUN0dkSTR3ZWxYMExiQnJ2aGtiVwozVE5makdlaXJsb0pWTFFUbnY3ekN2UEZWZHRoTm52SjNYak5TcnllcFdKMnZublJIc2RGZ1dGYTh5a0srWCtTClNWclIKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= + tls.key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBdUpkcWFTRTc0b0M5R3ErVHJlTnJxMXJySVRPRVV2bmJtUy9wa0NlMVFiNkx0RUlWCnJuVFErNm5MeFpSYytJYnN6RitVWjlFU2d5eWZCWEJjdkhMT2VQWmY1N1YzSWJBdmtiUUg2TmttQWlvbm14ZWsKemZmemg2RGNsbG1sN2p1N2hmSVlxY2lZZm00YjFsVWd4dGdCbDkxMnRIMmV5eDdrb0xEenNNMXUxOWdxT1paTApaZE5HS1pLa1N3Q2c1WDdPYkQya2Y3ZUlQS2xoS1IvbncrczF4VGFTaGtFZjZQQmpvbElTUTZGcC9OTnZoTndRCkJUaE9pdHhGaCtaSU1Yc2I1elhnUjVMT05CL3FoM0FpL2dFdk4rd0NDYWN4Ti9pTU5hRmwwazQva29MN2FhL3MKVDk0cmEwVFZVRnZRcThJeVk4UWVPaXlkSkdUdFYrd1M2Zy9ZcHdJREFRQUJBb0lCQUd3N3hCWU9lWW1PeU5MTAoza0NZVjNwcTNmRml2cFRVa2pGWkNZOFA2VlM2UURvYWdaSUFSc1U1UXhUL3NCKzlKVDJVVVhVcC9ydlJQeEMyCldIbmFxenY3NGpIL2tmRzcxN2lNSWhNaXVBbU81QmdwSGVYekcrVUxxaXV5TnZ2Z0pFMGVyZDFubEJxVnYxYkcKSldqU0lPVUY0dU5qd09jQ256V2xhODNnTnJ6eHNDUktWWU9BbnFpU0hTeW1adm9RRTU4Yi8zWkgyYTl4MXNYMgpaRHR0STE0Q2YzalpBbUJsQ0xRLzNPd2pxR3JwYVlydFEwQkQvVm1ZUk0xU25iSW1VVnFMb2RxRGxrRHl2VmhiCjFzZm5pbEFGb1lJUDk0VTZWZFhxODFWTEUvTGpWSE1NSjFZVE93ekt1MnZONmlZaGR0MjR3bGoxa3BvVkJrM1AKN2lQblBBRUNnWUVBeTlLQTV4S2Y0MTlpK2VnOE1oQWx4dFo3cHM3c1ZJQU5lT0NsZnJ6U2dFTDNUbjBWOWNKMApWeWdTTXpGMzcyTVBENUJCWDFMdUpPVS9TWk0rb242Q05CbjZCTTJGMkdlOHduekZjN3hWbmE4YWpZNjlWSEhiCjFZdDBlMiszMlovR24zWGQrSWR3VVhLZzBBUzRYRGVpdDBkaWxUbGZxalVNc3p3V0M0NlRuZ0VDZ1lFQTU5aWUKM1Bzem8xSzVuRHNOS0ZwcDhrNHRaRU5tNUpJSkUyaE8ybGxvMVlURUtaWGZoZVBKNmRBMzltTkxZTVpuVEJnRwovaTJ1YStJVWRTSVpvamRwazRQa1k3YWNuVERCdmNUaG1TL2sxVnp0N25NaXhaYkF5b1FKZXBOUGZJb0l3YTJxCldkUmo1MzZ6ZzlsOVp1Y2Y2NjFaSmJPVkF0OXBZcndPSDNtUHhxY0NnWUVBajhvUGVmU2ZxMjR0aXhRVTgxU0UKOGdONTlESGljN0pxenEzYzBzNHV5cUZ6aW9HZ0xtSmlaT0kyQkx1UFd1aE5SYk9GQ3RTY2dKYmgwT3Y1c2ZVQgpzZlBwZDkzdDRMOCtZUTVZMWM4MXJ5cEsxemF0eHBjVWVWQUtldEpUcDBtYWZBQlErZlhDZlJNYTgrV0FrajRGCm9yclBoMnFVWEtWVU1sWGZUNFBrREFFQ2dZRUFzWm1iNEZYQnNidmVHVktXK3JGLzlUQnp6eGxleVhzZzNyQUoKcjNQTVBidnRkSm10VjJndU5TRGVyYXhVZ3JhWlRJNGZWVGh6STR0VTlvRi90MmJSUmtKRGd5clJBQXpvYk5GVApxOEFuZ0ZZbW9ZR3JRa2NBT21JNHpKL3B4TklNY1NqeWxNdUJHRVZUaUkzalpSOXBzV1RpSkdKVHBKYTFxUCs3CnBkcStDNE1DZ1lFQXl0UEFjN3ZMSUVBN0VSM2U5UkVUc0YvQmlYTGxtbVh3VE1UYzBQQURWMU8rai9kSkhVdmcKQjhPU3o0MkNwdGliSjFRTjRjVzF6a1FHRlRwU3M0RXBNS1pYQm9RaE5VTlk1TUk3RVBpaUFHNVR6SWQ0WnEzRAoyZm5VOUo4TnVSR251TDgxRTBmVnY3azVwalVtM2NMS2lWQ1lDdVZGZ21zQjFPb1hLN0lkcUxNPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo= +kind: Secret +metadata: + name: monitoring-client-certs + namespace: openshift-monitoring +type: kubernetes.io/tls \ No newline at end of file diff --git a/temp/deployment.yaml b/temp/deployment.yaml new file mode 100644 index 00000000..75a979b9 --- /dev/null +++ b/temp/deployment.yaml @@ -0,0 +1,75 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + k8s-app: monitoring-client + name: monitoring-client + namespace: open-cluster-management-monitoring +spec: + progressDeadlineSeconds: 600 + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + k8s-app: monitoring-client + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + type: RollingUpdate + template: + metadata: + labels: + k8s-app: monitoring-client + spec: + containers: + - command: + - /usr/bin/telemeter-client + - --id=$(ID) + - --from=$(FROM) + - --from-ca-file=/etc/serving-certs-ca-bundle/service-ca.crt + - --from-token-file=/var/run/secrets/kubernetes.io/serviceaccount/token + - --to=url + - --to-upload=$(TO) + - --listen=localhost:8080 + - --match={__name__="up"} + - --limit-bytes=5242880 + - --log-level=debug + - --verbose=true + env: + - name: ANONYMIZE_LABELS + - name: FROM + value: https://prometheus-k8s.openshift-monitoring.svc:9091 + - name: ID + value: 9d9de635-6a7a-4da3-91ae-7b584b7a84e3 + - name: TO + value: http://observatorium-api-open-cluster-management-monitoring.apps.marco.dev05.red-chesterfield.com/api/metrics/v1/write + - name: HTTP_PROXY + - name: HTTPS_PROXY + - name: NO_PROXY + image: blue0/telemeter:latest + imagePullPolicy: Always + name: telemeter-client + ports: + - containerPort: 8080 + name: http + protocol: TCP + resources: + requests: + cpu: 1m + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - name: certs + readOnly: true + mountPath: /etc/certs + - mountPath: /etc/serving-certs-ca-bundle + name: serving-certs-ca-bundle + readOnly: false + volumes: + - name: certs + secret: + secretName: client-certs + - configMap: + name: telemeter-client-serving-certs-ca-bundle + name: serving-certs-ca-bundle diff --git a/temp/rolebinding.yaml b/temp/rolebinding.yaml new file mode 100644 index 00000000..a4318293 --- /dev/null +++ b/temp/rolebinding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: test-client-view +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-monitoring-view +subjects: +- kind: ServiceAccount + name: default + namespace: open-cluster-management-monitoring \ No newline at end of file diff --git a/temp/telemeter-client-serving-certs-ca-bundle.yaml b/temp/telemeter-client-serving-certs-ca-bundle.yaml new file mode 100644 index 00000000..857d7364 --- /dev/null +++ b/temp/telemeter-client-serving-certs-ca-bundle.yaml @@ -0,0 +1,9 @@ +kind: ConfigMap +apiVersion: v1 +metadata: + name: telemeter-client-serving-certs-ca-bundle + namespace: open-cluster-management-monitoring + annotations: + service.alpha.openshift.io/inject-cabundle: 'true' +data: + service-ca.crt: "" diff --git a/test/benchmark.sh b/test/benchmark.sh new file mode 100755 index 00000000..68c24e5f --- /dev/null +++ b/test/benchmark.sh @@ -0,0 +1,126 @@ +#!/bin/bash +set -euo pipefail + +TS=${1:-./test/timeseries.txt} +SERVERS=${2:-10} +CLIENTS=${3:-1000} +DIR=${4:-benchmark} +GOAL=${5:-0} +TSN=$(grep -v '^#' -c "$TS") +echo "Running benchmarking test" + +rc= +trap 'rc=$?; printf "cleaning up...\n" && oc delete -f ./manifests/benchmark/ --ignore-not-found=true && oc delete namespace telemeter-benchmark --ignore-not-found=true && jobs -p | xargs -r kill; exit $rc' EXIT + +benchmark() { + local current=$1 + local goal=$2 + local success=0 + while [ "$goal" == 0 ] || [ "$success" -lt "$goal" ]; do + printf "benchmarking with %d clients sending %d time series\n" "$current" "$TSN" + create + client "$current" http://"$(route telemeter-server)" & + if ! check "$current" https://"$(route benchmark-thanos-query)"; then + break + fi + jobs -p | xargs -r kill + success=$current + current=$((current+500)) + done + printf "Successfully handled %s clients\n" "$success" + # Only return non-zero if we set a goal and didn't meet it. + if [ "$goal" -gt 0 ] && [ "$success" -lt "$goal" ]; then + return 1 + fi + return 0 +} + +route() { + oc get route --namespace=telemeter-benchmark "$1" --output jsonpath='{.spec.host}' +} + +create() { + printf "removing stale resources...\n" + oc delete -f ./manifests/benchmark/ --ignore-not-found=true > /dev/null && oc delete namespace telemeter-benchmark --ignore-not-found=true > /dev/null + printf "creating telemeter-server...\n" + oc create namespace telemeter-benchmark > /dev/null + # Create everything but the Thanos resources. + find ./manifests/benchmark/ ! -name '*Thanos*' -type f -print0 | xargs -0l -I{} oc apply -f {} > /dev/null + oc scale statefulset telemeter-server --namespace telemeter-benchmark --replicas "$SERVERS" + local retries=20 + until [ "$(oc get pods -n telemeter-benchmark | grep telemeter-server- | grep Running -c)" -eq "$SERVERS" ]; do + retries=$((retries-1)) + if [ $retries -eq 0 ]; then + printf "timed out waiting for telemeter-server to be up\n" + return 1 + fi + printf "waiting for telemeter-server to be ready; checking again in 10s...\n" + sleep 10 + done + printf "creating Thanos...\n" + # Create everything but the Telemeter server resources as we want + # to avoid undoing the scaling event. + find ./manifests/benchmark/ ! -name '*TelemeterServer.yaml' -type f -print0 | xargs -0l -I{} oc apply -f {} > /dev/null + local retries=20 + until [ "$(oc get pods -n telemeter-benchmark -l 'app.kubernetes.io/part-of=telemeter-benchmark' | grep Running -c)" -eq 5 ]; do + retries=$((retries-1)) + if [ $retries -eq 0 ]; then + printf "timed out waiting for Thanos to be up\n" + return 1 + fi + printf "waiting for Thanos to be ready; checking again in 10s...\n" + sleep 10 + done + printf "successfully created all resources\n" +} + +client() { + trap 'jobs -p | xargs -r kill' EXIT + local n=$1 + local url=$2 + ./telemeter-benchmark --workers="$n" --metrics-file="$TS" --to="$url" --to-token=benchmark --listen=localhost:8888 > /dev/null 2>&1 & + wait +} + +check() { + local n=$1 + local url=$2 + local checks=60 + while query "$url" ; do + printf "\tno scrape failures; " + checks=$((checks-1)) + if [ $checks -eq 0 ]; then + printf "successfully completed run!\n" + printf "PASS: telemeter-server handled %d clients\n" "$n" + save "$n" "$url" + return 0 + fi + printf "checking again in 15s...\n" + sleep 15 + done + printf "FAIL: telemeter-server failed check\n" + save "$n" "$url" + return 1 +} + +query() { + local url=$1 + local res + res=$(curl --fail --silent -G -k "$url"/api/v1/query --data-urlencode 'query=sum_over_time(count(up{job=~"telemeter-server.*"} == 0)[2w:])') + echo "$res" | jq --exit-status '.data.result | length == 0' > /dev/null +} + +save() { + local n=$1 + local url=$2 + local res + mkdir -p "$DIR" + res=$(curl --fail --silent -G -k "$url"/api/v1/query_range --data-urlencode "query=(irate(process_cpu_seconds_total[1m]) * 100)" --data-urlencode "start=$(date -d '1 hour ago' +%s)" --data-urlencode "end=$(date +%s)" --data-urlencode "step=1") + echo "$res" > "$DIR"/"$SERVERS"_"$n"_cpu.json + res=$(curl --fail --silent -G -k "$url"/api/v1/query_range --data-urlencode "query=process_resident_memory_bytes" --data-urlencode "start=$(date -d '1 hour ago' +%s)" --data-urlencode "end=$(date +%s)" --data-urlencode "step=1") + echo "$res" > "$DIR"/"$SERVERS"_"$n"_mem.json +} + +benchmark "$CLIENTS" "$GOAL" + +exit $? diff --git a/test/e2e/forward_test.go b/test/e2e/forward_test.go new file mode 100644 index 00000000..acc3e7c2 --- /dev/null +++ b/test/e2e/forward_test.go @@ -0,0 +1,189 @@ +package e2e + +import ( + "bytes" + "context" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/go-kit/kit/log" + "github.com/gogo/protobuf/proto" + "github.com/golang/snappy" + clientmodel "github.com/prometheus/client_model/go" + "github.com/prometheus/common/expfmt" + "github.com/prometheus/prometheus/prompb" + + "github.com/openshift/telemeter/pkg/authorize" + "github.com/openshift/telemeter/pkg/metricfamily" + "github.com/openshift/telemeter/pkg/server" +) + +const sampleMetrics = ` +up{cluster="test",job="test",label="value0"} 1 1562500000000 +up{cluster="test",job="test",label="value1"} 1 1562600000000 +up{cluster="test",job="test",label="value2"} 0 1562700000000 +` + +var expectedTimeSeries = []prompb.TimeSeries{ + { + Labels: []prompb.Label{ + {Name: "__name__", Value: "up"}, + {Name: "cluster", Value: "test"}, + {Name: "job", Value: "test"}, + {Name: "label", Value: "value0"}, + }, + Samples: []prompb.Sample{{Value: 1}}, + }, + { + Labels: []prompb.Label{ + {Name: "__name__", Value: "up"}, + {Name: "cluster", Value: "test"}, + {Name: "job", Value: "test"}, + {Name: "label", Value: "value1"}, + }, + Samples: []prompb.Sample{{Value: 1}}, + }, + { + Labels: []prompb.Label{ + {Name: "__name__", Value: "up"}, + {Name: "cluster", Value: "test"}, + {Name: "job", Value: "test"}, + {Name: "label", Value: "value2"}, + }, + Samples: []prompb.Sample{{Value: 0}}, + }, +} + +func TestForward(t *testing.T) { + var receiveServer *httptest.Server + { + // This is the receiveServer that the Telemeter Server is going to forward to + // upon receiving metrics itself. + receiveServer = httptest.NewServer(mockedReceiver(t)) + defer receiveServer.Close() + } + var telemeterServer *httptest.Server + { + logger := log.NewNopLogger() + + labels := map[string]string{"cluster": "test"} + + receiveURL, _ := url.Parse(receiveServer.URL) + + telemeterServer = httptest.NewServer( + fakeAuthorizeHandler( + server.ClusterID(log.NewNopLogger(), "cluster", + server.Ratelimit(log.NewNopLogger(), 4*time.Minute+30*time.Second, time.Now, + server.Snappy( + server.Validate(log.NewNopLogger(), metricfamily.MultiTransformer{}, 10*365*24*time.Hour, 500*1024, time.Now, + server.ForwardHandler(logger, receiveURL, "default-tenant", &http.Client{}), + ), + ), + ), + ), + &authorize.Client{ID: "test", Labels: labels}, + ), + ) + defer telemeterServer.Close() + } + + metricFamilies := readMetrics(sampleMetrics) + + buf := &bytes.Buffer{} + encoder := expfmt.NewEncoder(buf, expfmt.FmtProtoDelim) + for _, f := range metricFamilies { + if err := encoder.Encode(f); err != nil { + t.Fatalf("failed to encode metric family: %v", err) + } + } + + // The following code send the initial request to the Telemeter Server + // which then forwards the converted metrics as time series to the mocked receive server. + // In the end we check for a 200 OK status code. + + resp, err := http.Post(telemeterServer.URL, string(expfmt.FmtProtoDelim), buf) + if err != nil { + t.Errorf("failed sending the upload request: %v", err) + } + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + if resp.StatusCode/100 != 2 { + t.Errorf("request did not return 2xx, but %s: %s", resp.Status, string(body)) + } +} + +func readMetrics(m string) []*clientmodel.MetricFamily { + var families []*clientmodel.MetricFamily + + decoder := expfmt.NewDecoder(bytes.NewBufferString(m), expfmt.FmtText) + for { + family := clientmodel.MetricFamily{} + if err := decoder.Decode(&family); err != nil { + if err == io.EOF { + break + } + panic(err) + } + families = append(families, &family) + } + + return families +} + +func fakeAuthorizeHandler(h http.Handler, client *authorize.Client) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + req = req.WithContext(context.WithValue(req.Context(), authorize.TenantKey, client.ID)) + req = req.WithContext(authorize.WithClient(req.Context(), client)) + h.ServeHTTP(w, req) + }) +} + +// mockedReceiver unmarshalls the request body into prompb.WriteRequests +// and asserts the seeing contents against the pre-defined expectedTimeSeries from the top. +func mockedReceiver(t *testing.T) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("failed reading body from forward request: %v", err) + } + + reqBuf, err := snappy.Decode(nil, body) + if err != nil { + t.Errorf("failed to decode the snappy request: %v", err) + } + + var wreq prompb.WriteRequest + if err := proto.Unmarshal(reqBuf, &wreq); err != nil { + t.Errorf("failed to unmarshal WriteRequest: %v", err) + } + + tsc := len(wreq.Timeseries) + if tsc != 3 { + t.Errorf("expected 3 timeseries to be forwarded, got %d", tsc) + } + + for i, ts := range expectedTimeSeries { + for j, l := range ts.Labels { + wl := wreq.Timeseries[i].Labels[j] + if l.Name != wl.Name { + t.Errorf("expected label name %s, got %s", l.Name, wl.Name) + } + if l.Value != wl.Value { + t.Errorf("expected label value %s, got %s", l.Value, wl.Value) + } + } + for j, s := range ts.Samples { + ws := wreq.Timeseries[i].Samples[j] + if s.Value != ws.Value { + t.Errorf("expected value for sample %2.f, got %2.f", s.Value, ws.Value) + } + } + } + } +} diff --git a/test/e2e/receive_test.go b/test/e2e/receive_test.go new file mode 100644 index 00000000..5f61206e --- /dev/null +++ b/test/e2e/receive_test.go @@ -0,0 +1,204 @@ +package e2e + +import ( + "bytes" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-kit/kit/log" + "github.com/gogo/protobuf/proto" + "github.com/golang/snappy" + "github.com/openshift/telemeter/pkg/authorize" + "github.com/openshift/telemeter/pkg/receive" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/prometheus/prompb" +) + +func TestReceiveValidateLabels(t *testing.T) { + testcases := []struct { + Name string + Timeseries []prompb.TimeSeries + ExpectStatusCode int + }{ + { + Name: "NoLabels", + Timeseries: []prompb.TimeSeries{{ + Labels: []prompb.Label{}, + }}, + ExpectStatusCode: http.StatusBadRequest, + }, + { + Name: "MissingRequiredLabel", + Timeseries: []prompb.TimeSeries{{ + Labels: []prompb.Label{{Name: "foo", Value: "bar"}}, + }}, + ExpectStatusCode: http.StatusBadRequest, + }, + { + Name: "Valid", + Timeseries: []prompb.TimeSeries{{ + Labels: []prompb.Label{{Name: "__name__", Value: "foo"}}, + }}, + ExpectStatusCode: http.StatusOK, + }, + { + Name: "MultipleMissingRequiredLabel", + Timeseries: []prompb.TimeSeries{{ + Labels: []prompb.Label{{Name: "foo", Value: "bar"}}, + }, { + Labels: []prompb.Label{{Name: "bar", Value: "baz"}}, + }}, + ExpectStatusCode: http.StatusBadRequest, + }, + { + Name: "OneMultipleMissingRequiredLabel", + Timeseries: []prompb.TimeSeries{{ + Labels: []prompb.Label{{Name: "foo", Value: "bar"}}, + }, { + Labels: []prompb.Label{{Name: "__name__", Value: "foo"}}, + }}, + ExpectStatusCode: http.StatusBadRequest, + }, + { + Name: "MultipleValid", + Timeseries: []prompb.TimeSeries{{ + Labels: []prompb.Label{{Name: "__name__", Value: "foo"}}, + }, { + Labels: []prompb.Label{{Name: "__name__", Value: "bar"}}, + }}, + ExpectStatusCode: http.StatusOK, + }, + } + + var receiveServer *httptest.Server + { + receiveServer = httptest.NewServer(func() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) {} + }()) + defer receiveServer.Close() + } + + var telemeterServer *httptest.Server + { + receiver := receive.NewHandler(log.NewNopLogger(), receiveServer.URL, prometheus.NewRegistry(), "default-tenant") + + telemeterServer = httptest.NewServer( + fakeAuthorizeHandler( + receive.ValidateLabels( + log.NewNopLogger(), + http.HandlerFunc(receiver.Receive), + "__name__", + ), + &authorize.Client{ID: "test"}, + ), + ) + + defer telemeterServer.Close() + } + + for _, tc := range testcases { + t.Run(tc.Name, func(t *testing.T) { + wreq := &prompb.WriteRequest{Timeseries: tc.Timeseries} + data, err := proto.Marshal(wreq) + if err != nil { + t.Error("failed to marshal proto message") + } + compressed := snappy.Encode(nil, data) + + resp, err := http.Post(telemeterServer.URL+"/metrics/v1/receive", "", bytes.NewBuffer(compressed)) + if err != nil { + t.Error("failed to send the receive request: %w", err) + } + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + if resp.StatusCode != tc.ExpectStatusCode { + t.Errorf("request did not return %d, but %s: %s", tc.ExpectStatusCode, resp.Status, string(body)) + } + }) + } +} + +func TestLimitBodySize(t *testing.T) { + var receiveServer *httptest.Server + { + receiveServer = httptest.NewServer(func() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) {} + }()) + defer receiveServer.Close() + } + + var telemeterServer *httptest.Server + { + receiver := receive.NewHandler(log.NewNopLogger(), receiveServer.URL, prometheus.NewRegistry(), "default-tenant") + + telemeterServer = httptest.NewServer( + fakeAuthorizeHandler( + receive.LimitBodySize(receive.DefaultRequestLimit, + http.HandlerFunc(receiver.Receive), + ), + &authorize.Client{ID: "test"}, + ), + ) + defer telemeterServer.Close() + } + + // Test if request within limit is fine + { + ts := []prompb.TimeSeries{{ + Labels: []prompb.Label{{Name: "__name__", Value: "foo"}}, + }} + + wreq := &prompb.WriteRequest{Timeseries: ts} + data, err := proto.Marshal(wreq) + if err != nil { + t.Error("failed to marshal proto message") + } + compressed := snappy.Encode(nil, data) + + resp, err := http.Post(telemeterServer.URL+"/metrics/v1/receive", "", bytes.NewBuffer(compressed)) + if err != nil { + t.Error("failed to send the receive request: %w", err) + } + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + t.Errorf("request did not return 200, but %s: %s", resp.Status, string(body)) + } + } + // Test if too large request is rejected + { + var timeSeries []prompb.TimeSeries + for i := 0; i < 1000; i++ { + ts := prompb.TimeSeries{Labels: []prompb.Label{{Name: "__name__", Value: "foo" + string(i)}}} + for j := 0; j < i; j++ { + ts.Samples = append(ts.Samples, prompb.Sample{ + Value: float64(j), + Timestamp: int64(j), + }) + } + timeSeries = append(timeSeries, ts) + } + + wreq := &prompb.WriteRequest{Timeseries: timeSeries} + data, err := proto.Marshal(wreq) + if err != nil { + t.Error("failed to marshal proto message") + } + compressed := snappy.Encode(nil, data) + + resp, err := http.Post(telemeterServer.URL+"/metrics/v1/receive", "", bytes.NewBuffer(compressed)) + if err != nil { + t.Error("failed to send the receive request: %w", err) + } + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + if resp.StatusCode != http.StatusRequestEntityTooLarge { + t.Errorf("request did not return 413, but %s: %s", resp.Status, string(body)) + } + } +} diff --git a/test/integration-v2.sh b/test/integration-v2.sh new file mode 100755 index 00000000..a65e82ae --- /dev/null +++ b/test/integration-v2.sh @@ -0,0 +1,84 @@ +#!/bin/bash + +# Runs a semi-realistic integration test with one producer generating metrics, +# a telemeter server, a stub authorization server, a memcached instance, +# a thanos receive for ingestion, and a thanos query for querying the metrics. + +set -euo pipefail + +result=1 +trap 'kill $(jobs -p); exit $result' EXIT + +(./authorization-server localhost:9101 ./test/tokens.json) & + +(memcached -u "$(whoami)") & + +( + thanos receive \ + --tsdb.path="$(mktemp -d)" \ + --remote-write.address=127.0.0.1:9105 \ + --grpc-address=127.0.0.1:9106 \ + --http-address=127.0.0.1:9116 \ + --receive.default-tenant-id="FB870BF3-9F3A-44FF-9BF7-D7A047A52F43" +) & + +( + thanos query \ + --grpc-address=127.0.0.1:9107 \ + --http-address=127.0.0.1:9108 \ + --store=127.0.0.1:9106 +) & + +echo "telemeter: waiting for dependencies to come up..." +sleep 10 + +until curl --output /dev/null --silent --fail http://localhost:9116/-/ready; do + printf '.' + sleep 1 +done + +until curl --output /dev/null --silent --fail http://localhost:9108/-/ready; do + printf '.' + sleep 1 +done + +( + ./telemeter-server \ + --authorize http://localhost:9101 \ + --listen localhost:9103 \ + --listen-internal localhost:9104 \ + --forward-url=http://localhost:9105/api/v1/receive \ + --memcached=localhost:11211 \ + -v +) & + +echo "up: waiting for dependencies to come up..." + +until curl --output /dev/null --silent --fail http://localhost:9103/healthz/ready; do + printf '.' + sleep 1 +done + +if + up \ + --endpoint-type=metrics \ + --endpoint-write=http://127.0.0.1:9103/metrics/v1/receive \ + --endpoint-read=http://127.0.0.1:9108/api/v1/query \ + --period=500ms \ + --initial-query-delay=250ms \ + --threshold=1 \ + --latency=10s \ + --duration=10s \ + --log.level=debug \ + --name cluster_installer \ + --labels '_id="test"' \ + --token="$(echo '{"authorization_token":"a","cluster_id":"test"}' | base64)" +then + result=0 + echo "tests: ok" + exit 0 +fi + +echo "tests: failed" 1>&2 +result=1 +exit 1 diff --git a/test/integration.sh b/test/integration.sh new file mode 100755 index 00000000..186b3598 --- /dev/null +++ b/test/integration.sh @@ -0,0 +1,99 @@ +#!/bin/bash + +# Runs a semi-realistic integration test with two servers, a stub authorization server, a +# prometheus that scrapes from them, and a single client that fetches "cluster" metrics. +# If no arguments are passed an integration test scenario is run. Otherwise $1 becomes +# the upstream prometheus server to test against and $2 is an optional bearer token to +# authenticate the request. + +set -euo pipefail + +result=1 +trap 'kill $(jobs -p); exit $result' EXIT + +( ./authorization-server localhost:9001 ./test/tokens.json ) & + +( prometheus --config.file=./test/prom-local.conf --web.listen-address=localhost:9090 "--storage.tsdb.path=$(mktemp -d)" --log.level=warn ) & + +( + sleep 5 + exec ./telemeter-client \ + --from "http://localhost:9090" \ + --to "http://localhost:9003" \ + --id "test" \ + --to-token a \ + --interval 15s \ + --anonymize-labels "instance" --anonymize-salt "a-unique-value" \ + --rename ALERTS=alerts --rename openshift_build_info=build_info --rename scrape_samples_scraped=scraped \ + --match '{__name__="ALERTS",alertstate="firing"}' \ + --match '{__name__="scrape_samples_scraped"}' +) & + +( +./telemeter-server \ + --ratelimit=15s \ + --authorize http://localhost:9001 \ + --shared-key=test/test.key \ + --listen localhost:9003 \ + --listen-internal localhost:9004 \ + --whitelist '{_id="test"}' \ + --elide-label '_elide' \ + --forward-url=http://localhost:9005/api/v1/receive \ + -v +) & + +( +thanos receive \ + --tsdb.path="$(mktemp -d)" \ + --remote-write.address=127.0.0.1:9005 \ + --grpc-address=127.0.0.1:9006 +) & + +( +thanos query \ + --grpc-address=127.0.0.1:9007 \ + --http-address=127.0.0.1:9008 \ + --store=127.0.0.1:9006 +) & + +sleep 1 + +retries=100 +while true; do + if [[ "${retries}" -lt 0 ]]; then + echo "error: Did not successfully retrieve cluster metrics from the local Prometheus server" 1>&2 + exit 1 + fi + # verify we scrape metrics from the test cluster and give it _id test + if [[ "$( curl http://localhost:9008/api/v1/query --data-urlencode 'query=count({_id="test"})' -G 2>/dev/null | python3 -c 'import sys, json; print(json.load(sys.stdin)["data"]["result"][0]["value"][1])' 2>/dev/null )" -eq 0 ]]; then + retries=$((retries-1)) + sleep 1 + continue + fi + # verify we rename scrape_samples_scraped to scraped + if [[ "$( curl http://localhost:9008/api/v1/query --data-urlencode 'query=count(scraped{_id="test"})' -G 2>/dev/null | python3 -c 'import sys, json; print(json.load(sys.stdin)["data"]["result"][0]["value"][1])' 2>/dev/null )" -eq 0 ]]; then + retries=$((retries-1)) + sleep 1 + continue + fi + # verify we got alerts as remapped from ALERTS + if [[ "$( curl http://localhost:9008/api/v1/query --data-urlencode 'query=count(alerts{_id="test"})' -G 2>/dev/null | python3 -c 'import sys, json; print(json.load(sys.stdin)["data"]["result"][0]["value"][1])' 2>/dev/null )" -eq 0 ]]; then + retries=$((retries-1)) + sleep 1 + continue + fi + # verify we don't get elided labels + if [[ "$( curl http://localhost:9008/api/v1/query --data-urlencode 'query=count(alerts{_id="test",_elide=~".+"})' -G 2>/dev/null | python3 -c 'import sys, json; print(len(json.load(sys.stdin)["data"]["result"]))' 2>/dev/null )" -gt 0 ]]; then + retries=$((retries-1)) + sleep 1 + continue + fi + break +done +echo "tests: ok" +result=0 +exit 0 + +echo "tests: failed" 1>&2 +result=1 +exit 1 diff --git a/test/plot.py b/test/plot.py new file mode 100644 index 00000000..5b93fb5b --- /dev/null +++ b/test/plot.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 + +import matplotlib.pyplot as plt +import json +import sys +import os + + +d = os.path.dirname(sys.argv[1]) +name = os.path.basename(sys.argv[1]) +if name.endswith('.json'): + name = name[:-5] +else: + sys.exit("expected a JSON file") + +parts = name.split('_') +ns = parts[0] +nc = parts[1] +metric = parts[2].upper() + +divisor = 1 +label = metric +if metric == "MEM": + divisor = 1024*1024 + label = label + " (MB)" +else: + label = label + " (%)" + +j = {} +with open(sys.argv[1]) as f: + j = json.load(f) + +min_x = -1 +results = [] +for r in j['data']['result']: + result = {} + x = [] + y = [] + for v in r['values']: + x.append(v[0]) + y.append(float(v[1])/divisor) + result['x'] = x + result['y'] = y + result['name'] = r['metric']['pod'] + results.append(result) + if (min_x == -1) or (x[0] < min_x): + min_x = x[0] + +for r in results: + r['x'][:] = [x - min_x for x in r['x']] + for x in r['x']: + x = x - min_x + plt.plot(r['x'], r['y'], label=r['name']) + +plt.legend(loc=2, ncol=2) + +# Add titles +plt.title(f"{metric} Utilization for {nc} Clients and {ns} Replicas") +plt.xlabel("Time (s)") +plt.ylabel(label) + +plt.savefig(f"{d}/{name}.pdf", format="pdf") diff --git a/test/prom-local-alerts.conf b/test/prom-local-alerts.conf new file mode 100644 index 00000000..6a5228bc --- /dev/null +++ b/test/prom-local-alerts.conf @@ -0,0 +1,7 @@ +groups: +- name: basic + rules: + - alert: DeadMansSwitch + expr: vector(1) + labels: + severity: none \ No newline at end of file diff --git a/test/prom-local.conf b/test/prom-local.conf new file mode 100644 index 00000000..eab1a547 --- /dev/null +++ b/test/prom-local.conf @@ -0,0 +1,15 @@ +rule_files: +- prom-local-alerts.conf + +scrape_configs: +- job_name: 'telemeter-server' + scrape_interval: 30s + + metrics_path: '/metrics' + + static_configs: + - targets: + - 'localhost:9004' + + labels: + _elide: 'integration-test' diff --git a/test/test.key b/test/test.key new file mode 100644 index 00000000..771d66aa --- /dev/null +++ b/test/test.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIMlXlC/Li9rTE0jkGf9tFHdoNjxJeqfdu6174mfpMqgmoAoGCCqGSM49 +AwEHoUQDQgAErm0H0AyXo0YEboDqDr3EeQi2cGGQB+V0vZ4b8oCNgyh8zf6+JL+b +zCSKH8mWW0N3IWR6Y9lTuClU2Fr/yV/rgQ== +-----END EC PRIVATE KEY----- diff --git a/test/timeseries.txt b/test/timeseries.txt new file mode 100644 index 00000000..58c2f0db --- /dev/null +++ b/test/timeseries.txt @@ -0,0 +1,258 @@ +# This file was generated using `make test/timeseries.txt`. +# TYPE ALERTS untyped +ALERTS{alertname="Watchdog",alertstate="firing",severity="none",instance="",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168610163 +# TYPE cluster:capacity_cpu_cores:sum untyped +cluster:capacity_cpu_cores:sum{label_beta_kubernetes_io_instance_type="m5.large",instance="",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 6 1562168616662 +cluster:capacity_cpu_cores:sum{label_beta_kubernetes_io_instance_type="m5.xlarge",label_node_role_kubernetes_io="master",instance="",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 12 1562168616662 +# TYPE cluster:capacity_memory_bytes:sum untyped +cluster:capacity_memory_bytes:sum{label_beta_kubernetes_io_instance_type="m5.large",instance="",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 2.41496064e+10 1562168616662 +cluster:capacity_memory_bytes:sum{label_beta_kubernetes_io_instance_type="m5.xlarge",label_node_role_kubernetes_io="master",instance="",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 4.8987103232e+10 1562168616662 +# TYPE cluster:cpu_usage_cores:sum untyped +cluster:cpu_usage_cores:sum{instance="",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1.6920000000000686 1562168616662 +# TYPE cluster:memory_usage_bytes:sum untyped +cluster:memory_usage_bytes:sum{instance="",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1.4823510016e+10 1562168616662 +# TYPE cluster:node_instance_type_count:sum untyped +cluster:node_instance_type_count:sum{label_beta_kubernetes_io_instance_type="m5.large",instance="",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 3 1562168616662 +cluster:node_instance_type_count:sum{label_beta_kubernetes_io_instance_type="m5.xlarge",label_node_role_kubernetes_io="master",instance="",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 3 1562168616662 +# TYPE cluster_installer untyped +cluster_installer{endpoint="metrics",instance="10.0.150.196:9099",invoker="alex",job="cluster-version-operator",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",type="openshift-install",version="unreleased-master-1209-gfd08f44181f2111486749e2fb38399088f315cfb",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +# TYPE cluster_operator_conditions untyped +cluster_operator_conditions{condition="Available",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="operator-lifecycle-manager-catalog",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Available",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="cloud-credential",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Available",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="cluster-autoscaler",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Available",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="console",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Available",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="dns",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Available",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="image-registry",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="Ready",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Available",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="ingress",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Available",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="kube-apiserver",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Available",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="kube-controller-manager",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Available",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="kube-scheduler",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Available",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="machine-api",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Available",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="machine-config",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Available",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="marketplace",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Available",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="monitoring",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Available",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="network",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Available",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="node-tuning",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Available",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="openshift-apiserver",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Available",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="authentication",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Available",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="openshift-samples",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Available",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="operator-lifecycle-manager",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Available",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="openshift-controller-manager",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Available",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="operator-lifecycle-manager-packageserver",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Available",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="service-ca",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Available",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="service-catalog-apiserver",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Available",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="service-catalog-controller-manager",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Available",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="storage",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Available",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="support",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Degraded",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="authentication",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Degraded",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="cloud-credential",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="NoCredentialsFailing",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Degraded",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="cluster-autoscaler",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Degraded",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="console",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Degraded",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="dns",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Degraded",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="image-registry",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Degraded",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="ingress",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Degraded",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="kube-apiserver",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Degraded",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="kube-controller-manager",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Degraded",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="kube-scheduler",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Degraded",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="machine-api",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Degraded",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="machine-config",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Degraded",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="marketplace",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Degraded",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="monitoring",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Degraded",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="network",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Degraded",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="node-tuning",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Degraded",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="openshift-apiserver",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Degraded",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="openshift-controller-manager",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Degraded",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="openshift-samples",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Degraded",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="operator-lifecycle-manager",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Degraded",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="operator-lifecycle-manager-catalog",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Degraded",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="operator-lifecycle-manager-packageserver",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Degraded",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="service-ca",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Degraded",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="service-catalog-apiserver",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Degraded",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="service-catalog-controller-manager",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Upgradeable",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="kube-scheduler",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Degraded",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="support",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Disabled",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="support",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="Disabled",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Progressing",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="authentication",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Progressing",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="cloud-credential",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="ReconcilingComplete",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Progressing",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="cluster-autoscaler",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Progressing",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="console",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Progressing",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="dns",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Progressing",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="image-registry",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="Ready",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Progressing",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="ingress",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Progressing",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="kube-apiserver",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Progressing",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="kube-controller-manager",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Progressing",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="kube-scheduler",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Progressing",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="machine-api",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Progressing",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="machine-config",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Progressing",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="marketplace",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Progressing",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="monitoring",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Progressing",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="network",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Progressing",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="node-tuning",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Progressing",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="openshift-apiserver",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Progressing",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="openshift-controller-manager",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Progressing",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="openshift-samples",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Progressing",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="operator-lifecycle-manager",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Progressing",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="operator-lifecycle-manager-catalog",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Progressing",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="operator-lifecycle-manager-packageserver",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Progressing",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="service-ca",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Progressing",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="service-catalog-apiserver",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Progressing",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="service-catalog-controller-manager",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Progressing",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="storage",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Progressing",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="support",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="Disabled",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Upgradeable",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="authentication",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Upgradeable",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="console",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Upgradeable",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="kube-apiserver",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Upgradeable",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="kube-controller-manager",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_conditions{condition="Degraded",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="storage",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +cluster_operator_conditions{condition="Upgradeable",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="openshift-apiserver",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",reason="AsExpected",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +# TYPE cluster_operator_up untyped +cluster_operator_up{endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="authentication",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",version="4.2.0-0.okd-2019-07-03-073817_openshift",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_up{endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="cloud-credential",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_up{endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="cluster-autoscaler",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_up{endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="console",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_up{endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="dns",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_up{endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="image-registry",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_up{endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="ingress",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_up{endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="kube-apiserver",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_up{endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="kube-controller-manager",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_up{endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="kube-scheduler",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_up{endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="storage",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_up{endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="machine-config",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_up{endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="support",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_up{endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="monitoring",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_up{endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="network",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_up{endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="node-tuning",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_up{endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="openshift-apiserver",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_up{endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="openshift-controller-manager",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_up{endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="openshift-samples",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_up{endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="operator-lifecycle-manager",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_up{endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="operator-lifecycle-manager-catalog",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_up{endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="operator-lifecycle-manager-packageserver",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_up{endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="service-ca",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_up{endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="service-catalog-apiserver",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_up{endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="service-catalog-controller-manager",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_up{endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="machine-api",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +cluster_operator_up{endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",name="marketplace",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +# TYPE cluster_version untyped +cluster_version{endpoint="metrics",from_version="4.2.0-0.okd-2019-07-03-073817",image="quay.io/crawford/openshift-release@sha256:6a262d69ab8a4013aee6f964d30cc9dccf69a4ed6583b03098fb5fa4460f0a9a",instance="10.0.150.196:9099",job="cluster-version-operator",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",type="cluster",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1.562147327e+09 1562168623759 +cluster_version{endpoint="metrics",from_version="4.2.0-0.okd-2019-07-03-073817",image="quay.io/crawford/openshift-release@sha256:6a262d69ab8a4013aee6f964d30cc9dccf69a4ed6583b03098fb5fa4460f0a9a",instance="10.0.150.196:9099",job="cluster-version-operator",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",type="current",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1.562145933e+09 1562168623759 +cluster_version{endpoint="metrics",image="quay.io/crawford/openshift-release@sha256:6a262d69ab8a4013aee6f964d30cc9dccf69a4ed6583b03098fb5fa4460f0a9a",instance="10.0.150.196:9099",job="cluster-version-operator",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",type="completed",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1.562148306e+09 1562168623759 +cluster_version{endpoint="metrics",image="quay.io/crawford/openshift-release@sha256:6a262d69ab8a4013aee6f964d30cc9dccf69a4ed6583b03098fb5fa4460f0a9a",instance="10.0.150.196:9099",job="cluster-version-operator",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",type="initial",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1.562147327e+09 1562168623759 +# TYPE cluster_version_available_updates untyped +cluster_version_available_updates{channel="stable-4.2",endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",upstream="https://api.openshift.com/api/upgrades_info/v1/graph",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +# TYPE cluster_version_payload untyped +cluster_version_payload{endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",type="applied",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 399 1562168623759 +cluster_version_payload{endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",type="pending",version="4.2.0-0.okd-2019-07-03-073817",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0 1562168623759 +# TYPE code:apiserver_request_count:rate:sum untyped +code:apiserver_request_count:rate:sum{code="200",instance="",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 33.082456140350565 1562168623634 +code:apiserver_request_count:rate:sum{code="201",instance="",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 4.231578947368422 1562168623634 +code:apiserver_request_count:rate:sum{code="404",instance="",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0.5087719298245614 1562168623634 +code:apiserver_request_count:rate:sum{code="409",instance="",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 0.21403508771929822 1562168623634 +# TYPE node_uname_info gauge +node_uname_info{domainname="(none)",endpoint="https",instance="10.0.141.152:9100",job="node-exporter",machine="x86_64",namespace="openshift-monitoring",nodename="ip-10-0-141-152",pod="node-exporter-ncdgg",release="4.18.0-80.1.2.el8_0.x86_64",service="node-exporter",sysname="Linux",version="#1 SMP Sun Apr 28 09:21:22 UTC 2019"} 1 +node_uname_info{domainname="(none)",endpoint="https",instance="10.0.141.66:9100",job="node-exporter",machine="x86_64",namespace="openshift-monitoring",nodename="ip-10-0-141-66",pod="node-exporter-7rzzv",release="4.18.0-80.1.2.el8_0.x86_64",service="node-exporter",sysname="Linux",version="#1 SMP Sun Apr 28 09:21:22 UTC 2019"} 1 +node_uname_info{domainname="(none)",endpoint="https",instance="10.0.148.170:9100",job="node-exporter",machine="x86_64",namespace="openshift-monitoring",nodename="ip-10-0-148-170",pod="node-exporter-q8h8m",release="4.18.0-80.1.2.el8_0.x86_64",service="node-exporter",sysname="Linux",version="#1 SMP Sun Apr 28 09:21:22 UTC 2019"} 1 +node_uname_info{domainname="(none)",endpoint="https",instance="10.0.149.139:9100",job="node-exporter",machine="x86_64",namespace="openshift-monitoring",nodename="ip-10-0-149-139",pod="node-exporter-r9vr2",release="4.18.0-80.1.2.el8_0.x86_64",service="node-exporter",sysname="Linux",version="#1 SMP Sun Apr 28 09:21:22 UTC 2019"} 1 +node_uname_info{domainname="(none)",endpoint="https",instance="10.0.172.222:9100",job="node-exporter",machine="x86_64",namespace="openshift-monitoring",nodename="ip-10-0-172-222",pod="node-exporter-75p8p",release="4.18.0-80.1.2.el8_0.x86_64",service="node-exporter",sysname="Linux",version="#1 SMP Sun Apr 28 09:21:22 UTC 2019"} 1 +node_uname_info{domainname="(none)",endpoint="https",instance="10.0.173.13:9100",job="node-exporter",machine="x86_64",namespace="openshift-monitoring",nodename="ip-10-0-173-13",pod="node-exporter-gxh8t",release="4.18.0-80.1.2.el8_0.x86_64",service="node-exporter",sysname="Linux",version="#1 SMP Sun Apr 28 09:21:22 UTC 2019"} 1 +# TYPE instance:etcd_object_counts:sum untyped +instance:etcd_object_counts:sum{instance="10.0.131.31:6443",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 2646 1562168616662 +instance:etcd_object_counts:sum{instance="10.0.150.196:6443",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 2818 1562168616662 +instance:etcd_object_counts:sum{instance="10.0.166.205:6443",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 2775 1562168616662 +instance:etcd_object_counts:sum{instance="10.128.0.31:8443",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 515 1562168616662 +instance:etcd_object_counts:sum{instance="10.129.0.26:8443",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 515 1562168616662 +instance:etcd_object_counts:sum{instance="10.130.0.34:8443",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 515 1562168616662 +# TYPE kube_pod_status_ready:etcd:sum untyped +kube_pod_status_ready:etcd:sum{condition="true",instance="",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 3 1562168623634 +# TYPE kube_pod_status_ready:image_registry:sum untyped +kube_pod_status_ready:image_registry:sum{condition="true",instance="",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623634 +# TYPE openshift:cpu_usage_cores:sum untyped +openshift:cpu_usage_cores:sum{instance="",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1.7143732519333255 1562168616662 +# TYPE openshift:memory_usage_bytes:sum untyped +openshift:memory_usage_bytes:sum{instance="",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1.8395660288e+10 1562168616662 +# TYPE job:noobaa_bucket_count:sum untyped +job:noobaa_bucket_count:sum{instance="",prometheus="ocsdemo/example",prometheus_replica="prometheus-example-0"} 4 1564496667435 +# TYPE job:noobaa_total_object_count:sum untyped +job:noobaa_total_object_count:sum{instance="",prometheus="ocsdemo/example",prometheus_replica="prometheus-example-0"} 10 1564496667435 +# TYPE job:noobaa_total_unhealthy_buckets:sum untyped +job:noobaa_total_unhealthy_buckets:sum{instance="",prometheus="ocsdemo/example",prometheus_replica="prometheus-example-0"} 1 1564496667435 +# TYPE noobaa_accounts_num untyped +noobaa_accounts_num{endpoint="mgmt",instance="10.131.1.141:8080",job="noobaa-mgmt",namespace="ocsdemo",pod="noobaa-server-0",service="noobaa-mgmt",prometheus="ocsdemo/example",prometheus_replica="prometheus-example-0"} 3 1564496667435 +# TYPE noobaa_total_usage untyped +noobaa_total_usage{endpoint="mgmt",instance="10.131.1.141:8080",job="noobaa-mgmt",namespace="ocsdemo",pod="noobaa-server-0",service="noobaa-mgmt",prometheus="ocsdemo/example",prometheus_replica="prometheus-example-0"} 593096 1564496667435 +# TYPE up untyped +up{endpoint="mgmt",instance="10.131.1.141:8080",job="noobaa-mgmt",namespace="ocsdemo",pod="noobaa-server-0",service="noobaa-mgmt",prometheus="ocsdemo/example",prometheus_replica="prometheus-example-0"} 1 1564496664533 +up{endpoint="https",instance="10.0.169.146:9100",job="node-exporter",namespace="openshift-monitoring",pod="node-exporter-w9fjk",service="node-exporter",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168612460 +up{endpoint="crio",instance="10.0.131.31:9537",job="crio",namespace="kube-system",node="ip-10-0-131-31.us-west-2.compute.internal",service="kubelet",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168616576 +up{endpoint="crio",instance="10.0.139.97:9537",job="crio",namespace="kube-system",node="ip-10-0-139-97.us-west-2.compute.internal",service="kubelet",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168629557 +up{endpoint="crio",instance="10.0.150.196:9537",job="crio",namespace="kube-system",node="ip-10-0-150-196.us-west-2.compute.internal",service="kubelet",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168610236 +up{endpoint="crio",instance="10.0.153.63:9537",job="crio",namespace="kube-system",node="ip-10-0-153-63.us-west-2.compute.internal",service="kubelet",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168630433 +up{endpoint="crio",instance="10.0.166.205:9537",job="crio",namespace="kube-system",node="ip-10-0-166-205.us-west-2.compute.internal",service="kubelet",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168621314 +up{endpoint="crio",instance="10.0.169.146:9537",job="crio",namespace="kube-system",node="ip-10-0-169-146.us-west-2.compute.internal",service="kubelet",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168613115 +up{endpoint="etcd-metrics",instance="10.0.131.31:9979",job="etcd",namespace="openshift-etcd",pod="etcd-member-ip-10-0-131-31.us-west-2.compute.internal",service="etcd",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168617855 +up{endpoint="etcd-metrics",instance="10.0.150.196:9979",job="etcd",namespace="openshift-etcd",pod="etcd-member-ip-10-0-150-196.us-west-2.compute.internal",service="etcd",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168628148 +up{endpoint="etcd-metrics",instance="10.0.166.205:9979",job="etcd",namespace="openshift-etcd",pod="etcd-member-ip-10-0-166-205.us-west-2.compute.internal",service="etcd",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168637138 +up{endpoint="http",instance="10.129.0.23:8080",job="cluster-monitoring-operator",namespace="openshift-monitoring",pod="cluster-monitoring-operator-758fc6d995-5vs7k",service="cluster-monitoring-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168614989 +up{endpoint="http",instance="10.129.2.5:8080",job="prometheus-operator",namespace="openshift-monitoring",pod="prometheus-operator-6859c6f7b8-4z9gp",service="prometheus-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168637095 +up{endpoint="https",instance="10.0.131.31:10257",job="kube-controller-manager",namespace="openshift-kube-controller-manager",pod="kube-controller-manager-ip-10-0-131-31.us-west-2.compute.internal",service="kube-controller-manager",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168619982 +up{endpoint="https",instance="10.0.131.31:10259",job="scheduler",namespace="openshift-kube-scheduler",pod="openshift-kube-scheduler-ip-10-0-131-31.us-west-2.compute.internal",service="scheduler",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168620884 +up{endpoint="https",instance="10.0.131.31:6443",job="apiserver",namespace="default",service="kubernetes",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168627368 +up{endpoint="https",instance="10.0.131.31:9100",job="node-exporter",namespace="openshift-monitoring",pod="node-exporter-sph4k",service="node-exporter",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168625122 +up{endpoint="https",instance="10.0.139.97:9100",job="node-exporter",namespace="openshift-monitoring",pod="node-exporter-vqtjg",service="node-exporter",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168638862 +up{endpoint="https",instance="10.0.150.196:10257",job="kube-controller-manager",namespace="openshift-kube-controller-manager",pod="kube-controller-manager-ip-10-0-150-196.us-west-2.compute.internal",service="kube-controller-manager",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168620415 +up{endpoint="https",instance="10.0.150.196:10259",job="scheduler",namespace="openshift-kube-scheduler",pod="openshift-kube-scheduler-ip-10-0-150-196.us-west-2.compute.internal",service="scheduler",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168628690 +up{endpoint="https",instance="10.0.150.196:6443",job="apiserver",namespace="default",service="kubernetes",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168619534 +up{endpoint="https",instance="10.0.150.196:9100",job="node-exporter",namespace="openshift-monitoring",pod="node-exporter-g4nlq",service="node-exporter",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168619465 +up{endpoint="https",instance="10.0.153.63:9100",job="node-exporter",namespace="openshift-monitoring",pod="node-exporter-m9r76",service="node-exporter",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168632421 +up{endpoint="https",instance="10.0.166.205:10257",job="kube-controller-manager",namespace="openshift-kube-controller-manager",pod="kube-controller-manager-ip-10-0-166-205.us-west-2.compute.internal",service="kube-controller-manager",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168619024 +up{endpoint="https",instance="10.0.166.205:10259",job="scheduler",namespace="openshift-kube-scheduler",pod="openshift-kube-scheduler-ip-10-0-166-205.us-west-2.compute.internal",service="scheduler",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168634015 +up{endpoint="https",instance="10.0.166.205:6443",job="apiserver",namespace="default",service="kubernetes",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168627766 +up{endpoint="https",instance="10.0.166.205:9100",job="node-exporter",namespace="openshift-monitoring",pod="node-exporter-mrc4f",service="node-exporter",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168629550 +up{endpoint="5000-tcp",instance="10.131.0.5:5000",job="image-registry",namespace="openshift-image-registry",pod="image-registry-6fb849db55-457xq",service="image-registry",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168610834 +up{endpoint="https",instance="10.128.0.10:8443",job="metrics",namespace="openshift-kube-controller-manager-operator",pod="kube-controller-manager-operator-565bbd4d87-mdw4h",service="metrics",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168618120 +up{endpoint="https",instance="10.128.0.31:8443",job="api",namespace="openshift-apiserver",pod="apiserver-vmsv9",service="api",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168613252 +up{endpoint="https",instance="10.128.0.33:8443",job="controller-manager",namespace="openshift-controller-manager",pod="controller-manager-644kj",service="controller-manager",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168611038 +up{endpoint="https",instance="10.128.0.4:8443",job="metrics",namespace="openshift-kube-apiserver-operator",pod="kube-apiserver-operator-97b9d7789-p5njb",service="metrics",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168625423 +up{endpoint="https",instance="10.128.0.6:8443",job="metrics",namespace="openshift-controller-manager-operator",pod="openshift-controller-manager-operator-84bb5b4c8-579sr",service="metrics",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168616570 +up{endpoint="https",instance="10.128.0.9:8443",job="metrics",namespace="openshift-apiserver-operator",pod="openshift-apiserver-operator-585b7cdffb-5dqz6",service="metrics",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168629658 +up{endpoint="https",instance="10.128.2.4:3000",job="grafana",namespace="openshift-monitoring",pod="grafana-66b94d49dc-g9lrx",service="grafana",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168630842 +up{endpoint="https",instance="10.129.0.26:8443",job="api",namespace="openshift-apiserver",pod="apiserver-zpsjj",service="api",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168624750 +up{endpoint="https",instance="10.129.0.29:8443",job="controller-manager",namespace="openshift-controller-manager",pod="controller-manager-l5slz",service="controller-manager",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168608984 +up{endpoint="https",instance="10.129.0.33:6443",job="oauth-openshift",namespace="openshift-authentication",pod="oauth-openshift-5d7fddfd46-7zzpc",service="oauth-openshift",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168630484 +up{endpoint="https",instance="10.130.0.14:8443",job="metrics",namespace="openshift-service-catalog-controller-manager-operator",pod="openshift-service-catalog-controller-manager-operator-5f7c4nqhb",service="metrics",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168625328 +up{endpoint="https",instance="10.130.0.15:8443",job="metrics",namespace="openshift-authentication-operator",pod="authentication-operator-748b6896f4-ggn2k",service="metrics",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168631800 +up{endpoint="https",instance="10.130.0.34:8443",job="api",namespace="openshift-apiserver",pod="apiserver-86l6c",service="api",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168636756 +up{endpoint="https",instance="10.130.0.38:8443",job="controller-manager",namespace="openshift-controller-manager",pod="controller-manager-dxqkv",service="controller-manager",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168637239 +up{endpoint="https",instance="10.130.0.40:6443",job="oauth-openshift",namespace="openshift-authentication",pod="oauth-openshift-5d7fddfd46-dk8t2",service="oauth-openshift",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168629611 +up{endpoint="https",instance="10.131.0.6:8443",job="telemeter-client",namespace="openshift-monitoring",pod="telemeter-client-7864f6d769-kpbjm",service="telemeter-client",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168625392 +up{endpoint="https-main",instance="10.131.0.10:8443",job="kube-state-metrics",namespace="openshift-monitoring",pod="kube-state-metrics-7587dcb8d7-b6cbf",service="kube-state-metrics",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168608463 +up{endpoint="https-metrics",instance="10.0.131.31:10250",job="kubelet",namespace="kube-system",node="ip-10-0-131-31.us-west-2.compute.internal",service="kubelet",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168637805 +up{endpoint="https-metrics",instance="10.0.139.97:10250",job="kubelet",namespace="kube-system",node="ip-10-0-139-97.us-west-2.compute.internal",service="kubelet",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168618792 +up{endpoint="https-metrics",instance="10.0.150.196:10250",job="kubelet",namespace="kube-system",node="ip-10-0-150-196.us-west-2.compute.internal",service="kubelet",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168631448 +up{endpoint="https-metrics",instance="10.0.153.63:10250",job="kubelet",namespace="kube-system",node="ip-10-0-153-63.us-west-2.compute.internal",service="kubelet",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168636839 +up{endpoint="https-metrics",instance="10.0.166.205:10250",job="kubelet",namespace="kube-system",node="ip-10-0-166-205.us-west-2.compute.internal",service="kubelet",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168634127 +up{endpoint="https-metrics",instance="10.0.169.146:10250",job="kubelet",namespace="kube-system",node="ip-10-0-169-146.us-west-2.compute.internal",service="kubelet",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168632428 +up{endpoint="https-metrics",instance="10.128.0.13:8081",job="catalog-operator-metrics",namespace="openshift-operator-lifecycle-manager",pod="catalog-operator-59f94675db-k6rzp",service="catalog-operator-metrics",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168610951 +up{endpoint="https-metrics",instance="10.128.0.14:8081",job="olm-operator-metrics",namespace="openshift-operator-lifecycle-manager",pod="olm-operator-664fd645b7-khm5n",service="olm-operator-metrics",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168626165 +up{endpoint="https-self",instance="10.131.0.10:9443",job="kube-state-metrics",namespace="openshift-monitoring",pod="kube-state-metrics-7587dcb8d7-b6cbf",service="kube-state-metrics",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168626791 +up{endpoint="metrics",instance="10.0.131.31:9101",job="sdn",namespace="openshift-sdn",pod="sdn-cbw5n",service="sdn",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168633759 +up{endpoint="metrics",instance="10.0.139.97:9101",job="sdn",namespace="openshift-sdn",pod="sdn-j72jt",service="sdn",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168616210 +up{endpoint="metrics",instance="10.0.150.196:9099",job="cluster-version-operator",namespace="openshift-cluster-version",pod="cluster-version-operator-5cb5f8c9db-st825",service="cluster-version-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168623759 +up{endpoint="metrics",instance="10.0.150.196:9101",job="sdn",namespace="openshift-sdn",pod="sdn-lwwmn",service="sdn",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168618425 +up{endpoint="metrics",instance="10.0.153.63:9101",job="sdn",namespace="openshift-sdn",pod="sdn-wmznn",service="sdn",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168614502 +up{endpoint="metrics",instance="10.0.166.205:9101",job="sdn",namespace="openshift-sdn",pod="sdn-w8m6v",service="sdn",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168614790 +up{endpoint="metrics",instance="10.0.169.146:9101",job="sdn",namespace="openshift-sdn",pod="sdn-wlvd4",service="sdn",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168625620 +up{endpoint="metrics",instance="10.128.0.11:9153",job="dns-default",namespace="openshift-dns",pod="dns-default-lxblt",service="dns-default",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168638006 +up{endpoint="metrics",instance="10.128.2.2:9153",job="dns-default",namespace="openshift-dns",pod="dns-default-4f9f6",service="dns-default",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168620308 +up{endpoint="metrics",instance="10.129.0.10:8080",job="cluster-autoscaler-operator",namespace="openshift-machine-api",pod="cluster-autoscaler-operator-758f875f98-7nd5b",service="cluster-autoscaler-operator",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168614931 +up{endpoint="metrics",instance="10.129.0.4:9153",job="dns-default",namespace="openshift-dns",pod="dns-default-lms8m",service="dns-default",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168617068 +up{endpoint="metrics",instance="10.129.2.2:9153",job="dns-default",namespace="openshift-dns",pod="dns-default-nx5nm",service="dns-default",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168615473 +up{endpoint="metrics",instance="10.129.2.4:1936",job="router-internal-default",namespace="openshift-ingress",pod="router-default-7d7fc5d857-t2gpw",service="router-internal-default",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168633608 +up{endpoint="metrics",instance="10.130.0.2:9153",job="dns-default",namespace="openshift-dns",pod="dns-default-22rph",service="dns-default",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168611955 +up{endpoint="metrics",instance="10.131.0.2:9153",job="dns-default",namespace="openshift-dns",pod="dns-default-zmk5n",service="dns-default",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168629067 +up{endpoint="metrics",instance="10.131.0.3:1936",job="router-internal-default",namespace="openshift-ingress",pod="router-default-7d7fc5d857-6pqs4",service="router-internal-default",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168636295 +up{endpoint="web",instance="10.128.2.6:9091",job="prometheus-k8s",namespace="openshift-monitoring",pod="prometheus-k8s-0",service="prometheus-k8s",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168621420 +up{endpoint="web",instance="10.128.2.7:9094",job="alertmanager-main",namespace="openshift-monitoring",pod="alertmanager-main-1",service="alertmanager-main",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168619299 +up{endpoint="web",instance="10.129.2.6:9094",job="alertmanager-main",namespace="openshift-monitoring",pod="alertmanager-main-0",service="alertmanager-main",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168626501 +up{endpoint="web",instance="10.129.2.8:9091",job="prometheus-k8s",namespace="openshift-monitoring",pod="prometheus-k8s-1",service="prometheus-k8s",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168629273 +up{endpoint="web",instance="10.131.0.13:9094",job="alertmanager-main",namespace="openshift-monitoring",pod="alertmanager-main-2",service="alertmanager-main",prometheus="openshift-monitoring/k8s",prometheus_replica="prometheus-k8s-0"} 1 1562168616104 diff --git a/test/tokens.json b/test/tokens.json new file mode 100644 index 00000000..3885e956 --- /dev/null +++ b/test/tokens.json @@ -0,0 +1,5 @@ +[ + { + "token": "a" + } +] diff --git a/tools/go.mod b/tools/go.mod new file mode 100644 index 00000000..92a446c1 --- /dev/null +++ b/tools/go.mod @@ -0,0 +1,15 @@ +module github.com/openshift/telemeter/tools + +go 1.14 + +require ( + github.com/brancz/gojsontoyaml v0.0.0-20200602132005-3697ded27e8c + github.com/campoy/embedmd v1.0.0 + github.com/google/go-jsonnet v0.16.0 + github.com/jsonnet-bundler/jsonnet-bundler v0.4.0 + github.com/observatorium/up v0.0.0-20200615121732-d763595ede50 + github.com/thanos-io/thanos v0.13.0 +) + +// Mitigation for: https://github.com/Azure/go-autorest/issues/414 +replace github.com/Azure/go-autorest => github.com/Azure/go-autorest v13.3.3+incompatible diff --git a/tools/go.sum b/tools/go.sum new file mode 100644 index 00000000..290e280d --- /dev/null +++ b/tools/go.sum @@ -0,0 +1,1200 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.49.0 h1:CH+lkubJzcPYB1Ggupcq0+k8Ni2ILdG2lYjDIgavDBQ= +cloud.google.com/go v0.49.0/go.mod h1:hGvAdzcWNbyuxS3nWhD7H2cIJxjRRTRLQVB0bdputVY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0 h1:sAbMqjY1PEQKZBWfbu6Y6bsupJ9c4QdHnzg/VvYTLcE= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigtable v1.1.0/go.mod h1:B6ByKcIdYmhoyDzmOnQxyOhN6r05qnewYIxxG6L0/b4= +cloud.google.com/go/datastore v1.0.0 h1:Kt+gOPPp2LEPWp8CSfxhsM8ik9CcyE/gYu+0r+RnZvM= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/pubsub v1.0.1 h1:W9tAK3E57P75u0XLLR82LZyw8VpAnhmyTOxW9qzmyj8= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.3.0 h1:2Ze/3nQD5F+HfL0xOPM2EeawDWs+NPRtzgcre+17iZU= +cloud.google.com/go/storage v1.3.0/go.mod h1:9IAwXhoyBJ7z9LcAwkj0/7NnPzYaPeZxxVp3zm+5IqA= +contrib.go.opencensus.io/exporter/ocagent v0.6.0/go.mod h1:zmKjrJcdo0aYcVS7bmEeSEBLPA9YJp5bjrofdU3pIXs= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4= +github.com/Azure/azure-pipeline-go v0.2.2 h1:6oiIS9yaG6XCCzhgAgKFfIWyo4LLCiDhZot6ltoThhY= +github.com/Azure/azure-pipeline-go v0.2.2/go.mod h1:4rQ/NZncSvGqNkkOsNpOU1tgoNuIlp9AfUH5G1tvCHc= +github.com/Azure/azure-sdk-for-go v23.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v36.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v39.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v40.1.0+incompatible h1:VeXBqO/0U7yXiMgjRsfcmqPWXFZRA/heTlzE4qun0T4= +github.com/Azure/azure-sdk-for-go v40.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-storage-blob-go v0.8.0 h1:53qhf0Oxa0nOjgbDeeYPUeyiNmafAFEY95rZLK0Tj6o= +github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-autorest v13.3.3+incompatible h1:oYzB8/Ldlo1Bq7By79KO/1nxWuoLnEoGQiToUM2rBZo= +github.com/Azure/go-autorest v13.3.3+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest v0.9.3-0.20191028180845-3492b2aff503/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest v0.9.5/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630= +github.com/Azure/go-autorest/autorest v0.10.0 h1:mvdtztBqcL8se7MdrUweNieTNi4kfNG6GOJuurQJpuY= +github.com/Azure/go-autorest/autorest v0.10.0/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630= +github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= +github.com/Azure/go-autorest/autorest/adal v0.8.1-0.20191028180845-3492b2aff503/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc= +github.com/Azure/go-autorest/autorest/adal v0.8.2 h1:O1X4oexUxnZCaEUGsvMnr8ZGj8HI37tNezwY4npRqA0= +github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/date v0.2.0 h1:yW+Zlqf26583pE43KhfnhFcdmSWlm5Ew6bxipnr/tbM= +github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.3.0 h1:qJumjCaCudz+OcqE9/XtEPfvtOjOmKaui4EOpFI6zZc= +github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM= +github.com/Azure/go-autorest/autorest/to v0.3.0/go.mod h1:MgwOyqaIuKdG4TL/2ywSsIWKAfJfgHDo8ObuUk3t5sA= +github.com/Azure/go-autorest/autorest/to v0.3.1-0.20191028180845-3492b2aff503 h1:2McfZNaDqGPjv2pddK547PENIk4HV+NT7gvqRq4L0us= +github.com/Azure/go-autorest/autorest/to v0.3.1-0.20191028180845-3492b2aff503/go.mod h1:MgwOyqaIuKdG4TL/2ywSsIWKAfJfgHDo8ObuUk3t5sA= +github.com/Azure/go-autorest/autorest/validation v0.2.0/go.mod h1:3EEqHnBxQGHXRYq3HT1WyXAvT7LLY3tl70hw6tQIbjI= +github.com/Azure/go-autorest/autorest/validation v0.2.1-0.20191028180845-3492b2aff503 h1:RBrGlrkPWapMcLp1M6ywCqyYKOAT5ERI6lYFvGKOThE= +github.com/Azure/go-autorest/autorest/validation v0.2.1-0.20191028180845-3492b2aff503/go.mod h1:3EEqHnBxQGHXRYq3HT1WyXAvT7LLY3tl70hw6tQIbjI= +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.5.0 h1:TRn4WjSnkcSy5AEG3pnbtFSwNtwzjr4VYyQflFE619k= +github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/Masterminds/squirrel v0.0.0-20161115235646-20f192218cf5/go.mod h1:xnKTFzjGUiZtiOagBsfnvomW+nJg2usB1ZpordQWqNM= +github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/OneOfOne/xxhash v1.2.5/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= +github.com/OneOfOne/xxhash v1.2.6 h1:U68crOE3y3MPttCMQGywZOLrTeF5HHJ3/vDBCJn9/bA= +github.com/OneOfOne/xxhash v1.2.6/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= +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/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/aliyun/aliyun-oss-go-sdk v2.0.4+incompatible h1:EaK5256H3ELiyaq5O/Zwd6fnghD6DqmZDQmmzzJklUU= +github.com/aliyun/aliyun-oss-go-sdk v2.0.4+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= +github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg= +github.com/armon/go-metrics v0.3.0 h1:B7AQgHi8QSEi4uHu7Sbsga+IJDU+CENgjxoo81vDUqU= +github.com/armon/go-metrics v0.3.0/go.mod h1:zXjbSimjXTd7vOpY8B0/2LpvNvDoXBuplAD+gJD3GYs= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +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/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM= +github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.22.4/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.25.48/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.29.4/go.mod h1:1KvfttTE3SPKMpo8g2c6jL3ZKfXtFvKscTgahTma5Xg= +github.com/aws/aws-sdk-go v1.29.18 h1:3T6OdmTwOiEX/didd+RkTdOm6WPzXKFLMVS+ZH9DX1I= +github.com/aws/aws-sdk-go v1.29.18/go.mod h1:1KvfttTE3SPKMpo8g2c6jL3ZKfXtFvKscTgahTma5Xg= +github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f h1:ZNv7On9kyUzm7fvRZumSyy/IUiSC7AzL0I1jKKtwooA= +github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= +github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0= +github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= +github.com/brancz/gojsontoyaml v0.0.0-20200602132005-3697ded27e8c h1:hb6WqfcKQZlNx/vahy51SaIvKnoXD5609Nm0PC4msEM= +github.com/brancz/gojsontoyaml v0.0.0-20200602132005-3697ded27e8c/go.mod h1:+00lOjYXPgMfxHVPvg9GDtc3BX5Xh5aFpB4gMB8gfMo= +github.com/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY= +github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8= +github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/cenkalti/backoff v0.0.0-20181003080854-62661b46c409/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff v1.0.0/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v0.0.0-20181017004759-096ff4a8a059/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk= +github.com/cockroachdb/datadriven v0.0.0-20190531201743-edce55837238/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/containerd/containerd v1.2.7/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cortexproject/cortex v0.6.1-0.20200228110116-92ab6cbe0995/go.mod h1:3Xa3DjJxtpXqxcMGdk850lcIRb81M0fyY1MQ6udY134= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8= +github.com/cznic/fileutil v0.0.0-20180108211300-6a051e75936f/go.mod h1:8S58EK26zhXSxzv7NQFpnliaOQsmDUxvoQO3rt154Vg= +github.com/cznic/golex v0.0.0-20170803123110-4ab7c5e190e4/go.mod h1:+bmmJDNmKlhWNG+gwWCkaBoTy39Fs+bzRxVBzoTQbIc= +github.com/cznic/internal v0.0.0-20180608152220-f44710a21d00/go.mod h1:olo7eAdKwJdXxb55TKGLiJ6xt1H0/tiiRCWKVLmtjY4= +github.com/cznic/lldb v1.1.0/go.mod h1:FIZVUmYUVhPwRiPzL8nD/mpFcJ/G7SSXjjXYG4uRI3A= +github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= +github.com/cznic/ql v1.2.0/go.mod h1:FbpzhyZrqr0PVlK6ury+PoW3T0ODUV22OeWIxcaOrSE= +github.com/cznic/sortutil v0.0.0-20150617083342-4c7342852e65/go.mod h1:q2w6Bg5jeox1B+QkJ6Wp/+Vn0G/bo3f1uY7Fn3vivIQ= +github.com/cznic/strutil v0.0.0-20171016134553-529a34b1c186/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc= +github.com/cznic/zappy v0.0.0-20160723133515-2533cb5b45cc/go.mod h1:Y1SNZ4dRUOKXshKUbwUapqNncRrho4mkjQebgEHZLj8= +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/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= +github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +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/dgryski/go-sip13 v0.0.0-20190329191031-25c5027a8c7b/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dhui/dktest v0.3.0/go.mod h1:cyzIUfGsBEbZ6BT7tnXqAShHSXCZhSNmFl70sZ7c1yc= +github.com/docker/distribution v2.7.0+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v0.7.3-0.20190103212154-2b7e084dc98b/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v0.7.3-0.20190817195342-4760db040282/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +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/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +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/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/elastic/go-sysinfo v1.0.1/go.mod h1:O/D5m1VpYLwGjCYzEt63g3Z1uO3jXfwyzzjiW90t8cY= +github.com/elastic/go-sysinfo v1.1.1 h1:ZVlaLDyhVkDfjwPGU55CQRCRolNpc7P0BbyhhQZQmMI= +github.com/elastic/go-sysinfo v1.1.1/go.mod h1:i1ZYdU10oLNfRzq4vq62BEwD2fH8KaWh6eh0ikPT9F0= +github.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6CcloAxnPgU= +github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0= +github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss= +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/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.5.0+incompatible h1:ouOWdg56aJriqS0huScTkVXPC5IcNrDCXZ6OoTAWu7M= +github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:IT4JYU7k4ikYg1SCxNI1/Tieq/NFvh6dzLdgi7eu0tM= +github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb/go.mod h1:bH6Xx7IW64qjjJq8M2u4dxNaBiDfKK+z/3eGDpXEQhc= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/structtag v1.1.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsouza/fake-gcs-server v1.7.0/go.mod h1:5XIRs4YvwNbNoz+1JF8j6KLAyDh7RHGAyAK3EP2EsNk= +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/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-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.10.0 h1:dXFJfIHVvUcpSgDOV+Ne6t7jXri8Tfv2uOLHUZ2XNuo= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +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.17.2/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.17.2/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.17.2/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 h1:A9+F4Dc/MCNB5jibxf6rRvOvR/iFgQdyNx9eIhnGqq0= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +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.17.2/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.17.2/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.18.0/go.mod h1:uI6pHuxWYTy94zZxgcwJkUWa9wbIlhteGfloI10GD4U= +github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= +github.com/go-openapi/runtime v0.19.3/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= +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.17.2/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 h1:SStNd1jRcYtfKCN7R0laGNs80WYYvn5CbBjM2sOmCrE= +github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= +github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.17.2/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.4 h1:eRvaqAhpL0IL6Trh5fDsGnGhiXndzHFuA05w6sXH6/g= +github.com/go-openapi/strfmt v0.19.4/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= +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.17.2/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.4/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.17.2/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= +github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= +github.com/go-openapi/validate v0.19.2 h1:ky5l57HjyVRrsJfd2+Ro5Z9PjGuKbsmftwyMtk8H7js= +github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +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/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4/go.mod h1:4Fw1eo5iaEhDUs8XyuhSVCVy52Jq3L+/3GJgYkwc+/0= +github.com/gocql/gocql v0.0.0-20200121121104-95d072f1b5bb/go.mod h1:DL0ekTmBSTdlNF25Orwt/JMzqIq3EJ4MVa/J/uK64OY= +github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= +github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +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.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.2.2-0.20190730201129-28a6bbf47e48/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.0/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/gogo/status v1.0.3/go.mod h1:SavQ51ycCLnc7dGyJxp8YAmudx8xqiVrRf+6IXRsugc= +github.com/golang-migrate/migrate/v4 v4.7.0/go.mod h1:Qvut3N4xKWjoH3sokBccML6WyHSnggXm/DvMMnTsQIc= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +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/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 h1:uHTyIjqVhYRhLbJ8nIiOJHkEZZ+5YoOsAbD3sk82NiE= +github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +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/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/google/btree v0.0.0-20160524151835-7d79101e329e/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +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/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-jsonnet v0.16.0 h1:Nb4EEOp+rdeGGyB1rQ5eisgSAqrTnhf9ip+X6lzZbY0= +github.com/google/go-jsonnet v0.16.0/go.mod h1:sOcuej3UW1vpPTZOr8L7RQimqai1a57bt5j22LzGZCw= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/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/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190723021845-34ac40c74b70/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +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/gax-go v2.0.2+incompatible h1:silFMLAnr330+NRuag/VjIGF7TLp/LBrV2CJKFLWEww= +github.com/googleapis/gax-go v2.0.2+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gnostic v0.0.0-20170426233943-68f4ded48ba9/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.3.0/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.0.0-20190126172459-c818fa66e4c8/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4= +github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= +github.com/gophercloud/gophercloud v0.3.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= +github.com/gophercloud/gophercloud v0.6.0/go.mod h1:GICNByuaEBibcjmjvI7QvYJSZEbGkcYwAR7EZK2WMqM= +github.com/gophercloud/gophercloud v0.8.0 h1:1ylFFLRx7otpfRPSuOm77q8HLSlSOwYCGDeXmXJhX7A= +github.com/gophercloud/gophercloud v0.8.0/go.mod h1:Kc/QKr9thLKruO/dG0szY8kRIYS+iENz0ziI0hJf76A= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20191106031601-ce3c9ade29de h1:F7WD09S8QB4LrkEpka0dFPLSotH11HRpCsLIbIcJ7sU= +github.com/gopherjs/gopherjs v0.0.0-20191106031601-ce3c9ade29de/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +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/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.1.0 h1:THDBEeQ9xZ8JEaCLyLQqXMMdRqNr0QAUJTIkQAUtFjg= +github.com/grpc-ecosystem/go-grpc-middleware v1.1.0/go.mod h1:f5nM7jw/oeRSadq3xCzHAvxcr8HZnzsqU6ILg/0NiiE= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.4.1/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/grpc-ecosystem/grpc-gateway v1.9.4/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.12.1/go.mod h1:8XEsbTttt/W+VvjtQhLACqCisSPWTxCZ7sBRjU6iH9c= +github.com/grpc-ecosystem/grpc-gateway v1.13.0/go.mod h1:8XEsbTttt/W+VvjtQhLACqCisSPWTxCZ7sBRjU6iH9c= +github.com/grpc-ecosystem/grpc-gateway v1.14.1 h1:YuM9SXYy583fxvSOkzCDyBPCtY+/IMSHEG1dKFMLZsA= +github.com/grpc-ecosystem/grpc-gateway v1.14.1/go.mod h1:6CwZWGDSPRJidgKAtJVvND6soZe6fT7iteq8wDPdhb0= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= +github.com/hashicorp/consul/api v1.4.0 h1:jfESivXnO5uLdH650JU/6AnjRoHrLhULq0FnC3Kp9EY= +github.com/hashicorp/consul/api v1.4.0/go.mod h1:xc8u05kyMa3Wjr9eEAsIAo3dg8+LywT5E/Cl7cNS5nU= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/consul/sdk v0.4.0 h1:zBtCfKJZcJDBvSCkQJch4ulp59m1rATFLKwNo/LYY30= +github.com/hashicorp/consul/sdk v0.4.0/go.mod h1:fY08Y9z5SvJqevyZNy6WWPXiG3KwBPAvlcdx16zZ0fM= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.12.0 h1:d4QkX8FRTYaKaCZBoXYY8zJX2BXjWxurN/GA2tkrmZM= +github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.1.0 h1:vN9wG1D6KG6YHRTWr8512cxGOVgTMEfgEdSj/hr8MPc= +github.com/hashicorp/go-immutable-radix v1.1.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI= +github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +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/hashicorp/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8Bppgk= +github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/memberlist v0.1.4/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/memberlist v0.1.5 h1:AYBsgJOW9gab/toO5tEB8lWetVgDKZycqkebJ8xxpqM= +github.com/hashicorp/memberlist v0.1.5/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hashicorp/serf v0.8.3/go.mod h1:UpNcs7fFbpKIyZaUuSW6EPiH+eZC7OuyFD+wc1oal+k= +github.com/hashicorp/serf v0.8.5 h1:ZynDUIQiA8usmRgPdGPHFdPnb1wgGI9tK3mO9hcAJjc= +github.com/hashicorp/serf v0.8.5/go.mod h1:UpNcs7fFbpKIyZaUuSW6EPiH+eZC7OuyFD+wc1oal+k= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/influxdata/influxdb v1.7.7/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ= +github.com/jackc/pgx v3.2.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= +github.com/jessevdk/go-flags v0.0.0-20180331124232-1c38ed7ad0cc/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4= +github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jsonnet-bundler/jsonnet-bundler v0.4.0 h1:4BKZ6LDqPc2wJDmaKnmYD/vDjUptJtnUpai802MibFc= +github.com/jsonnet-bundler/jsonnet-bundler v0.4.0/go.mod h1:/by7P/OoohkI3q4CgSFqcoFsVY+IaNbzOVDknEsKDeU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +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/konsorten/go-windows-terminal-sequences v1.0.2/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/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.0.0/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +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/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kshvakov/clickhouse v1.3.5/go.mod h1:DMzX7FxRymoNkVgizH0DWAL8Cur7wHLgx3MUnGwJqpE= +github.com/kylelemons/godebug v0.0.0-20160406211939-eadb3ce320cb/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/lann/builder v0.0.0-20150808151131-f22ce00fd939/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/leanovate/gopter v0.2.4 h1:U4YLBggDFhJdqQsG4Na2zX7joVTky9vHaj/AGEwSuXU= +github.com/leanovate/gopter v0.2.4/go.mod h1:gNcbPWNEWRe4lm+bycKqxUYoH5uoVje5SkOJ3uoLer8= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743 h1:143Bb8f8DuGWck/xpNUOckBVYfFbBTnLevfRZ1aVVqo= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.0/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/lightstep/lightstep-tracer-go v0.18.1 h1:vi1F1IQ8N7hNWytK9DpJsUfQhGuNSc19z330K6vl4zk= +github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/lovoo/gcloud-opentracing v0.3.0 h1:nAeKG70rIsog0TelcEtt6KU0Y1s5qXtsDLnHp0urPLU= +github.com/lovoo/gcloud-opentracing v0.3.0/go.mod h1:ZFqk2y38kMDDikZPAK7ynTTGuyt17nSPdS3K5e+ZTBY= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +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/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= +github.com/mattn/go-ieproxy v0.0.0-20190702010315-6dee0af9227d/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= +github.com/mattn/go-ieproxy v0.0.0-20191113090002-7c0f6868bffe h1:YioO2TiJyAHWHyCRQCP8jk5IzTqmsbGc5qQPIhHo6xs= +github.com/mattn/go-ieproxy v0.0.0-20191113090002-7c0f6868bffe/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.6/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.6 h1:V2iyH+aX9C5fsYCpK60U8BYIvmhqxuOL3JZcqc1NB7k= +github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/matttproud/golang_protobuf_extensions v1.0.0/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.15/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.22/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.27 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM= +github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= +github.com/minio/minio-go/v6 v6.0.44/go.mod h1:qD0lajrGW49lKZLtXKtCB4X/qkMf0a5tBvN2PaZg7Gg= +github.com/minio/minio-go/v6 v6.0.56 h1:H4+v6UFV1V7VkEf1HjL15W9OvTL1Gy8EbMmjQZHqEbg= +github.com/minio/minio-go/v6 v6.0.56/go.mod h1:KQMM+/44DSlSGSQWSfRrAZ12FVMmpWNuX37i2AX0jfI= +github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU= +github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +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/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +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/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/mozillazg/go-cos v0.13.0 h1:RylOpEESdWMLb13bl0ADhko12uMN3JmHqqwFu4OYGBY= +github.com/mozillazg/go-cos v0.13.0/go.mod h1:Zp6DvvXn0RUOXGJ2chmWt2bLEqRAnJnS3DnAZsJsoaE= +github.com/mozillazg/go-httpheader v0.2.1 h1:geV7TrjbL8KXSyvghnFm+NyTux/hxwueTSrwhe88TQQ= +github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60= +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/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/observatorium/up v0.0.0-20200615121732-d763595ede50 h1:axH2fCXFokh5beK+YhCqGQT6FrTbFqfHLA4yiFY+h9A= +github.com/observatorium/up v0.0.0-20200615121732-d763595ede50/go.mod h1:ysto9FMxd/1rHh9N9l1JfHLjT3X7ix4oWjMfnyrpqsY= +github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/oklog/ulid v0.0.0-20170117200651-66bb6560562f/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.2 h1:sq53g+DWf0J6/ceFUHpQ0nAEb6WgM++fq16MZ91cS6o= +github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ= +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.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY= +github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v0.0.0-20190113212917-5533ce8a0da3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opentracing-contrib/go-grpc v0.0.0-20180928155321-4b5a12d3ff02/go.mod h1:JNdpVEzCpXBgIiv4ds+TzhN1hrtxq6ClLrTlT9OQRSc= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing-contrib/go-stdlib v0.0.0-20190519235532-cf7a6c988dc9/go.mod h1:PLldrQSroqzH70Xl+1DQcGnefIbqsKR7UDaiux3zV+w= +github.com/opentracing/basictracer-go v1.0.0 h1:YyUAhaEfjoWXclZVJ9sGoNct7j4TVk7lZWlQw5UXuoo= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.1-0.20200124165624-2876d2018785 h1:Oi9nYnU9jbiUVyoRTQfMpSdGzNVmEI+/9fija3lcnjU= +github.com/opentracing/opentracing-go v1.1.1-0.20200124165624-2876d2018785/go.mod h1:C+iumr2ni468+1jvcHXLCdqP9uQnoQbdX93F3aWahWU= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +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/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/alertmanager v0.18.0/go.mod h1:WcxHBl40VSPuOaqWae6l6HpnEOVRIycEJ7i9iYkadEE= +github.com/prometheus/alertmanager v0.19.0/go.mod h1:Eyp94Yi/T+kdeb2qvq66E3RGuph5T/jm/RBVh4yz1xo= +github.com/prometheus/alertmanager v0.20.0 h1:PBMNY7oyIvYMBBIag35/C0hO7xn8+35p4V5rNAph5N8= +github.com/prometheus/alertmanager v0.20.0/go.mod h1:9g2i48FAyZW6BtbsnvHtMHQXl2aVtrORKwKVCQ+nbrg= +github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_golang v1.2.0/go.mod h1:XMU6Z2MjaRKVu/dC1qupJI9SiNkDYzz3xecMgSW/F+U= +github.com/prometheus/client_golang v1.2.1/go.mod h1:XMU6Z2MjaRKVu/dC1qupJI9SiNkDYzz3xecMgSW/F+U= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= +github.com/prometheus/client_golang v1.4.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.5.1 h1:bdHYieyGlH+6OLEk2YQha8THib30KP0/yD0YH9m6xcA= +github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_model v0.0.0-20170216185247-6f3806018612/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +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/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20180518154759-7600349dcfe1/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/common v0.8.0/go.mod h1:PC/OgXc+UN7B4ALwvn1yzVZmVwvhXp5JsbBv6wSv6i0= +github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/procfs v0.0.0-20180612222113-7d6f385de8be/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190425082905-87a4384529e0/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.6/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/prometheus v0.0.0-20180315085919-58e2a31db8de/go.mod h1:oAIUtOny2rjMX0OWN5vPR5/q/twIROJvdqnQKDdil/s= +github.com/prometheus/prometheus v0.0.0-20190818123050-43acd0e2e93f/go.mod h1:rMTlmxGCvukf2KMu3fClMDKLLoJ5hl61MhcJ7xKakf0= +github.com/prometheus/prometheus v1.8.2-0.20200107122003-4708915ac6ef/go.mod h1:7U90zPoLkWjEIQcy/rweQla82OCTUzxVHE51G3OhJbI= +github.com/prometheus/prometheus v1.8.2-0.20200213233353-b90be6f32a33/go.mod h1:fkIPPkuZnkXyopYHmXPxf9rgiPkVgZCN8w9o8+UgBlY= +github.com/prometheus/prometheus v1.8.2-0.20200305080338-7164b58945bb/go.mod h1:/feD9J3z9H+U+gA6A8c4nZ+hB6kJuyelr0XHOF4eNWM= +github.com/prometheus/prometheus v1.8.2-0.20200407102557-cd73b3d33e06 h1:0e+fXKnc3vkMOrd1TGPk1/ifloLaGgql401Qi81jHS4= +github.com/prometheus/prometheus v1.8.2-0.20200407102557-cd73b3d33e06/go.mod h1:nNlxoC0YYSxWNe7N6dNJ0ScHt+1ddHc/y9i/R4ay7Ko= +github.com/rafaeljusto/redigomock v0.0.0-20190202135759-257e089e14a1/go.mod h1:JaY6n2sDr+z2WTsXkOmNRUfDy6FN0L6Nk7x06ndm4tY= +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/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/samuel/go-zookeeper v0.0.0-20190810000440-0ceca61e4d75/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da h1:p3Vo3i64TCLY7gIfzeQaUJ+kppEO5WQG3cL8iE8tGHU= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis= +github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4= +github.com/satori/go.uuid v0.0.0-20160603004225-b111a074d5ef/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/segmentio/fasthash v0.0.0-20180216231524-a72b379d632e/go.mod h1:tm/wZFQ8e24NYaBGIlnO2WGCAi67re4HHuOm0sftE/M= +github.com/sercand/kuberesolver v2.1.0+incompatible/go.mod h1:lWF3GL0xptCB/vCiJPl/ZshwPsX/n4Y7u0CW9E7aQIQ= +github.com/sercand/kuberesolver v2.4.0+incompatible/go.mod h1:lWF3GL0xptCB/vCiJPl/ZshwPsX/n4Y7u0CW9E7aQIQ= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/vfsgen v0.0.0-20180825020608-02ddb050ef6b/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= +github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= +github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w= +github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= +github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1/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/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= +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/thanos-io/thanos v0.8.1-0.20200109203923-552ffa4c1a0d/go.mod h1:usT/TxtJQ7DzinTt+G9kinDQmRS5sxwu0unVKZ9vdcw= +github.com/thanos-io/thanos v0.13.0 h1:o/1tPB1CmfXpYvmVxsPfCedyZdU4iBrYt2AKWHXUeV8= +github.com/thanos-io/thanos v0.13.0/go.mod h1:FQZENi6Ti0vxrea6BbYfT0ekayzxK4QtzIKIkxsXysY= +github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/uber/jaeger-client-go v2.15.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-client-go v2.20.1+incompatible h1:HgqpYBng0n7tLJIlyT4kPCIv5XgCsF+kai1NnnrJzEU= +github.com/uber/jaeger-client-go v2.20.1+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v1.5.1-0.20181102163054-1fc5c315e03c/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= +github.com/uber/jaeger-lib v2.2.0+incompatible h1:MxZXOiR2JuoANZ3J6DE/U0kSFv/eJ/GfSYVCjK7dyaw= +github.com/uber/jaeger-lib v2.2.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/weaveworks/common v0.0.0-20200206153930-760e36ae819a/go.mod h1:6enWAqfQBFrE8X/XdJwZr8IKgh1chStuFR0mjU/UOUw= +github.com/weaveworks/promrus v1.2.0/go.mod h1:SaE82+OJ91yqjrE1rsvBWVzNZKcHYFtMUyS1+Ogs/KA= +github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs= +github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= +github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= +gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= +go.elastic.co/apm v1.5.0 h1:arba7i+CVc36Jptww3R1ttW+O10ydvnBtidyd85DLpg= +go.elastic.co/apm v1.5.0/go.mod h1:OdB9sPtM6Vt7oz3VXt7+KR96i9li74qrxBGHTQygFvk= +go.elastic.co/apm/module/apmhttp v1.5.0 h1:sxntP97oENyWWi+6GAwXUo05oEpkwbiarZLqrzLRA4o= +go.elastic.co/apm/module/apmhttp v1.5.0/go.mod h1:1FbmNuyD3ddauwzgVwFB0fqY6KbZt3JkV187tGCYYhY= +go.elastic.co/apm/module/apmot v1.5.0 h1:rPyHRI6Ooqjwny67au6e2eIxLZshqd7bJfAUpdgOw/4= +go.elastic.co/apm/module/apmot v1.5.0/go.mod h1:d2KYwhJParTpyw2WnTNy8geNlHKKFX+4oK3YLlsesWE= +go.elastic.co/fastjson v1.0.0 h1:ooXV/ABvf+tBul26jcVViPT3sBir0PvXgibYB1IQQzg= +go.elastic.co/fastjson v1.0.0/go.mod h1:PmeUOMMtLHQr9ZS9J9owrAVg0FkaZDRZJEFTTGHtchs= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v0.0.0-20190709142735-eb7dd97135a5/go.mod h1:N0RPWo9FXJYZQI4BTkDtQylrstIigYHeR18ONnyTufk= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.0.4/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.1.0 h1:aeOqSrhl9eDRAap/3T5pCfMBEBxZ0vuXBP+RMtp2KX8= +go.mongodb.org/mongo-driver v1.1.0/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/automaxprocs v1.2.0 h1:+RUihKM+nmYUoB9w0D0Ov5TJ2PpFO2FgenTxMJiZBZA= +go.uber.org/automaxprocs v1.2.0/go.mod h1:YfO3fm683kQpzETxlTGZhGIVmXAhaw3gxeBADbpZtnU= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +golang.org/x/crypto v0.0.0-20180608092829-8ac0e0d97ce4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/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-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +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-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191029154019-8994fa331a53/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136 h1:A1gGSx58LAGVHUUsOf7IiR0u8Xb6W51gRwfDBhkdcaw= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +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/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +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-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181108082009-03003ca0c849/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-20181201002055-351d144fa1fc/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-20190206173232-65e2d4e15006/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-20190424112056-4829fb13d2c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +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-20190628185345-da137c7871d7/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-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/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-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/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-20180823144017-11551d06cbcc/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-20181026203630-95b1ffbd15a5/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-20190102155601-82a175fd1598/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190209173611-3b5209105503/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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190310054646-10058d7d4faa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +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-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190425145619-16072639606e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190426135247-a129542de9ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191025021431-6c3a3bfe00ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191113165036-4c7a9d0fe056/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/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.1-0.20180805044716-cb6730876b98/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +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-20161028155119-f51c12702a4d/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +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-20190118193359-16909d206f00/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-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190425222832-ad9eeb80039a/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +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-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190813034749-528a2984e271/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190918214516-5a1a30219888/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191111182352-50fa39b762bc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191203134012-c197fd4bf371/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200305205014-bc073721adb6/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200306191617-51e69f71924f h1:bFIWQKTZ5vXyr7xMDvzbWUj5Y/WBE4a4sf35MAyZjx0= +golang.org/x/tools v0.0.0-20200306191617-51e69f71924f/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +google.golang.org/api v0.3.2/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0 h1:jz2KixHX7EcCPiQrySzPdnYT7DbINAypCqKZ1Z7GM40= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180608181217-32ee49c4dd80/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191028173616-919d9bdd9fe6/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171 h1:xes2Q2k+d/+YNXVw0FpZkIDJiaux4OVrRKXRAzH6A0U= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +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.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= +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/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/fsnotify/fsnotify.v1 v1.4.7 h1:XNNYLJHt73EyYiCZi6+xjupS9CpvmiDgjPTAjrBlQbo= +gopkg.in/fsnotify/fsnotify.v1 v1.4.7/go.mod h1:Fyux9zXlo4rWoMSIzpn9fDAYjalPqJ/K1qJ27s+7ltE= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +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/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +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.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71 h1:Xe2gvTZUJpsvOWUnvmL/tmhVBZUmHSvLbMjRj6NUUKo= +gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +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= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +howett.net/plist v0.0.0-20181124034731-591f970eefbb h1:jhnBjNi9UFpfpl8YZhA9CrOqpnJdvzuiHsl/dnxl11M= +howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= +k8s.io/api v0.0.0-20190620084959-7cf5895f2711/go.mod h1:TBhBqb1AWbBQbW3XRusr7n7E4v2+5ZY8r8sAMnyFC5A= +k8s.io/api v0.0.0-20190813020757-36bff7324fb7/go.mod h1:3Iy+myeAORNCLgjd/Xu9ebwN7Vh59Bw0vh9jhoX+V58= +k8s.io/api v0.0.0-20191115095533-47f6de673b26/go.mod h1:iA/8arsvelvo4IDqIhX4IbjTEKBGgvsf2OraTuRtLFU= +k8s.io/api v0.17.3 h1:XAm3PZp3wnEdzekNkcmj/9Y1zdmQYJ1I4GKSBBZ8aG0= +k8s.io/api v0.17.3/go.mod h1:YZ0OTkuw7ipbe305fMpIdf3GLXZKRigjtZaV5gzC2J0= +k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719/go.mod h1:I4A+glKBHiTgiEjQiCCQfCAIcIMFGt291SmsvcrFzJA= +k8s.io/apimachinery v0.0.0-20190809020650-423f5d784010/go.mod h1:Waf/xTS2FGRrgXCkO5FP3XxTOWh0qLf2QhL1qFZZ/R8= +k8s.io/apimachinery v0.0.0-20191115015347-3c7067801da2/go.mod h1:dXFS2zaQR8fyzuvRdJDHw2Aerij/yVGJSre0bZQSVJA= +k8s.io/apimachinery v0.17.3 h1:f+uZV6rm4/tHE7xXgLyToprg6xWairaClGVkm2t8omg= +k8s.io/apimachinery v0.17.3/go.mod h1:gxLnyZcGNdZTCLnq3fgzyg2A5BVCHTNDFrw8AmuJ+0g= +k8s.io/client-go v0.0.0-20190620085101-78d2af792bab/go.mod h1:E95RaSlHr79aHaX0aGSwcPNfygDiPKOVXdmivCIZT0k= +k8s.io/client-go v0.17.3/go.mod h1:cLXlTMtWHkuK4tD360KpWz2gG2KtdWEr/OT02i3emRQ= +k8s.io/client-go v12.0.0+incompatible h1:YlJxncpeVUC98/WMZKC3JZGk/OXQWCZjAB4Xr3B17RY= +k8s.io/client-go v12.0.0+incompatible/go.mod h1:E95RaSlHr79aHaX0aGSwcPNfygDiPKOVXdmivCIZT0k= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +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 v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.4.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +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-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= +k8s.io/kube-openapi v0.0.0-20190709113604-33be087ad058/go.mod h1:nfDlWeOsu3pUf4yWGL+ERqohP4YsZcBJXWMK+gkzOA4= +k8s.io/kube-openapi v0.0.0-20190722073852-5e22f3d471e6/go.mod h1:RZvgC8MSN6DjiMV6oIfEE9pDL9CYXokkfaCKZeHm3nc= +k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a h1:UcxjrRMyNx/i/y8G7kPvLyy7rfbeuf1PYyBf973pgyU= +k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/utils v0.0.0-20190221042446-c2654d5206da/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0= +k8s.io/utils v0.0.0-20190809000727-6c36bc71fc4a/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +k8s.io/utils v0.0.0-20191114200735-6ca3b61696b6 h1:p0Ai3qVtkbCG/Af26dBmU0E1W58NID3hSSh7cMyylpM= +k8s.io/utils v0.0.0-20191114200735-6ca3b61696b6/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +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= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/tools/tools.go b/tools/tools.go new file mode 100644 index 00000000..de1a2b9c --- /dev/null +++ b/tools/tools.go @@ -0,0 +1,14 @@ +// +build tools + +// Package tools tracks dependencies for tools that used in the build process. +// See https://github.com/golang/go/issues/25922 +package tools + +import ( + _ "github.com/brancz/gojsontoyaml" + _ "github.com/campoy/embedmd" + _ "github.com/google/go-jsonnet/cmd/jsonnet" + _ "github.com/jsonnet-bundler/jsonnet-bundler/cmd/jb" + _ "github.com/observatorium/up" + _ "github.com/thanos-io/thanos/cmd/thanos" +) diff --git a/vendor/github.com/alecthomas/units/COPYING b/vendor/github.com/alecthomas/units/COPYING new file mode 100644 index 00000000..2993ec08 --- /dev/null +++ b/vendor/github.com/alecthomas/units/COPYING @@ -0,0 +1,19 @@ +Copyright (C) 2014 Alec Thomas + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/alecthomas/units/README.md b/vendor/github.com/alecthomas/units/README.md new file mode 100644 index 00000000..bee884e3 --- /dev/null +++ b/vendor/github.com/alecthomas/units/README.md @@ -0,0 +1,11 @@ +# Units - Helpful unit multipliers and functions for Go + +The goal of this package is to have functionality similar to the [time](http://golang.org/pkg/time/) package. + +It allows for code like this: + +```go +n, err := ParseBase2Bytes("1KB") +// n == 1024 +n = units.Mebibyte * 512 +``` diff --git a/vendor/github.com/alecthomas/units/bytes.go b/vendor/github.com/alecthomas/units/bytes.go new file mode 100644 index 00000000..61d0ca47 --- /dev/null +++ b/vendor/github.com/alecthomas/units/bytes.go @@ -0,0 +1,85 @@ +package units + +// Base2Bytes is the old non-SI power-of-2 byte scale (1024 bytes in a kilobyte, +// etc.). +type Base2Bytes int64 + +// Base-2 byte units. +const ( + Kibibyte Base2Bytes = 1024 + KiB = Kibibyte + Mebibyte = Kibibyte * 1024 + MiB = Mebibyte + Gibibyte = Mebibyte * 1024 + GiB = Gibibyte + Tebibyte = Gibibyte * 1024 + TiB = Tebibyte + Pebibyte = Tebibyte * 1024 + PiB = Pebibyte + Exbibyte = Pebibyte * 1024 + EiB = Exbibyte +) + +var ( + bytesUnitMap = MakeUnitMap("iB", "B", 1024) + oldBytesUnitMap = MakeUnitMap("B", "B", 1024) +) + +// ParseBase2Bytes supports both iB and B in base-2 multipliers. That is, KB +// and KiB are both 1024. +// However "kB", which is the correct SI spelling of 1000 Bytes, is rejected. +func ParseBase2Bytes(s string) (Base2Bytes, error) { + n, err := ParseUnit(s, bytesUnitMap) + if err != nil { + n, err = ParseUnit(s, oldBytesUnitMap) + } + return Base2Bytes(n), err +} + +func (b Base2Bytes) String() string { + return ToString(int64(b), 1024, "iB", "B") +} + +var ( + metricBytesUnitMap = MakeUnitMap("B", "B", 1000) +) + +// MetricBytes are SI byte units (1000 bytes in a kilobyte). +type MetricBytes SI + +// SI base-10 byte units. +const ( + Kilobyte MetricBytes = 1000 + KB = Kilobyte + Megabyte = Kilobyte * 1000 + MB = Megabyte + Gigabyte = Megabyte * 1000 + GB = Gigabyte + Terabyte = Gigabyte * 1000 + TB = Terabyte + Petabyte = Terabyte * 1000 + PB = Petabyte + Exabyte = Petabyte * 1000 + EB = Exabyte +) + +// ParseMetricBytes parses base-10 metric byte units. That is, KB is 1000 bytes. +func ParseMetricBytes(s string) (MetricBytes, error) { + n, err := ParseUnit(s, metricBytesUnitMap) + return MetricBytes(n), err +} + +// TODO: represents 1000B as uppercase "KB", while SI standard requires "kB". +func (m MetricBytes) String() string { + return ToString(int64(m), 1000, "B", "B") +} + +// ParseStrictBytes supports both iB and B suffixes for base 2 and metric, +// respectively. That is, KiB represents 1024 and kB, KB represent 1000. +func ParseStrictBytes(s string) (int64, error) { + n, err := ParseUnit(s, bytesUnitMap) + if err != nil { + n, err = ParseUnit(s, metricBytesUnitMap) + } + return int64(n), err +} diff --git a/vendor/github.com/alecthomas/units/doc.go b/vendor/github.com/alecthomas/units/doc.go new file mode 100644 index 00000000..156ae386 --- /dev/null +++ b/vendor/github.com/alecthomas/units/doc.go @@ -0,0 +1,13 @@ +// Package units provides helpful unit multipliers and functions for Go. +// +// The goal of this package is to have functionality similar to the time [1] package. +// +// +// [1] http://golang.org/pkg/time/ +// +// It allows for code like this: +// +// n, err := ParseBase2Bytes("1KB") +// // n == 1024 +// n = units.Mebibyte * 512 +package units diff --git a/vendor/github.com/alecthomas/units/go.mod b/vendor/github.com/alecthomas/units/go.mod new file mode 100644 index 00000000..c7fb91f2 --- /dev/null +++ b/vendor/github.com/alecthomas/units/go.mod @@ -0,0 +1,3 @@ +module github.com/alecthomas/units + +require github.com/stretchr/testify v1.4.0 diff --git a/vendor/github.com/alecthomas/units/go.sum b/vendor/github.com/alecthomas/units/go.sum new file mode 100644 index 00000000..8fdee585 --- /dev/null +++ b/vendor/github.com/alecthomas/units/go.sum @@ -0,0 +1,11 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/vendor/github.com/alecthomas/units/si.go b/vendor/github.com/alecthomas/units/si.go new file mode 100644 index 00000000..99b2fa4f --- /dev/null +++ b/vendor/github.com/alecthomas/units/si.go @@ -0,0 +1,50 @@ +package units + +// SI units. +type SI int64 + +// SI unit multiples. +const ( + Kilo SI = 1000 + Mega = Kilo * 1000 + Giga = Mega * 1000 + Tera = Giga * 1000 + Peta = Tera * 1000 + Exa = Peta * 1000 +) + +func MakeUnitMap(suffix, shortSuffix string, scale int64) map[string]float64 { + res := map[string]float64{ + shortSuffix: 1, + // see below for "k" / "K" + "M" + suffix: float64(scale * scale), + "G" + suffix: float64(scale * scale * scale), + "T" + suffix: float64(scale * scale * scale * scale), + "P" + suffix: float64(scale * scale * scale * scale * scale), + "E" + suffix: float64(scale * scale * scale * scale * scale * scale), + } + + // Standard SI prefixes use lowercase "k" for kilo = 1000. + // For compatibility, and to be fool-proof, we accept both "k" and "K" in metric mode. + // + // However, official binary prefixes are always capitalized - "KiB" - + // and we specifically never parse "kB" as 1024B because: + // + // (1) people pedantic enough to use lowercase according to SI unlikely to abuse "k" to mean 1024 :-) + // + // (2) Use of capital K for 1024 was an informal tradition predating IEC prefixes: + // "The binary meaning of the kilobyte for 1024 bytes typically uses the symbol KB, with an + // uppercase letter K." + // -- https://en.wikipedia.org/wiki/Kilobyte#Base_2_(1024_bytes) + // "Capitalization of the letter K became the de facto standard for binary notation, although this + // could not be extended to higher powers, and use of the lowercase k did persist.[13][14][15]" + // -- https://en.wikipedia.org/wiki/Binary_prefix#History + // See also the extensive https://en.wikipedia.org/wiki/Timeline_of_binary_prefixes. + if scale == 1024 { + res["K"+suffix] = float64(scale) + } else { + res["k"+suffix] = float64(scale) + res["K"+suffix] = float64(scale) + } + return res +} diff --git a/vendor/github.com/alecthomas/units/util.go b/vendor/github.com/alecthomas/units/util.go new file mode 100644 index 00000000..6527e92d --- /dev/null +++ b/vendor/github.com/alecthomas/units/util.go @@ -0,0 +1,138 @@ +package units + +import ( + "errors" + "fmt" + "strings" +) + +var ( + siUnits = []string{"", "K", "M", "G", "T", "P", "E"} +) + +func ToString(n int64, scale int64, suffix, baseSuffix string) string { + mn := len(siUnits) + out := make([]string, mn) + for i, m := range siUnits { + if n%scale != 0 || i == 0 && n == 0 { + s := suffix + if i == 0 { + s = baseSuffix + } + out[mn-1-i] = fmt.Sprintf("%d%s%s", n%scale, m, s) + } + n /= scale + if n == 0 { + break + } + } + return strings.Join(out, "") +} + +// Below code ripped straight from http://golang.org/src/pkg/time/format.go?s=33392:33438#L1123 +var errLeadingInt = errors.New("units: bad [0-9]*") // never printed + +// leadingInt consumes the leading [0-9]* from s. +func leadingInt(s string) (x int64, rem string, err error) { + i := 0 + for ; i < len(s); i++ { + c := s[i] + if c < '0' || c > '9' { + break + } + if x >= (1<<63-10)/10 { + // overflow + return 0, "", errLeadingInt + } + x = x*10 + int64(c) - '0' + } + return x, s[i:], nil +} + +func ParseUnit(s string, unitMap map[string]float64) (int64, error) { + // [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+ + orig := s + f := float64(0) + neg := false + + // Consume [-+]? + if s != "" { + c := s[0] + if c == '-' || c == '+' { + neg = c == '-' + s = s[1:] + } + } + // Special case: if all that is left is "0", this is zero. + if s == "0" { + return 0, nil + } + if s == "" { + return 0, errors.New("units: invalid " + orig) + } + for s != "" { + g := float64(0) // this element of the sequence + + var x int64 + var err error + + // The next character must be [0-9.] + if !(s[0] == '.' || ('0' <= s[0] && s[0] <= '9')) { + return 0, errors.New("units: invalid " + orig) + } + // Consume [0-9]* + pl := len(s) + x, s, err = leadingInt(s) + if err != nil { + return 0, errors.New("units: invalid " + orig) + } + g = float64(x) + pre := pl != len(s) // whether we consumed anything before a period + + // Consume (\.[0-9]*)? + post := false + if s != "" && s[0] == '.' { + s = s[1:] + pl := len(s) + x, s, err = leadingInt(s) + if err != nil { + return 0, errors.New("units: invalid " + orig) + } + scale := 1.0 + for n := pl - len(s); n > 0; n-- { + scale *= 10 + } + g += float64(x) / scale + post = pl != len(s) + } + if !pre && !post { + // no digits (e.g. ".s" or "-.s") + return 0, errors.New("units: invalid " + orig) + } + + // Consume unit. + i := 0 + for ; i < len(s); i++ { + c := s[i] + if c == '.' || ('0' <= c && c <= '9') { + break + } + } + u := s[:i] + s = s[i:] + unit, ok := unitMap[u] + if !ok { + return 0, errors.New("units: unknown unit " + u + " in " + orig) + } + + f += g * unit + } + + if neg { + f = -f + } + if f < float64(-1<<63) || f > float64(1<<63-1) { + return 0, errors.New("units: overflow parsing unit") + } + return int64(f), nil +} diff --git a/vendor/github.com/beorn7/perks/LICENSE b/vendor/github.com/beorn7/perks/LICENSE new file mode 100644 index 00000000..339177be --- /dev/null +++ b/vendor/github.com/beorn7/perks/LICENSE @@ -0,0 +1,20 @@ +Copyright (C) 2013 Blake Mizerany + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/beorn7/perks/quantile/exampledata.txt b/vendor/github.com/beorn7/perks/quantile/exampledata.txt new file mode 100644 index 00000000..1602287d --- /dev/null +++ b/vendor/github.com/beorn7/perks/quantile/exampledata.txt @@ -0,0 +1,2388 @@ +8 +5 +26 +12 +5 +235 +13 +6 +28 +30 +3 +3 +3 +3 +5 +2 +33 +7 +2 +4 +7 +12 +14 +5 +8 +3 +10 +4 +5 +3 +6 +6 +209 +20 +3 +10 +14 +3 +4 +6 +8 +5 +11 +7 +3 +2 +3 +3 +212 +5 +222 +4 +10 +10 +5 +6 +3 +8 +3 +10 +254 +220 +2 +3 +5 +24 +5 +4 +222 +7 +3 +3 +223 +8 +15 +12 +14 +14 +3 +2 +2 +3 +13 +3 +11 +4 +4 +6 +5 +7 +13 +5 +3 +5 +2 +5 +3 +5 +2 +7 +15 +17 +14 +3 +6 +6 +3 +17 +5 +4 +7 +6 +4 +4 +8 +6 +8 +3 +9 +3 +6 +3 +4 +5 +3 +3 +660 +4 +6 +10 +3 +6 +3 +2 +5 +13 +2 +4 +4 +10 +4 +8 +4 +3 +7 +9 +9 +3 +10 +37 +3 +13 +4 +12 +3 +6 +10 +8 +5 +21 +2 +3 +8 +3 +2 +3 +3 +4 +12 +2 +4 +8 +8 +4 +3 +2 +20 +1 +6 +32 +2 +11 +6 +18 +3 +8 +11 +3 +212 +3 +4 +2 +6 +7 +12 +11 +3 +2 +16 +10 +6 +4 +6 +3 +2 +7 +3 +2 +2 +2 +2 +5 +6 +4 +3 +10 +3 +4 +6 +5 +3 +4 +4 +5 +6 +4 +3 +4 +4 +5 +7 +5 +5 +3 +2 +7 +2 +4 +12 +4 +5 +6 +2 +4 +4 +8 +4 +15 +13 +7 +16 +5 +3 +23 +5 +5 +7 +3 +2 +9 +8 +7 +5 +8 +11 +4 +10 +76 +4 +47 +4 +3 +2 +7 +4 +2 +3 +37 +10 +4 +2 +20 +5 +4 +4 +10 +10 +4 +3 +7 +23 +240 +7 +13 +5 +5 +3 +3 +2 +5 +4 +2 +8 +7 +19 +2 +23 +8 +7 +2 +5 +3 +8 +3 +8 +13 +5 +5 +5 +2 +3 +23 +4 +9 +8 +4 +3 +3 +5 +220 +2 +3 +4 +6 +14 +3 +53 +6 +2 +5 +18 +6 +3 +219 +6 +5 +2 +5 +3 +6 +5 +15 +4 +3 +17 +3 +2 +4 +7 +2 +3 +3 +4 +4 +3 +2 +664 +6 +3 +23 +5 +5 +16 +5 +8 +2 +4 +2 +24 +12 +3 +2 +3 +5 +8 +3 +5 +4 +3 +14 +3 +5 +8 +2 +3 +7 +9 +4 +2 +3 +6 +8 +4 +3 +4 +6 +5 +3 +3 +6 +3 +19 +4 +4 +6 +3 +6 +3 +5 +22 +5 +4 +4 +3 +8 +11 +4 +9 +7 +6 +13 +4 +4 +4 +6 +17 +9 +3 +3 +3 +4 +3 +221 +5 +11 +3 +4 +2 +12 +6 +3 +5 +7 +5 +7 +4 +9 +7 +14 +37 +19 +217 +16 +3 +5 +2 +2 +7 +19 +7 +6 +7 +4 +24 +5 +11 +4 +7 +7 +9 +13 +3 +4 +3 +6 +28 +4 +4 +5 +5 +2 +5 +6 +4 +4 +6 +10 +5 +4 +3 +2 +3 +3 +6 +5 +5 +4 +3 +2 +3 +7 +4 +6 +18 +16 +8 +16 +4 +5 +8 +6 +9 +13 +1545 +6 +215 +6 +5 +6 +3 +45 +31 +5 +2 +2 +4 +3 +3 +2 +5 +4 +3 +5 +7 +7 +4 +5 +8 +5 +4 +749 +2 +31 +9 +11 +2 +11 +5 +4 +4 +7 +9 +11 +4 +5 +4 +7 +3 +4 +6 +2 +15 +3 +4 +3 +4 +3 +5 +2 +13 +5 +5 +3 +3 +23 +4 +4 +5 +7 +4 +13 +2 +4 +3 +4 +2 +6 +2 +7 +3 +5 +5 +3 +29 +5 +4 +4 +3 +10 +2 +3 +79 +16 +6 +6 +7 +7 +3 +5 +5 +7 +4 +3 +7 +9 +5 +6 +5 +9 +6 +3 +6 +4 +17 +2 +10 +9 +3 +6 +2 +3 +21 +22 +5 +11 +4 +2 +17 +2 +224 +2 +14 +3 +4 +4 +2 +4 +4 +4 +4 +5 +3 +4 +4 +10 +2 +6 +3 +3 +5 +7 +2 +7 +5 +6 +3 +218 +2 +2 +5 +2 +6 +3 +5 +222 +14 +6 +33 +3 +2 +5 +3 +3 +3 +9 +5 +3 +3 +2 +7 +4 +3 +4 +3 +5 +6 +5 +26 +4 +13 +9 +7 +3 +221 +3 +3 +4 +4 +4 +4 +2 +18 +5 +3 +7 +9 +6 +8 +3 +10 +3 +11 +9 +5 +4 +17 +5 +5 +6 +6 +3 +2 +4 +12 +17 +6 +7 +218 +4 +2 +4 +10 +3 +5 +15 +3 +9 +4 +3 +3 +6 +29 +3 +3 +4 +5 +5 +3 +8 +5 +6 +6 +7 +5 +3 +5 +3 +29 +2 +31 +5 +15 +24 +16 +5 +207 +4 +3 +3 +2 +15 +4 +4 +13 +5 +5 +4 +6 +10 +2 +7 +8 +4 +6 +20 +5 +3 +4 +3 +12 +12 +5 +17 +7 +3 +3 +3 +6 +10 +3 +5 +25 +80 +4 +9 +3 +2 +11 +3 +3 +2 +3 +8 +7 +5 +5 +19 +5 +3 +3 +12 +11 +2 +6 +5 +5 +5 +3 +3 +3 +4 +209 +14 +3 +2 +5 +19 +4 +4 +3 +4 +14 +5 +6 +4 +13 +9 +7 +4 +7 +10 +2 +9 +5 +7 +2 +8 +4 +6 +5 +5 +222 +8 +7 +12 +5 +216 +3 +4 +4 +6 +3 +14 +8 +7 +13 +4 +3 +3 +3 +3 +17 +5 +4 +3 +33 +6 +6 +33 +7 +5 +3 +8 +7 +5 +2 +9 +4 +2 +233 +24 +7 +4 +8 +10 +3 +4 +15 +2 +16 +3 +3 +13 +12 +7 +5 +4 +207 +4 +2 +4 +27 +15 +2 +5 +2 +25 +6 +5 +5 +6 +13 +6 +18 +6 +4 +12 +225 +10 +7 +5 +2 +2 +11 +4 +14 +21 +8 +10 +3 +5 +4 +232 +2 +5 +5 +3 +7 +17 +11 +6 +6 +23 +4 +6 +3 +5 +4 +2 +17 +3 +6 +5 +8 +3 +2 +2 +14 +9 +4 +4 +2 +5 +5 +3 +7 +6 +12 +6 +10 +3 +6 +2 +2 +19 +5 +4 +4 +9 +2 +4 +13 +3 +5 +6 +3 +6 +5 +4 +9 +6 +3 +5 +7 +3 +6 +6 +4 +3 +10 +6 +3 +221 +3 +5 +3 +6 +4 +8 +5 +3 +6 +4 +4 +2 +54 +5 +6 +11 +3 +3 +4 +4 +4 +3 +7 +3 +11 +11 +7 +10 +6 +13 +223 +213 +15 +231 +7 +3 +7 +228 +2 +3 +4 +4 +5 +6 +7 +4 +13 +3 +4 +5 +3 +6 +4 +6 +7 +2 +4 +3 +4 +3 +3 +6 +3 +7 +3 +5 +18 +5 +6 +8 +10 +3 +3 +3 +2 +4 +2 +4 +4 +5 +6 +6 +4 +10 +13 +3 +12 +5 +12 +16 +8 +4 +19 +11 +2 +4 +5 +6 +8 +5 +6 +4 +18 +10 +4 +2 +216 +6 +6 +6 +2 +4 +12 +8 +3 +11 +5 +6 +14 +5 +3 +13 +4 +5 +4 +5 +3 +28 +6 +3 +7 +219 +3 +9 +7 +3 +10 +6 +3 +4 +19 +5 +7 +11 +6 +15 +19 +4 +13 +11 +3 +7 +5 +10 +2 +8 +11 +2 +6 +4 +6 +24 +6 +3 +3 +3 +3 +6 +18 +4 +11 +4 +2 +5 +10 +8 +3 +9 +5 +3 +4 +5 +6 +2 +5 +7 +4 +4 +14 +6 +4 +4 +5 +5 +7 +2 +4 +3 +7 +3 +3 +6 +4 +5 +4 +4 +4 +3 +3 +3 +3 +8 +14 +2 +3 +5 +3 +2 +4 +5 +3 +7 +3 +3 +18 +3 +4 +4 +5 +7 +3 +3 +3 +13 +5 +4 +8 +211 +5 +5 +3 +5 +2 +5 +4 +2 +655 +6 +3 +5 +11 +2 +5 +3 +12 +9 +15 +11 +5 +12 +217 +2 +6 +17 +3 +3 +207 +5 +5 +4 +5 +9 +3 +2 +8 +5 +4 +3 +2 +5 +12 +4 +14 +5 +4 +2 +13 +5 +8 +4 +225 +4 +3 +4 +5 +4 +3 +3 +6 +23 +9 +2 +6 +7 +233 +4 +4 +6 +18 +3 +4 +6 +3 +4 +4 +2 +3 +7 +4 +13 +227 +4 +3 +5 +4 +2 +12 +9 +17 +3 +7 +14 +6 +4 +5 +21 +4 +8 +9 +2 +9 +25 +16 +3 +6 +4 +7 +8 +5 +2 +3 +5 +4 +3 +3 +5 +3 +3 +3 +2 +3 +19 +2 +4 +3 +4 +2 +3 +4 +4 +2 +4 +3 +3 +3 +2 +6 +3 +17 +5 +6 +4 +3 +13 +5 +3 +3 +3 +4 +9 +4 +2 +14 +12 +4 +5 +24 +4 +3 +37 +12 +11 +21 +3 +4 +3 +13 +4 +2 +3 +15 +4 +11 +4 +4 +3 +8 +3 +4 +4 +12 +8 +5 +3 +3 +4 +2 +220 +3 +5 +223 +3 +3 +3 +10 +3 +15 +4 +241 +9 +7 +3 +6 +6 +23 +4 +13 +7 +3 +4 +7 +4 +9 +3 +3 +4 +10 +5 +5 +1 +5 +24 +2 +4 +5 +5 +6 +14 +3 +8 +2 +3 +5 +13 +13 +3 +5 +2 +3 +15 +3 +4 +2 +10 +4 +4 +4 +5 +5 +3 +5 +3 +4 +7 +4 +27 +3 +6 +4 +15 +3 +5 +6 +6 +5 +4 +8 +3 +9 +2 +6 +3 +4 +3 +7 +4 +18 +3 +11 +3 +3 +8 +9 +7 +24 +3 +219 +7 +10 +4 +5 +9 +12 +2 +5 +4 +4 +4 +3 +3 +19 +5 +8 +16 +8 +6 +22 +3 +23 +3 +242 +9 +4 +3 +3 +5 +7 +3 +3 +5 +8 +3 +7 +5 +14 +8 +10 +3 +4 +3 +7 +4 +6 +7 +4 +10 +4 +3 +11 +3 +7 +10 +3 +13 +6 +8 +12 +10 +5 +7 +9 +3 +4 +7 +7 +10 +8 +30 +9 +19 +4 +3 +19 +15 +4 +13 +3 +215 +223 +4 +7 +4 +8 +17 +16 +3 +7 +6 +5 +5 +4 +12 +3 +7 +4 +4 +13 +4 +5 +2 +5 +6 +5 +6 +6 +7 +10 +18 +23 +9 +3 +3 +6 +5 +2 +4 +2 +7 +3 +3 +2 +5 +5 +14 +10 +224 +6 +3 +4 +3 +7 +5 +9 +3 +6 +4 +2 +5 +11 +4 +3 +3 +2 +8 +4 +7 +4 +10 +7 +3 +3 +18 +18 +17 +3 +3 +3 +4 +5 +3 +3 +4 +12 +7 +3 +11 +13 +5 +4 +7 +13 +5 +4 +11 +3 +12 +3 +6 +4 +4 +21 +4 +6 +9 +5 +3 +10 +8 +4 +6 +4 +4 +6 +5 +4 +8 +6 +4 +6 +4 +4 +5 +9 +6 +3 +4 +2 +9 +3 +18 +2 +4 +3 +13 +3 +6 +6 +8 +7 +9 +3 +2 +16 +3 +4 +6 +3 +2 +33 +22 +14 +4 +9 +12 +4 +5 +6 +3 +23 +9 +4 +3 +5 +5 +3 +4 +5 +3 +5 +3 +10 +4 +5 +5 +8 +4 +4 +6 +8 +5 +4 +3 +4 +6 +3 +3 +3 +5 +9 +12 +6 +5 +9 +3 +5 +3 +2 +2 +2 +18 +3 +2 +21 +2 +5 +4 +6 +4 +5 +10 +3 +9 +3 +2 +10 +7 +3 +6 +6 +4 +4 +8 +12 +7 +3 +7 +3 +3 +9 +3 +4 +5 +4 +4 +5 +5 +10 +15 +4 +4 +14 +6 +227 +3 +14 +5 +216 +22 +5 +4 +2 +2 +6 +3 +4 +2 +9 +9 +4 +3 +28 +13 +11 +4 +5 +3 +3 +2 +3 +3 +5 +3 +4 +3 +5 +23 +26 +3 +4 +5 +6 +4 +6 +3 +5 +5 +3 +4 +3 +2 +2 +2 +7 +14 +3 +6 +7 +17 +2 +2 +15 +14 +16 +4 +6 +7 +13 +6 +4 +5 +6 +16 +3 +3 +28 +3 +6 +15 +3 +9 +2 +4 +6 +3 +3 +22 +4 +12 +6 +7 +2 +5 +4 +10 +3 +16 +6 +9 +2 +5 +12 +7 +5 +5 +5 +5 +2 +11 +9 +17 +4 +3 +11 +7 +3 +5 +15 +4 +3 +4 +211 +8 +7 +5 +4 +7 +6 +7 +6 +3 +6 +5 +6 +5 +3 +4 +4 +26 +4 +6 +10 +4 +4 +3 +2 +3 +3 +4 +5 +9 +3 +9 +4 +4 +5 +5 +8 +2 +4 +2 +3 +8 +4 +11 +19 +5 +8 +6 +3 +5 +6 +12 +3 +2 +4 +16 +12 +3 +4 +4 +8 +6 +5 +6 +6 +219 +8 +222 +6 +16 +3 +13 +19 +5 +4 +3 +11 +6 +10 +4 +7 +7 +12 +5 +3 +3 +5 +6 +10 +3 +8 +2 +5 +4 +7 +2 +4 +4 +2 +12 +9 +6 +4 +2 +40 +2 +4 +10 +4 +223 +4 +2 +20 +6 +7 +24 +5 +4 +5 +2 +20 +16 +6 +5 +13 +2 +3 +3 +19 +3 +2 +4 +5 +6 +7 +11 +12 +5 +6 +7 +7 +3 +5 +3 +5 +3 +14 +3 +4 +4 +2 +11 +1 +7 +3 +9 +6 +11 +12 +5 +8 +6 +221 +4 +2 +12 +4 +3 +15 +4 +5 +226 +7 +218 +7 +5 +4 +5 +18 +4 +5 +9 +4 +4 +2 +9 +18 +18 +9 +5 +6 +6 +3 +3 +7 +3 +5 +4 +4 +4 +12 +3 +6 +31 +5 +4 +7 +3 +6 +5 +6 +5 +11 +2 +2 +11 +11 +6 +7 +5 +8 +7 +10 +5 +23 +7 +4 +3 +5 +34 +2 +5 +23 +7 +3 +6 +8 +4 +4 +4 +2 +5 +3 +8 +5 +4 +8 +25 +2 +3 +17 +8 +3 +4 +8 +7 +3 +15 +6 +5 +7 +21 +9 +5 +6 +6 +5 +3 +2 +3 +10 +3 +6 +3 +14 +7 +4 +4 +8 +7 +8 +2 +6 +12 +4 +213 +6 +5 +21 +8 +2 +5 +23 +3 +11 +2 +3 +6 +25 +2 +3 +6 +7 +6 +6 +4 +4 +6 +3 +17 +9 +7 +6 +4 +3 +10 +7 +2 +3 +3 +3 +11 +8 +3 +7 +6 +4 +14 +36 +3 +4 +3 +3 +22 +13 +21 +4 +2 +7 +4 +4 +17 +15 +3 +7 +11 +2 +4 +7 +6 +209 +6 +3 +2 +2 +24 +4 +9 +4 +3 +3 +3 +29 +2 +2 +4 +3 +3 +5 +4 +6 +3 +3 +2 +4 diff --git a/vendor/github.com/beorn7/perks/quantile/stream.go b/vendor/github.com/beorn7/perks/quantile/stream.go new file mode 100644 index 00000000..d7d14f8e --- /dev/null +++ b/vendor/github.com/beorn7/perks/quantile/stream.go @@ -0,0 +1,316 @@ +// Package quantile computes approximate quantiles over an unbounded data +// stream within low memory and CPU bounds. +// +// A small amount of accuracy is traded to achieve the above properties. +// +// Multiple streams can be merged before calling Query to generate a single set +// of results. This is meaningful when the streams represent the same type of +// data. See Merge and Samples. +// +// For more detailed information about the algorithm used, see: +// +// Effective Computation of Biased Quantiles over Data Streams +// +// http://www.cs.rutgers.edu/~muthu/bquant.pdf +package quantile + +import ( + "math" + "sort" +) + +// Sample holds an observed value and meta information for compression. JSON +// tags have been added for convenience. +type Sample struct { + Value float64 `json:",string"` + Width float64 `json:",string"` + Delta float64 `json:",string"` +} + +// Samples represents a slice of samples. It implements sort.Interface. +type Samples []Sample + +func (a Samples) Len() int { return len(a) } +func (a Samples) Less(i, j int) bool { return a[i].Value < a[j].Value } +func (a Samples) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +type invariant func(s *stream, r float64) float64 + +// NewLowBiased returns an initialized Stream for low-biased quantiles +// (e.g. 0.01, 0.1, 0.5) where the needed quantiles are not known a priori, but +// error guarantees can still be given even for the lower ranks of the data +// distribution. +// +// The provided epsilon is a relative error, i.e. the true quantile of a value +// returned by a query is guaranteed to be within (1±Epsilon)*Quantile. +// +// See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error +// properties. +func NewLowBiased(epsilon float64) *Stream { + Æ’ := func(s *stream, r float64) float64 { + return 2 * epsilon * r + } + return newStream(Æ’) +} + +// NewHighBiased returns an initialized Stream for high-biased quantiles +// (e.g. 0.01, 0.1, 0.5) where the needed quantiles are not known a priori, but +// error guarantees can still be given even for the higher ranks of the data +// distribution. +// +// The provided epsilon is a relative error, i.e. the true quantile of a value +// returned by a query is guaranteed to be within 1-(1±Epsilon)*(1-Quantile). +// +// See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error +// properties. +func NewHighBiased(epsilon float64) *Stream { + Æ’ := func(s *stream, r float64) float64 { + return 2 * epsilon * (s.n - r) + } + return newStream(Æ’) +} + +// NewTargeted returns an initialized Stream concerned with a particular set of +// quantile values that are supplied a priori. Knowing these a priori reduces +// space and computation time. The targets map maps the desired quantiles to +// their absolute errors, i.e. the true quantile of a value returned by a query +// is guaranteed to be within (Quantile±Epsilon). +// +// See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error properties. +func NewTargeted(targetMap map[float64]float64) *Stream { + // Convert map to slice to avoid slow iterations on a map. + // Æ’ is called on the hot path, so converting the map to a slice + // beforehand results in significant CPU savings. + targets := targetMapToSlice(targetMap) + + Æ’ := func(s *stream, r float64) float64 { + var m = math.MaxFloat64 + var f float64 + for _, t := range targets { + if t.quantile*s.n <= r { + f = (2 * t.epsilon * r) / t.quantile + } else { + f = (2 * t.epsilon * (s.n - r)) / (1 - t.quantile) + } + if f < m { + m = f + } + } + return m + } + return newStream(Æ’) +} + +type target struct { + quantile float64 + epsilon float64 +} + +func targetMapToSlice(targetMap map[float64]float64) []target { + targets := make([]target, 0, len(targetMap)) + + for quantile, epsilon := range targetMap { + t := target{ + quantile: quantile, + epsilon: epsilon, + } + targets = append(targets, t) + } + + return targets +} + +// Stream computes quantiles for a stream of float64s. It is not thread-safe by +// design. Take care when using across multiple goroutines. +type Stream struct { + *stream + b Samples + sorted bool +} + +func newStream(Æ’ invariant) *Stream { + x := &stream{Æ’: Æ’} + return &Stream{x, make(Samples, 0, 500), true} +} + +// Insert inserts v into the stream. +func (s *Stream) Insert(v float64) { + s.insert(Sample{Value: v, Width: 1}) +} + +func (s *Stream) insert(sample Sample) { + s.b = append(s.b, sample) + s.sorted = false + if len(s.b) == cap(s.b) { + s.flush() + } +} + +// Query returns the computed qth percentiles value. If s was created with +// NewTargeted, and q is not in the set of quantiles provided a priori, Query +// will return an unspecified result. +func (s *Stream) Query(q float64) float64 { + if !s.flushed() { + // Fast path when there hasn't been enough data for a flush; + // this also yields better accuracy for small sets of data. + l := len(s.b) + if l == 0 { + return 0 + } + i := int(math.Ceil(float64(l) * q)) + if i > 0 { + i -= 1 + } + s.maybeSort() + return s.b[i].Value + } + s.flush() + return s.stream.query(q) +} + +// Merge merges samples into the underlying streams samples. This is handy when +// merging multiple streams from separate threads, database shards, etc. +// +// ATTENTION: This method is broken and does not yield correct results. The +// underlying algorithm is not capable of merging streams correctly. +func (s *Stream) Merge(samples Samples) { + sort.Sort(samples) + s.stream.merge(samples) +} + +// Reset reinitializes and clears the list reusing the samples buffer memory. +func (s *Stream) Reset() { + s.stream.reset() + s.b = s.b[:0] +} + +// Samples returns stream samples held by s. +func (s *Stream) Samples() Samples { + if !s.flushed() { + return s.b + } + s.flush() + return s.stream.samples() +} + +// Count returns the total number of samples observed in the stream +// since initialization. +func (s *Stream) Count() int { + return len(s.b) + s.stream.count() +} + +func (s *Stream) flush() { + s.maybeSort() + s.stream.merge(s.b) + s.b = s.b[:0] +} + +func (s *Stream) maybeSort() { + if !s.sorted { + s.sorted = true + sort.Sort(s.b) + } +} + +func (s *Stream) flushed() bool { + return len(s.stream.l) > 0 +} + +type stream struct { + n float64 + l []Sample + Æ’ invariant +} + +func (s *stream) reset() { + s.l = s.l[:0] + s.n = 0 +} + +func (s *stream) insert(v float64) { + s.merge(Samples{{v, 1, 0}}) +} + +func (s *stream) merge(samples Samples) { + // TODO(beorn7): This tries to merge not only individual samples, but + // whole summaries. The paper doesn't mention merging summaries at + // all. Unittests show that the merging is inaccurate. Find out how to + // do merges properly. + var r float64 + i := 0 + for _, sample := range samples { + for ; i < len(s.l); i++ { + c := s.l[i] + if c.Value > sample.Value { + // Insert at position i. + s.l = append(s.l, Sample{}) + copy(s.l[i+1:], s.l[i:]) + s.l[i] = Sample{ + sample.Value, + sample.Width, + math.Max(sample.Delta, math.Floor(s.Æ’(s, r))-1), + // TODO(beorn7): How to calculate delta correctly? + } + i++ + goto inserted + } + r += c.Width + } + s.l = append(s.l, Sample{sample.Value, sample.Width, 0}) + i++ + inserted: + s.n += sample.Width + r += sample.Width + } + s.compress() +} + +func (s *stream) count() int { + return int(s.n) +} + +func (s *stream) query(q float64) float64 { + t := math.Ceil(q * s.n) + t += math.Ceil(s.Æ’(s, t) / 2) + p := s.l[0] + var r float64 + for _, c := range s.l[1:] { + r += p.Width + if r+c.Width+c.Delta > t { + return p.Value + } + p = c + } + return p.Value +} + +func (s *stream) compress() { + if len(s.l) < 2 { + return + } + x := s.l[len(s.l)-1] + xi := len(s.l) - 1 + r := s.n - 1 - x.Width + + for i := len(s.l) - 2; i >= 0; i-- { + c := s.l[i] + if c.Width+x.Width+x.Delta <= s.Æ’(s, r) { + x.Width += c.Width + s.l[xi] = x + // Remove element at i. + copy(s.l[i:], s.l[i+1:]) + s.l = s.l[:len(s.l)-1] + xi -= 1 + } else { + x = c + xi = i + } + r -= c.Width + } +} + +func (s *stream) samples() Samples { + samples := make(Samples, len(s.l)) + copy(samples, s.l) + return samples +} diff --git a/vendor/github.com/bradfitz/gomemcache/LICENSE b/vendor/github.com/bradfitz/gomemcache/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/vendor/github.com/bradfitz/gomemcache/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/bradfitz/gomemcache/memcache/memcache.go b/vendor/github.com/bradfitz/gomemcache/memcache/memcache.go new file mode 100644 index 00000000..545a3e79 --- /dev/null +++ b/vendor/github.com/bradfitz/gomemcache/memcache/memcache.go @@ -0,0 +1,718 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package memcache provides a client for the memcached cache server. +package memcache + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + "net" + + "strconv" + "strings" + "sync" + "time" +) + +// Similar to: +// https://godoc.org/google.golang.org/appengine/memcache + +var ( + // ErrCacheMiss means that a Get failed because the item wasn't present. + ErrCacheMiss = errors.New("memcache: cache miss") + + // ErrCASConflict means that a CompareAndSwap call failed due to the + // cached value being modified between the Get and the CompareAndSwap. + // If the cached value was simply evicted rather than replaced, + // ErrNotStored will be returned instead. + ErrCASConflict = errors.New("memcache: compare-and-swap conflict") + + // ErrNotStored means that a conditional write operation (i.e. Add or + // CompareAndSwap) failed because the condition was not satisfied. + ErrNotStored = errors.New("memcache: item not stored") + + // ErrServer means that a server error occurred. + ErrServerError = errors.New("memcache: server error") + + // ErrNoStats means that no statistics were available. + ErrNoStats = errors.New("memcache: no statistics available") + + // ErrMalformedKey is returned when an invalid key is used. + // Keys must be at maximum 250 bytes long and not + // contain whitespace or control characters. + ErrMalformedKey = errors.New("malformed: key is too long or contains invalid characters") + + // ErrNoServers is returned when no servers are configured or available. + ErrNoServers = errors.New("memcache: no servers configured or available") +) + +const ( + // DefaultTimeout is the default socket read/write timeout. + DefaultTimeout = 100 * time.Millisecond + + // DefaultMaxIdleConns is the default maximum number of idle connections + // kept for any single address. + DefaultMaxIdleConns = 2 +) + +const buffered = 8 // arbitrary buffered channel size, for readability + +// resumableError returns true if err is only a protocol-level cache error. +// This is used to determine whether or not a server connection should +// be re-used or not. If an error occurs, by default we don't reuse the +// connection, unless it was just a cache error. +func resumableError(err error) bool { + switch err { + case ErrCacheMiss, ErrCASConflict, ErrNotStored, ErrMalformedKey: + return true + } + return false +} + +func legalKey(key string) bool { + if len(key) > 250 { + return false + } + for i := 0; i < len(key); i++ { + if key[i] <= ' ' || key[i] == 0x7f { + return false + } + } + return true +} + +var ( + crlf = []byte("\r\n") + space = []byte(" ") + resultOK = []byte("OK\r\n") + resultStored = []byte("STORED\r\n") + resultNotStored = []byte("NOT_STORED\r\n") + resultExists = []byte("EXISTS\r\n") + resultNotFound = []byte("NOT_FOUND\r\n") + resultDeleted = []byte("DELETED\r\n") + resultEnd = []byte("END\r\n") + resultOk = []byte("OK\r\n") + resultTouched = []byte("TOUCHED\r\n") + + resultClientErrorPrefix = []byte("CLIENT_ERROR ") + versionPrefix = []byte("VERSION") +) + +// New returns a memcache client using the provided server(s) +// with equal weight. If a server is listed multiple times, +// it gets a proportional amount of weight. +func New(server ...string) *Client { + ss := new(ServerList) + ss.SetServers(server...) + return NewFromSelector(ss) +} + +// NewFromSelector returns a new Client using the provided ServerSelector. +func NewFromSelector(ss ServerSelector) *Client { + return &Client{selector: ss} +} + +// Client is a memcache client. +// It is safe for unlocked use by multiple concurrent goroutines. +type Client struct { + // Timeout specifies the socket read/write timeout. + // If zero, DefaultTimeout is used. + Timeout time.Duration + + // MaxIdleConns specifies the maximum number of idle connections that will + // be maintained per address. If less than one, DefaultMaxIdleConns will be + // used. + // + // Consider your expected traffic rates and latency carefully. This should + // be set to a number higher than your peak parallel requests. + MaxIdleConns int + + selector ServerSelector + + lk sync.Mutex + freeconn map[string][]*conn +} + +// Item is an item to be got or stored in a memcached server. +type Item struct { + // Key is the Item's key (250 bytes maximum). + Key string + + // Value is the Item's value. + Value []byte + + // Flags are server-opaque flags whose semantics are entirely + // up to the app. + Flags uint32 + + // Expiration is the cache expiration time, in seconds: either a relative + // time from now (up to 1 month), or an absolute Unix epoch time. + // Zero means the Item has no expiration time. + Expiration int32 + + // Compare and swap ID. + casid uint64 +} + +// conn is a connection to a server. +type conn struct { + nc net.Conn + rw *bufio.ReadWriter + addr net.Addr + c *Client +} + +// release returns this connection back to the client's free pool +func (cn *conn) release() { + cn.c.putFreeConn(cn.addr, cn) +} + +func (cn *conn) extendDeadline() { + cn.nc.SetDeadline(time.Now().Add(cn.c.netTimeout())) +} + +// condRelease releases this connection if the error pointed to by err +// is nil (not an error) or is only a protocol level error (e.g. a +// cache miss). The purpose is to not recycle TCP connections that +// are bad. +func (cn *conn) condRelease(err *error) { + if *err == nil || resumableError(*err) { + cn.release() + } else { + cn.nc.Close() + } +} + +func (c *Client) putFreeConn(addr net.Addr, cn *conn) { + c.lk.Lock() + defer c.lk.Unlock() + if c.freeconn == nil { + c.freeconn = make(map[string][]*conn) + } + freelist := c.freeconn[addr.String()] + if len(freelist) >= c.maxIdleConns() { + cn.nc.Close() + return + } + c.freeconn[addr.String()] = append(freelist, cn) +} + +func (c *Client) getFreeConn(addr net.Addr) (cn *conn, ok bool) { + c.lk.Lock() + defer c.lk.Unlock() + if c.freeconn == nil { + return nil, false + } + freelist, ok := c.freeconn[addr.String()] + if !ok || len(freelist) == 0 { + return nil, false + } + cn = freelist[len(freelist)-1] + c.freeconn[addr.String()] = freelist[:len(freelist)-1] + return cn, true +} + +func (c *Client) netTimeout() time.Duration { + if c.Timeout != 0 { + return c.Timeout + } + return DefaultTimeout +} + +func (c *Client) maxIdleConns() int { + if c.MaxIdleConns > 0 { + return c.MaxIdleConns + } + return DefaultMaxIdleConns +} + +// ConnectTimeoutError is the error type used when it takes +// too long to connect to the desired host. This level of +// detail can generally be ignored. +type ConnectTimeoutError struct { + Addr net.Addr +} + +func (cte *ConnectTimeoutError) Error() string { + return "memcache: connect timeout to " + cte.Addr.String() +} + +func (c *Client) dial(addr net.Addr) (net.Conn, error) { + type connError struct { + cn net.Conn + err error + } + + nc, err := net.DialTimeout(addr.Network(), addr.String(), c.netTimeout()) + if err == nil { + return nc, nil + } + + if ne, ok := err.(net.Error); ok && ne.Timeout() { + return nil, &ConnectTimeoutError{addr} + } + + return nil, err +} + +func (c *Client) getConn(addr net.Addr) (*conn, error) { + cn, ok := c.getFreeConn(addr) + if ok { + cn.extendDeadline() + return cn, nil + } + nc, err := c.dial(addr) + if err != nil { + return nil, err + } + cn = &conn{ + nc: nc, + addr: addr, + rw: bufio.NewReadWriter(bufio.NewReader(nc), bufio.NewWriter(nc)), + c: c, + } + cn.extendDeadline() + return cn, nil +} + +func (c *Client) onItem(item *Item, fn func(*Client, *bufio.ReadWriter, *Item) error) error { + addr, err := c.selector.PickServer(item.Key) + if err != nil { + return err + } + cn, err := c.getConn(addr) + if err != nil { + return err + } + defer cn.condRelease(&err) + if err = fn(c, cn.rw, item); err != nil { + return err + } + return nil +} + +func (c *Client) FlushAll() error { + return c.selector.Each(c.flushAllFromAddr) +} + +// Get gets the item for the given key. ErrCacheMiss is returned for a +// memcache cache miss. The key must be at most 250 bytes in length. +func (c *Client) Get(key string) (item *Item, err error) { + err = c.withKeyAddr(key, func(addr net.Addr) error { + return c.getFromAddr(addr, []string{key}, func(it *Item) { item = it }) + }) + if err == nil && item == nil { + err = ErrCacheMiss + } + return +} + +// Touch updates the expiry for the given key. The seconds parameter is either +// a Unix timestamp or, if seconds is less than 1 month, the number of seconds +// into the future at which time the item will expire. Zero means the item has +// no expiration time. ErrCacheMiss is returned if the key is not in the cache. +// The key must be at most 250 bytes in length. +func (c *Client) Touch(key string, seconds int32) (err error) { + return c.withKeyAddr(key, func(addr net.Addr) error { + return c.touchFromAddr(addr, []string{key}, seconds) + }) +} + +func (c *Client) withKeyAddr(key string, fn func(net.Addr) error) (err error) { + if !legalKey(key) { + return ErrMalformedKey + } + addr, err := c.selector.PickServer(key) + if err != nil { + return err + } + return fn(addr) +} + +func (c *Client) withAddrRw(addr net.Addr, fn func(*bufio.ReadWriter) error) (err error) { + cn, err := c.getConn(addr) + if err != nil { + return err + } + defer cn.condRelease(&err) + return fn(cn.rw) +} + +func (c *Client) withKeyRw(key string, fn func(*bufio.ReadWriter) error) error { + return c.withKeyAddr(key, func(addr net.Addr) error { + return c.withAddrRw(addr, fn) + }) +} + +func (c *Client) getFromAddr(addr net.Addr, keys []string, cb func(*Item)) error { + return c.withAddrRw(addr, func(rw *bufio.ReadWriter) error { + if _, err := fmt.Fprintf(rw, "gets %s\r\n", strings.Join(keys, " ")); err != nil { + return err + } + if err := rw.Flush(); err != nil { + return err + } + if err := parseGetResponse(rw.Reader, cb); err != nil { + return err + } + return nil + }) +} + +// flushAllFromAddr send the flush_all command to the given addr +func (c *Client) flushAllFromAddr(addr net.Addr) error { + return c.withAddrRw(addr, func(rw *bufio.ReadWriter) error { + if _, err := fmt.Fprintf(rw, "flush_all\r\n"); err != nil { + return err + } + if err := rw.Flush(); err != nil { + return err + } + line, err := rw.ReadSlice('\n') + if err != nil { + return err + } + switch { + case bytes.Equal(line, resultOk): + break + default: + return fmt.Errorf("memcache: unexpected response line from flush_all: %q", string(line)) + } + return nil + }) +} + +// ping sends the version command to the given addr +func (c *Client) ping(addr net.Addr) error { + return c.withAddrRw(addr, func(rw *bufio.ReadWriter) error { + if _, err := fmt.Fprintf(rw, "version\r\n"); err != nil { + return err + } + if err := rw.Flush(); err != nil { + return err + } + line, err := rw.ReadSlice('\n') + if err != nil { + return err + } + + switch { + case bytes.HasPrefix(line, versionPrefix): + break + default: + return fmt.Errorf("memcache: unexpected response line from ping: %q", string(line)) + } + return nil + }) +} + +func (c *Client) touchFromAddr(addr net.Addr, keys []string, expiration int32) error { + return c.withAddrRw(addr, func(rw *bufio.ReadWriter) error { + for _, key := range keys { + if _, err := fmt.Fprintf(rw, "touch %s %d\r\n", key, expiration); err != nil { + return err + } + if err := rw.Flush(); err != nil { + return err + } + line, err := rw.ReadSlice('\n') + if err != nil { + return err + } + switch { + case bytes.Equal(line, resultTouched): + break + case bytes.Equal(line, resultNotFound): + return ErrCacheMiss + default: + return fmt.Errorf("memcache: unexpected response line from touch: %q", string(line)) + } + } + return nil + }) +} + +// GetMulti is a batch version of Get. The returned map from keys to +// items may have fewer elements than the input slice, due to memcache +// cache misses. Each key must be at most 250 bytes in length. +// If no error is returned, the returned map will also be non-nil. +func (c *Client) GetMulti(keys []string) (map[string]*Item, error) { + var lk sync.Mutex + m := make(map[string]*Item) + addItemToMap := func(it *Item) { + lk.Lock() + defer lk.Unlock() + m[it.Key] = it + } + + keyMap := make(map[net.Addr][]string) + for _, key := range keys { + if !legalKey(key) { + return nil, ErrMalformedKey + } + addr, err := c.selector.PickServer(key) + if err != nil { + return nil, err + } + keyMap[addr] = append(keyMap[addr], key) + } + + ch := make(chan error, buffered) + for addr, keys := range keyMap { + go func(addr net.Addr, keys []string) { + ch <- c.getFromAddr(addr, keys, addItemToMap) + }(addr, keys) + } + + var err error + for _ = range keyMap { + if ge := <-ch; ge != nil { + err = ge + } + } + return m, err +} + +// parseGetResponse reads a GET response from r and calls cb for each +// read and allocated Item +func parseGetResponse(r *bufio.Reader, cb func(*Item)) error { + for { + line, err := r.ReadSlice('\n') + if err != nil { + return err + } + if bytes.Equal(line, resultEnd) { + return nil + } + it := new(Item) + size, err := scanGetResponseLine(line, it) + if err != nil { + return err + } + it.Value = make([]byte, size+2) + _, err = io.ReadFull(r, it.Value) + if err != nil { + it.Value = nil + return err + } + if !bytes.HasSuffix(it.Value, crlf) { + it.Value = nil + return fmt.Errorf("memcache: corrupt get result read") + } + it.Value = it.Value[:size] + cb(it) + } +} + +// scanGetResponseLine populates it and returns the declared size of the item. +// It does not read the bytes of the item. +func scanGetResponseLine(line []byte, it *Item) (size int, err error) { + pattern := "VALUE %s %d %d %d\r\n" + dest := []interface{}{&it.Key, &it.Flags, &size, &it.casid} + if bytes.Count(line, space) == 3 { + pattern = "VALUE %s %d %d\r\n" + dest = dest[:3] + } + n, err := fmt.Sscanf(string(line), pattern, dest...) + if err != nil || n != len(dest) { + return -1, fmt.Errorf("memcache: unexpected line in get response: %q", line) + } + return size, nil +} + +// Set writes the given item, unconditionally. +func (c *Client) Set(item *Item) error { + return c.onItem(item, (*Client).set) +} + +func (c *Client) set(rw *bufio.ReadWriter, item *Item) error { + return c.populateOne(rw, "set", item) +} + +// Add writes the given item, if no value already exists for its +// key. ErrNotStored is returned if that condition is not met. +func (c *Client) Add(item *Item) error { + return c.onItem(item, (*Client).add) +} + +func (c *Client) add(rw *bufio.ReadWriter, item *Item) error { + return c.populateOne(rw, "add", item) +} + +// Replace writes the given item, but only if the server *does* +// already hold data for this key +func (c *Client) Replace(item *Item) error { + return c.onItem(item, (*Client).replace) +} + +func (c *Client) replace(rw *bufio.ReadWriter, item *Item) error { + return c.populateOne(rw, "replace", item) +} + +// CompareAndSwap writes the given item that was previously returned +// by Get, if the value was neither modified or evicted between the +// Get and the CompareAndSwap calls. The item's Key should not change +// between calls but all other item fields may differ. ErrCASConflict +// is returned if the value was modified in between the +// calls. ErrNotStored is returned if the value was evicted in between +// the calls. +func (c *Client) CompareAndSwap(item *Item) error { + return c.onItem(item, (*Client).cas) +} + +func (c *Client) cas(rw *bufio.ReadWriter, item *Item) error { + return c.populateOne(rw, "cas", item) +} + +func (c *Client) populateOne(rw *bufio.ReadWriter, verb string, item *Item) error { + if !legalKey(item.Key) { + return ErrMalformedKey + } + var err error + if verb == "cas" { + _, err = fmt.Fprintf(rw, "%s %s %d %d %d %d\r\n", + verb, item.Key, item.Flags, item.Expiration, len(item.Value), item.casid) + } else { + _, err = fmt.Fprintf(rw, "%s %s %d %d %d\r\n", + verb, item.Key, item.Flags, item.Expiration, len(item.Value)) + } + if err != nil { + return err + } + if _, err = rw.Write(item.Value); err != nil { + return err + } + if _, err := rw.Write(crlf); err != nil { + return err + } + if err := rw.Flush(); err != nil { + return err + } + line, err := rw.ReadSlice('\n') + if err != nil { + return err + } + switch { + case bytes.Equal(line, resultStored): + return nil + case bytes.Equal(line, resultNotStored): + return ErrNotStored + case bytes.Equal(line, resultExists): + return ErrCASConflict + case bytes.Equal(line, resultNotFound): + return ErrCacheMiss + } + return fmt.Errorf("memcache: unexpected response line from %q: %q", verb, string(line)) +} + +func writeReadLine(rw *bufio.ReadWriter, format string, args ...interface{}) ([]byte, error) { + _, err := fmt.Fprintf(rw, format, args...) + if err != nil { + return nil, err + } + if err := rw.Flush(); err != nil { + return nil, err + } + line, err := rw.ReadSlice('\n') + return line, err +} + +func writeExpectf(rw *bufio.ReadWriter, expect []byte, format string, args ...interface{}) error { + line, err := writeReadLine(rw, format, args...) + if err != nil { + return err + } + switch { + case bytes.Equal(line, resultOK): + return nil + case bytes.Equal(line, expect): + return nil + case bytes.Equal(line, resultNotStored): + return ErrNotStored + case bytes.Equal(line, resultExists): + return ErrCASConflict + case bytes.Equal(line, resultNotFound): + return ErrCacheMiss + } + return fmt.Errorf("memcache: unexpected response line: %q", string(line)) +} + +// Delete deletes the item with the provided key. The error ErrCacheMiss is +// returned if the item didn't already exist in the cache. +func (c *Client) Delete(key string) error { + return c.withKeyRw(key, func(rw *bufio.ReadWriter) error { + return writeExpectf(rw, resultDeleted, "delete %s\r\n", key) + }) +} + +// DeleteAll deletes all items in the cache. +func (c *Client) DeleteAll() error { + return c.withKeyRw("", func(rw *bufio.ReadWriter) error { + return writeExpectf(rw, resultDeleted, "flush_all\r\n") + }) +} + +// Ping checks all instances if they are alive. Returns error if any +// of them is down. +func (c *Client) Ping() error { + return c.selector.Each(c.ping) +} + +// Increment atomically increments key by delta. The return value is +// the new value after being incremented or an error. If the value +// didn't exist in memcached the error is ErrCacheMiss. The value in +// memcached must be an decimal number, or an error will be returned. +// On 64-bit overflow, the new value wraps around. +func (c *Client) Increment(key string, delta uint64) (newValue uint64, err error) { + return c.incrDecr("incr", key, delta) +} + +// Decrement atomically decrements key by delta. The return value is +// the new value after being decremented or an error. If the value +// didn't exist in memcached the error is ErrCacheMiss. The value in +// memcached must be an decimal number, or an error will be returned. +// On underflow, the new value is capped at zero and does not wrap +// around. +func (c *Client) Decrement(key string, delta uint64) (newValue uint64, err error) { + return c.incrDecr("decr", key, delta) +} + +func (c *Client) incrDecr(verb, key string, delta uint64) (uint64, error) { + var val uint64 + err := c.withKeyRw(key, func(rw *bufio.ReadWriter) error { + line, err := writeReadLine(rw, "%s %s %d\r\n", verb, key, delta) + if err != nil { + return err + } + switch { + case bytes.Equal(line, resultNotFound): + return ErrCacheMiss + case bytes.HasPrefix(line, resultClientErrorPrefix): + errMsg := line[len(resultClientErrorPrefix) : len(line)-2] + return errors.New("memcache: client error: " + string(errMsg)) + } + val, err = strconv.ParseUint(string(line[:len(line)-2]), 10, 64) + if err != nil { + return err + } + return nil + }) + return val, err +} diff --git a/vendor/github.com/bradfitz/gomemcache/memcache/selector.go b/vendor/github.com/bradfitz/gomemcache/memcache/selector.go new file mode 100644 index 00000000..89ad81e0 --- /dev/null +++ b/vendor/github.com/bradfitz/gomemcache/memcache/selector.go @@ -0,0 +1,129 @@ +/* +Copyright 2011 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package memcache + +import ( + "hash/crc32" + "net" + "strings" + "sync" +) + +// ServerSelector is the interface that selects a memcache server +// as a function of the item's key. +// +// All ServerSelector implementations must be safe for concurrent use +// by multiple goroutines. +type ServerSelector interface { + // PickServer returns the server address that a given item + // should be shared onto. + PickServer(key string) (net.Addr, error) + Each(func(net.Addr) error) error +} + +// ServerList is a simple ServerSelector. Its zero value is usable. +type ServerList struct { + mu sync.RWMutex + addrs []net.Addr +} + +// staticAddr caches the Network() and String() values from any net.Addr. +type staticAddr struct { + ntw, str string +} + +func newStaticAddr(a net.Addr) net.Addr { + return &staticAddr{ + ntw: a.Network(), + str: a.String(), + } +} + +func (s *staticAddr) Network() string { return s.ntw } +func (s *staticAddr) String() string { return s.str } + +// SetServers changes a ServerList's set of servers at runtime and is +// safe for concurrent use by multiple goroutines. +// +// Each server is given equal weight. A server is given more weight +// if it's listed multiple times. +// +// SetServers returns an error if any of the server names fail to +// resolve. No attempt is made to connect to the server. If any error +// is returned, no changes are made to the ServerList. +func (ss *ServerList) SetServers(servers ...string) error { + naddr := make([]net.Addr, len(servers)) + for i, server := range servers { + if strings.Contains(server, "/") { + addr, err := net.ResolveUnixAddr("unix", server) + if err != nil { + return err + } + naddr[i] = newStaticAddr(addr) + } else { + tcpaddr, err := net.ResolveTCPAddr("tcp", server) + if err != nil { + return err + } + naddr[i] = newStaticAddr(tcpaddr) + } + } + + ss.mu.Lock() + defer ss.mu.Unlock() + ss.addrs = naddr + return nil +} + +// Each iterates over each server calling the given function +func (ss *ServerList) Each(f func(net.Addr) error) error { + ss.mu.RLock() + defer ss.mu.RUnlock() + for _, a := range ss.addrs { + if err := f(a); nil != err { + return err + } + } + return nil +} + +// keyBufPool returns []byte buffers for use by PickServer's call to +// crc32.ChecksumIEEE to avoid allocations. (but doesn't avoid the +// copies, which at least are bounded in size and small) +var keyBufPool = sync.Pool{ + New: func() interface{} { + b := make([]byte, 256) + return &b + }, +} + +func (ss *ServerList) PickServer(key string) (net.Addr, error) { + ss.mu.RLock() + defer ss.mu.RUnlock() + if len(ss.addrs) == 0 { + return nil, ErrNoServers + } + if len(ss.addrs) == 1 { + return ss.addrs[0], nil + } + bufp := keyBufPool.Get().(*[]byte) + n := copy(*bufp, key) + cs := crc32.ChecksumIEEE((*bufp)[:n]) + keyBufPool.Put(bufp) + + return ss.addrs[cs%uint32(len(ss.addrs))], nil +} diff --git a/vendor/github.com/cespare/xxhash/LICENSE.txt b/vendor/github.com/cespare/xxhash/LICENSE.txt new file mode 100644 index 00000000..24b53065 --- /dev/null +++ b/vendor/github.com/cespare/xxhash/LICENSE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2016 Caleb Spare + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/cespare/xxhash/README.md b/vendor/github.com/cespare/xxhash/README.md new file mode 100644 index 00000000..0982fd25 --- /dev/null +++ b/vendor/github.com/cespare/xxhash/README.md @@ -0,0 +1,50 @@ +# xxhash + +[![GoDoc](https://godoc.org/github.com/cespare/xxhash?status.svg)](https://godoc.org/github.com/cespare/xxhash) + +xxhash is a Go implementation of the 64-bit +[xxHash](http://cyan4973.github.io/xxHash/) algorithm, XXH64. This is a +high-quality hashing algorithm that is much faster than anything in the Go +standard library. + +The API is very small, taking its cue from the other hashing packages in the +standard library: + + $ go doc github.com/cespare/xxhash ! + package xxhash // import "github.com/cespare/xxhash" + + Package xxhash implements the 64-bit variant of xxHash (XXH64) as described + at http://cyan4973.github.io/xxHash/. + + func New() hash.Hash64 + func Sum64(b []byte) uint64 + func Sum64String(s string) uint64 + +This implementation provides a fast pure-Go implementation and an even faster +assembly implementation for amd64. + +## Benchmarks + +Here are some quick benchmarks comparing the pure-Go and assembly +implementations of Sum64 against another popular Go XXH64 implementation, +[github.com/OneOfOne/xxhash](https://github.com/OneOfOne/xxhash): + +| input size | OneOfOne | cespare (purego) | cespare | +| --- | --- | --- | --- | +| 5 B | 416 MB/s | 720 MB/s | 872 MB/s | +| 100 B | 3980 MB/s | 5013 MB/s | 5252 MB/s | +| 4 KB | 12727 MB/s | 12999 MB/s | 13026 MB/s | +| 10 MB | 9879 MB/s | 10775 MB/s | 10913 MB/s | + +These numbers were generated with: + +``` +$ go test -benchtime 10s -bench '/OneOfOne,' +$ go test -tags purego -benchtime 10s -bench '/xxhash,' +$ go test -benchtime 10s -bench '/xxhash,' +``` + +## Projects using this package + +- [InfluxDB](https://github.com/influxdata/influxdb) +- [Prometheus](https://github.com/prometheus/prometheus) diff --git a/vendor/github.com/cespare/xxhash/go.mod b/vendor/github.com/cespare/xxhash/go.mod new file mode 100644 index 00000000..10605a6a --- /dev/null +++ b/vendor/github.com/cespare/xxhash/go.mod @@ -0,0 +1,6 @@ +module github.com/cespare/xxhash + +require ( + github.com/OneOfOne/xxhash v1.2.2 + github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 +) diff --git a/vendor/github.com/cespare/xxhash/go.sum b/vendor/github.com/cespare/xxhash/go.sum new file mode 100644 index 00000000..f6b55426 --- /dev/null +++ b/vendor/github.com/cespare/xxhash/go.sum @@ -0,0 +1,4 @@ +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= diff --git a/vendor/github.com/cespare/xxhash/rotate.go b/vendor/github.com/cespare/xxhash/rotate.go new file mode 100644 index 00000000..f3eac5eb --- /dev/null +++ b/vendor/github.com/cespare/xxhash/rotate.go @@ -0,0 +1,14 @@ +// +build !go1.9 + +package xxhash + +// TODO(caleb): After Go 1.10 comes out, remove this fallback code. + +func rol1(x uint64) uint64 { return (x << 1) | (x >> (64 - 1)) } +func rol7(x uint64) uint64 { return (x << 7) | (x >> (64 - 7)) } +func rol11(x uint64) uint64 { return (x << 11) | (x >> (64 - 11)) } +func rol12(x uint64) uint64 { return (x << 12) | (x >> (64 - 12)) } +func rol18(x uint64) uint64 { return (x << 18) | (x >> (64 - 18)) } +func rol23(x uint64) uint64 { return (x << 23) | (x >> (64 - 23)) } +func rol27(x uint64) uint64 { return (x << 27) | (x >> (64 - 27)) } +func rol31(x uint64) uint64 { return (x << 31) | (x >> (64 - 31)) } diff --git a/vendor/github.com/cespare/xxhash/rotate19.go b/vendor/github.com/cespare/xxhash/rotate19.go new file mode 100644 index 00000000..b99612ba --- /dev/null +++ b/vendor/github.com/cespare/xxhash/rotate19.go @@ -0,0 +1,14 @@ +// +build go1.9 + +package xxhash + +import "math/bits" + +func rol1(x uint64) uint64 { return bits.RotateLeft64(x, 1) } +func rol7(x uint64) uint64 { return bits.RotateLeft64(x, 7) } +func rol11(x uint64) uint64 { return bits.RotateLeft64(x, 11) } +func rol12(x uint64) uint64 { return bits.RotateLeft64(x, 12) } +func rol18(x uint64) uint64 { return bits.RotateLeft64(x, 18) } +func rol23(x uint64) uint64 { return bits.RotateLeft64(x, 23) } +func rol27(x uint64) uint64 { return bits.RotateLeft64(x, 27) } +func rol31(x uint64) uint64 { return bits.RotateLeft64(x, 31) } diff --git a/vendor/github.com/cespare/xxhash/v2/.travis.yml b/vendor/github.com/cespare/xxhash/v2/.travis.yml new file mode 100644 index 00000000..c516ea88 --- /dev/null +++ b/vendor/github.com/cespare/xxhash/v2/.travis.yml @@ -0,0 +1,8 @@ +language: go +go: + - "1.x" + - master +env: + - TAGS="" + - TAGS="-tags purego" +script: go test $TAGS -v ./... diff --git a/vendor/github.com/cespare/xxhash/v2/LICENSE.txt b/vendor/github.com/cespare/xxhash/v2/LICENSE.txt new file mode 100644 index 00000000..24b53065 --- /dev/null +++ b/vendor/github.com/cespare/xxhash/v2/LICENSE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2016 Caleb Spare + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/cespare/xxhash/v2/README.md b/vendor/github.com/cespare/xxhash/v2/README.md new file mode 100644 index 00000000..cbcc6ea5 --- /dev/null +++ b/vendor/github.com/cespare/xxhash/v2/README.md @@ -0,0 +1,55 @@ +# xxhash + +[![GoDoc](https://godoc.org/github.com/cespare/xxhash?status.svg)](https://godoc.org/github.com/cespare/xxhash) +[![Build Status](https://travis-ci.org/cespare/xxhash.svg?branch=master)](https://travis-ci.org/cespare/xxhash) + +xxhash is a Go implementation of the 64-bit +[xxHash](http://cyan4973.github.io/xxHash/) algorithm, XXH64. This is a +high-quality hashing algorithm that is much faster than anything in the Go +standard library. + +This package provides a straightforward API: + +``` +func Sum64(b []byte) uint64 +func Sum64String(s string) uint64 +type Digest struct{ ... } + func New() *Digest +``` + +The `Digest` type implements hash.Hash64. Its key methods are: + +``` +func (*Digest) Write([]byte) (int, error) +func (*Digest) WriteString(string) (int, error) +func (*Digest) Sum64() uint64 +``` + +This implementation provides a fast pure-Go implementation and an even faster +assembly implementation for amd64. + +## Benchmarks + +Here are some quick benchmarks comparing the pure-Go and assembly +implementations of Sum64. + +| input size | purego | asm | +| --- | --- | --- | +| 5 B | 979.66 MB/s | 1291.17 MB/s | +| 100 B | 7475.26 MB/s | 7973.40 MB/s | +| 4 KB | 17573.46 MB/s | 17602.65 MB/s | +| 10 MB | 17131.46 MB/s | 17142.16 MB/s | + +These numbers were generated on Ubuntu 18.04 with an Intel i7-8700K CPU using +the following commands under Go 1.11.2: + +``` +$ go test -tags purego -benchtime 10s -bench '/xxhash,direct,bytes' +$ go test -benchtime 10s -bench '/xxhash,direct,bytes' +``` + +## Projects using this package + +- [InfluxDB](https://github.com/influxdata/influxdb) +- [Prometheus](https://github.com/prometheus/prometheus) +- [FreeCache](https://github.com/coocood/freecache) diff --git a/vendor/github.com/cespare/xxhash/v2/go.mod b/vendor/github.com/cespare/xxhash/v2/go.mod new file mode 100644 index 00000000..e81cef10 --- /dev/null +++ b/vendor/github.com/cespare/xxhash/v2/go.mod @@ -0,0 +1,3 @@ +module github.com/cespare/xxhash/v2 + +go 1.13 diff --git a/vendor/github.com/cespare/xxhash/v2/go.sum b/vendor/github.com/cespare/xxhash/v2/go.sum new file mode 100644 index 00000000..e69de29b diff --git a/vendor/github.com/cespare/xxhash/v2/xxhash.go b/vendor/github.com/cespare/xxhash/v2/xxhash.go new file mode 100644 index 00000000..db0b35fb --- /dev/null +++ b/vendor/github.com/cespare/xxhash/v2/xxhash.go @@ -0,0 +1,236 @@ +// Package xxhash implements the 64-bit variant of xxHash (XXH64) as described +// at http://cyan4973.github.io/xxHash/. +package xxhash + +import ( + "encoding/binary" + "errors" + "math/bits" +) + +const ( + prime1 uint64 = 11400714785074694791 + prime2 uint64 = 14029467366897019727 + prime3 uint64 = 1609587929392839161 + prime4 uint64 = 9650029242287828579 + prime5 uint64 = 2870177450012600261 +) + +// NOTE(caleb): I'm using both consts and vars of the primes. Using consts where +// possible in the Go code is worth a small (but measurable) performance boost +// by avoiding some MOVQs. Vars are needed for the asm and also are useful for +// convenience in the Go code in a few places where we need to intentionally +// avoid constant arithmetic (e.g., v1 := prime1 + prime2 fails because the +// result overflows a uint64). +var ( + prime1v = prime1 + prime2v = prime2 + prime3v = prime3 + prime4v = prime4 + prime5v = prime5 +) + +// Digest implements hash.Hash64. +type Digest struct { + v1 uint64 + v2 uint64 + v3 uint64 + v4 uint64 + total uint64 + mem [32]byte + n int // how much of mem is used +} + +// New creates a new Digest that computes the 64-bit xxHash algorithm. +func New() *Digest { + var d Digest + d.Reset() + return &d +} + +// Reset clears the Digest's state so that it can be reused. +func (d *Digest) Reset() { + d.v1 = prime1v + prime2 + d.v2 = prime2 + d.v3 = 0 + d.v4 = -prime1v + d.total = 0 + d.n = 0 +} + +// Size always returns 8 bytes. +func (d *Digest) Size() int { return 8 } + +// BlockSize always returns 32 bytes. +func (d *Digest) BlockSize() int { return 32 } + +// Write adds more data to d. It always returns len(b), nil. +func (d *Digest) Write(b []byte) (n int, err error) { + n = len(b) + d.total += uint64(n) + + if d.n+n < 32 { + // This new data doesn't even fill the current block. + copy(d.mem[d.n:], b) + d.n += n + return + } + + if d.n > 0 { + // Finish off the partial block. + copy(d.mem[d.n:], b) + d.v1 = round(d.v1, u64(d.mem[0:8])) + d.v2 = round(d.v2, u64(d.mem[8:16])) + d.v3 = round(d.v3, u64(d.mem[16:24])) + d.v4 = round(d.v4, u64(d.mem[24:32])) + b = b[32-d.n:] + d.n = 0 + } + + if len(b) >= 32 { + // One or more full blocks left. + nw := writeBlocks(d, b) + b = b[nw:] + } + + // Store any remaining partial block. + copy(d.mem[:], b) + d.n = len(b) + + return +} + +// Sum appends the current hash to b and returns the resulting slice. +func (d *Digest) Sum(b []byte) []byte { + s := d.Sum64() + return append( + b, + byte(s>>56), + byte(s>>48), + byte(s>>40), + byte(s>>32), + byte(s>>24), + byte(s>>16), + byte(s>>8), + byte(s), + ) +} + +// Sum64 returns the current hash. +func (d *Digest) Sum64() uint64 { + var h uint64 + + if d.total >= 32 { + v1, v2, v3, v4 := d.v1, d.v2, d.v3, d.v4 + h = rol1(v1) + rol7(v2) + rol12(v3) + rol18(v4) + h = mergeRound(h, v1) + h = mergeRound(h, v2) + h = mergeRound(h, v3) + h = mergeRound(h, v4) + } else { + h = d.v3 + prime5 + } + + h += d.total + + i, end := 0, d.n + for ; i+8 <= end; i += 8 { + k1 := round(0, u64(d.mem[i:i+8])) + h ^= k1 + h = rol27(h)*prime1 + prime4 + } + if i+4 <= end { + h ^= uint64(u32(d.mem[i:i+4])) * prime1 + h = rol23(h)*prime2 + prime3 + i += 4 + } + for i < end { + h ^= uint64(d.mem[i]) * prime5 + h = rol11(h) * prime1 + i++ + } + + h ^= h >> 33 + h *= prime2 + h ^= h >> 29 + h *= prime3 + h ^= h >> 32 + + return h +} + +const ( + magic = "xxh\x06" + marshaledSize = len(magic) + 8*5 + 32 +) + +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (d *Digest) MarshalBinary() ([]byte, error) { + b := make([]byte, 0, marshaledSize) + b = append(b, magic...) + b = appendUint64(b, d.v1) + b = appendUint64(b, d.v2) + b = appendUint64(b, d.v3) + b = appendUint64(b, d.v4) + b = appendUint64(b, d.total) + b = append(b, d.mem[:d.n]...) + b = b[:len(b)+len(d.mem)-d.n] + return b, nil +} + +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +func (d *Digest) UnmarshalBinary(b []byte) error { + if len(b) < len(magic) || string(b[:len(magic)]) != magic { + return errors.New("xxhash: invalid hash state identifier") + } + if len(b) != marshaledSize { + return errors.New("xxhash: invalid hash state size") + } + b = b[len(magic):] + b, d.v1 = consumeUint64(b) + b, d.v2 = consumeUint64(b) + b, d.v3 = consumeUint64(b) + b, d.v4 = consumeUint64(b) + b, d.total = consumeUint64(b) + copy(d.mem[:], b) + b = b[len(d.mem):] + d.n = int(d.total % uint64(len(d.mem))) + return nil +} + +func appendUint64(b []byte, x uint64) []byte { + var a [8]byte + binary.LittleEndian.PutUint64(a[:], x) + return append(b, a[:]...) +} + +func consumeUint64(b []byte) ([]byte, uint64) { + x := u64(b) + return b[8:], x +} + +func u64(b []byte) uint64 { return binary.LittleEndian.Uint64(b) } +func u32(b []byte) uint32 { return binary.LittleEndian.Uint32(b) } + +func round(acc, input uint64) uint64 { + acc += input * prime2 + acc = rol31(acc) + acc *= prime1 + return acc +} + +func mergeRound(acc, val uint64) uint64 { + val = round(0, val) + acc ^= val + acc = acc*prime1 + prime4 + return acc +} + +func rol1(x uint64) uint64 { return bits.RotateLeft64(x, 1) } +func rol7(x uint64) uint64 { return bits.RotateLeft64(x, 7) } +func rol11(x uint64) uint64 { return bits.RotateLeft64(x, 11) } +func rol12(x uint64) uint64 { return bits.RotateLeft64(x, 12) } +func rol18(x uint64) uint64 { return bits.RotateLeft64(x, 18) } +func rol23(x uint64) uint64 { return bits.RotateLeft64(x, 23) } +func rol27(x uint64) uint64 { return bits.RotateLeft64(x, 27) } +func rol31(x uint64) uint64 { return bits.RotateLeft64(x, 31) } diff --git a/vendor/github.com/cespare/xxhash/v2/xxhash_amd64.go b/vendor/github.com/cespare/xxhash/v2/xxhash_amd64.go new file mode 100644 index 00000000..35318d7c --- /dev/null +++ b/vendor/github.com/cespare/xxhash/v2/xxhash_amd64.go @@ -0,0 +1,13 @@ +// +build !appengine +// +build gc +// +build !purego + +package xxhash + +// Sum64 computes the 64-bit xxHash digest of b. +// +//go:noescape +func Sum64(b []byte) uint64 + +//go:noescape +func writeBlocks(*Digest, []byte) int diff --git a/vendor/github.com/cespare/xxhash/v2/xxhash_amd64.s b/vendor/github.com/cespare/xxhash/v2/xxhash_amd64.s new file mode 100644 index 00000000..d580e32a --- /dev/null +++ b/vendor/github.com/cespare/xxhash/v2/xxhash_amd64.s @@ -0,0 +1,215 @@ +// +build !appengine +// +build gc +// +build !purego + +#include "textflag.h" + +// Register allocation: +// AX h +// CX pointer to advance through b +// DX n +// BX loop end +// R8 v1, k1 +// R9 v2 +// R10 v3 +// R11 v4 +// R12 tmp +// R13 prime1v +// R14 prime2v +// R15 prime4v + +// round reads from and advances the buffer pointer in CX. +// It assumes that R13 has prime1v and R14 has prime2v. +#define round(r) \ + MOVQ (CX), R12 \ + ADDQ $8, CX \ + IMULQ R14, R12 \ + ADDQ R12, r \ + ROLQ $31, r \ + IMULQ R13, r + +// mergeRound applies a merge round on the two registers acc and val. +// It assumes that R13 has prime1v, R14 has prime2v, and R15 has prime4v. +#define mergeRound(acc, val) \ + IMULQ R14, val \ + ROLQ $31, val \ + IMULQ R13, val \ + XORQ val, acc \ + IMULQ R13, acc \ + ADDQ R15, acc + +// func Sum64(b []byte) uint64 +TEXT ·Sum64(SB), NOSPLIT, $0-32 + // Load fixed primes. + MOVQ ·prime1v(SB), R13 + MOVQ ·prime2v(SB), R14 + MOVQ ·prime4v(SB), R15 + + // Load slice. + MOVQ b_base+0(FP), CX + MOVQ b_len+8(FP), DX + LEAQ (CX)(DX*1), BX + + // The first loop limit will be len(b)-32. + SUBQ $32, BX + + // Check whether we have at least one block. + CMPQ DX, $32 + JLT noBlocks + + // Set up initial state (v1, v2, v3, v4). + MOVQ R13, R8 + ADDQ R14, R8 + MOVQ R14, R9 + XORQ R10, R10 + XORQ R11, R11 + SUBQ R13, R11 + + // Loop until CX > BX. +blockLoop: + round(R8) + round(R9) + round(R10) + round(R11) + + CMPQ CX, BX + JLE blockLoop + + MOVQ R8, AX + ROLQ $1, AX + MOVQ R9, R12 + ROLQ $7, R12 + ADDQ R12, AX + MOVQ R10, R12 + ROLQ $12, R12 + ADDQ R12, AX + MOVQ R11, R12 + ROLQ $18, R12 + ADDQ R12, AX + + mergeRound(AX, R8) + mergeRound(AX, R9) + mergeRound(AX, R10) + mergeRound(AX, R11) + + JMP afterBlocks + +noBlocks: + MOVQ ·prime5v(SB), AX + +afterBlocks: + ADDQ DX, AX + + // Right now BX has len(b)-32, and we want to loop until CX > len(b)-8. + ADDQ $24, BX + + CMPQ CX, BX + JG fourByte + +wordLoop: + // Calculate k1. + MOVQ (CX), R8 + ADDQ $8, CX + IMULQ R14, R8 + ROLQ $31, R8 + IMULQ R13, R8 + + XORQ R8, AX + ROLQ $27, AX + IMULQ R13, AX + ADDQ R15, AX + + CMPQ CX, BX + JLE wordLoop + +fourByte: + ADDQ $4, BX + CMPQ CX, BX + JG singles + + MOVL (CX), R8 + ADDQ $4, CX + IMULQ R13, R8 + XORQ R8, AX + + ROLQ $23, AX + IMULQ R14, AX + ADDQ ·prime3v(SB), AX + +singles: + ADDQ $4, BX + CMPQ CX, BX + JGE finalize + +singlesLoop: + MOVBQZX (CX), R12 + ADDQ $1, CX + IMULQ ·prime5v(SB), R12 + XORQ R12, AX + + ROLQ $11, AX + IMULQ R13, AX + + CMPQ CX, BX + JL singlesLoop + +finalize: + MOVQ AX, R12 + SHRQ $33, R12 + XORQ R12, AX + IMULQ R14, AX + MOVQ AX, R12 + SHRQ $29, R12 + XORQ R12, AX + IMULQ ·prime3v(SB), AX + MOVQ AX, R12 + SHRQ $32, R12 + XORQ R12, AX + + MOVQ AX, ret+24(FP) + RET + +// writeBlocks uses the same registers as above except that it uses AX to store +// the d pointer. + +// func writeBlocks(d *Digest, b []byte) int +TEXT ·writeBlocks(SB), NOSPLIT, $0-40 + // Load fixed primes needed for round. + MOVQ ·prime1v(SB), R13 + MOVQ ·prime2v(SB), R14 + + // Load slice. + MOVQ b_base+8(FP), CX + MOVQ b_len+16(FP), DX + LEAQ (CX)(DX*1), BX + SUBQ $32, BX + + // Load vN from d. + MOVQ d+0(FP), AX + MOVQ 0(AX), R8 // v1 + MOVQ 8(AX), R9 // v2 + MOVQ 16(AX), R10 // v3 + MOVQ 24(AX), R11 // v4 + + // We don't need to check the loop condition here; this function is + // always called with at least one block of data to process. +blockLoop: + round(R8) + round(R9) + round(R10) + round(R11) + + CMPQ CX, BX + JLE blockLoop + + // Copy vN back to d. + MOVQ R8, 0(AX) + MOVQ R9, 8(AX) + MOVQ R10, 16(AX) + MOVQ R11, 24(AX) + + // The number of bytes written is CX minus the old base pointer. + SUBQ b_base+8(FP), CX + MOVQ CX, ret+32(FP) + + RET diff --git a/vendor/github.com/cespare/xxhash/v2/xxhash_other.go b/vendor/github.com/cespare/xxhash/v2/xxhash_other.go new file mode 100644 index 00000000..4a5a8216 --- /dev/null +++ b/vendor/github.com/cespare/xxhash/v2/xxhash_other.go @@ -0,0 +1,76 @@ +// +build !amd64 appengine !gc purego + +package xxhash + +// Sum64 computes the 64-bit xxHash digest of b. +func Sum64(b []byte) uint64 { + // A simpler version would be + // d := New() + // d.Write(b) + // return d.Sum64() + // but this is faster, particularly for small inputs. + + n := len(b) + var h uint64 + + if n >= 32 { + v1 := prime1v + prime2 + v2 := prime2 + v3 := uint64(0) + v4 := -prime1v + for len(b) >= 32 { + v1 = round(v1, u64(b[0:8:len(b)])) + v2 = round(v2, u64(b[8:16:len(b)])) + v3 = round(v3, u64(b[16:24:len(b)])) + v4 = round(v4, u64(b[24:32:len(b)])) + b = b[32:len(b):len(b)] + } + h = rol1(v1) + rol7(v2) + rol12(v3) + rol18(v4) + h = mergeRound(h, v1) + h = mergeRound(h, v2) + h = mergeRound(h, v3) + h = mergeRound(h, v4) + } else { + h = prime5 + } + + h += uint64(n) + + i, end := 0, len(b) + for ; i+8 <= end; i += 8 { + k1 := round(0, u64(b[i:i+8:len(b)])) + h ^= k1 + h = rol27(h)*prime1 + prime4 + } + if i+4 <= end { + h ^= uint64(u32(b[i:i+4:len(b)])) * prime1 + h = rol23(h)*prime2 + prime3 + i += 4 + } + for ; i < end; i++ { + h ^= uint64(b[i]) * prime5 + h = rol11(h) * prime1 + } + + h ^= h >> 33 + h *= prime2 + h ^= h >> 29 + h *= prime3 + h ^= h >> 32 + + return h +} + +func writeBlocks(d *Digest, b []byte) int { + v1, v2, v3, v4 := d.v1, d.v2, d.v3, d.v4 + n := len(b) + for len(b) >= 32 { + v1 = round(v1, u64(b[0:8:len(b)])) + v2 = round(v2, u64(b[8:16:len(b)])) + v3 = round(v3, u64(b[16:24:len(b)])) + v4 = round(v4, u64(b[24:32:len(b)])) + b = b[32:len(b):len(b)] + } + d.v1, d.v2, d.v3, d.v4 = v1, v2, v3, v4 + return n - len(b) +} diff --git a/vendor/github.com/cespare/xxhash/v2/xxhash_safe.go b/vendor/github.com/cespare/xxhash/v2/xxhash_safe.go new file mode 100644 index 00000000..fc9bea7a --- /dev/null +++ b/vendor/github.com/cespare/xxhash/v2/xxhash_safe.go @@ -0,0 +1,15 @@ +// +build appengine + +// This file contains the safe implementations of otherwise unsafe-using code. + +package xxhash + +// Sum64String computes the 64-bit xxHash digest of s. +func Sum64String(s string) uint64 { + return Sum64([]byte(s)) +} + +// WriteString adds more data to d. It always returns len(s), nil. +func (d *Digest) WriteString(s string) (n int, err error) { + return d.Write([]byte(s)) +} diff --git a/vendor/github.com/cespare/xxhash/v2/xxhash_unsafe.go b/vendor/github.com/cespare/xxhash/v2/xxhash_unsafe.go new file mode 100644 index 00000000..53bf76ef --- /dev/null +++ b/vendor/github.com/cespare/xxhash/v2/xxhash_unsafe.go @@ -0,0 +1,46 @@ +// +build !appengine + +// This file encapsulates usage of unsafe. +// xxhash_safe.go contains the safe implementations. + +package xxhash + +import ( + "reflect" + "unsafe" +) + +// Notes: +// +// See https://groups.google.com/d/msg/golang-nuts/dcjzJy-bSpw/tcZYBzQqAQAJ +// for some discussion about these unsafe conversions. +// +// In the future it's possible that compiler optimizations will make these +// unsafe operations unnecessary: https://golang.org/issue/2205. +// +// Both of these wrapper functions still incur function call overhead since they +// will not be inlined. We could write Go/asm copies of Sum64 and Digest.Write +// for strings to squeeze out a bit more speed. Mid-stack inlining should +// eventually fix this. + +// Sum64String computes the 64-bit xxHash digest of s. +// It may be faster than Sum64([]byte(s)) by avoiding a copy. +func Sum64String(s string) uint64 { + var b []byte + bh := (*reflect.SliceHeader)(unsafe.Pointer(&b)) + bh.Data = (*reflect.StringHeader)(unsafe.Pointer(&s)).Data + bh.Len = len(s) + bh.Cap = len(s) + return Sum64(b) +} + +// WriteString adds more data to d. It always returns len(s), nil. +// It may be faster than Write([]byte(s)) by avoiding a copy. +func (d *Digest) WriteString(s string) (n int, err error) { + var b []byte + bh := (*reflect.SliceHeader)(unsafe.Pointer(&b)) + bh.Data = (*reflect.StringHeader)(unsafe.Pointer(&s)).Data + bh.Len = len(s) + bh.Cap = len(s) + return d.Write(b) +} diff --git a/vendor/github.com/cespare/xxhash/xxhash.go b/vendor/github.com/cespare/xxhash/xxhash.go new file mode 100644 index 00000000..f896bd28 --- /dev/null +++ b/vendor/github.com/cespare/xxhash/xxhash.go @@ -0,0 +1,168 @@ +// Package xxhash implements the 64-bit variant of xxHash (XXH64) as described +// at http://cyan4973.github.io/xxHash/. +package xxhash + +import ( + "encoding/binary" + "hash" +) + +const ( + prime1 uint64 = 11400714785074694791 + prime2 uint64 = 14029467366897019727 + prime3 uint64 = 1609587929392839161 + prime4 uint64 = 9650029242287828579 + prime5 uint64 = 2870177450012600261 +) + +// NOTE(caleb): I'm using both consts and vars of the primes. Using consts where +// possible in the Go code is worth a small (but measurable) performance boost +// by avoiding some MOVQs. Vars are needed for the asm and also are useful for +// convenience in the Go code in a few places where we need to intentionally +// avoid constant arithmetic (e.g., v1 := prime1 + prime2 fails because the +// result overflows a uint64). +var ( + prime1v = prime1 + prime2v = prime2 + prime3v = prime3 + prime4v = prime4 + prime5v = prime5 +) + +type xxh struct { + v1 uint64 + v2 uint64 + v3 uint64 + v4 uint64 + total int + mem [32]byte + n int // how much of mem is used +} + +// New creates a new hash.Hash64 that implements the 64-bit xxHash algorithm. +func New() hash.Hash64 { + var x xxh + x.Reset() + return &x +} + +func (x *xxh) Reset() { + x.n = 0 + x.total = 0 + x.v1 = prime1v + prime2 + x.v2 = prime2 + x.v3 = 0 + x.v4 = -prime1v +} + +func (x *xxh) Size() int { return 8 } +func (x *xxh) BlockSize() int { return 32 } + +// Write adds more data to x. It always returns len(b), nil. +func (x *xxh) Write(b []byte) (n int, err error) { + n = len(b) + x.total += len(b) + + if x.n+len(b) < 32 { + // This new data doesn't even fill the current block. + copy(x.mem[x.n:], b) + x.n += len(b) + return + } + + if x.n > 0 { + // Finish off the partial block. + copy(x.mem[x.n:], b) + x.v1 = round(x.v1, u64(x.mem[0:8])) + x.v2 = round(x.v2, u64(x.mem[8:16])) + x.v3 = round(x.v3, u64(x.mem[16:24])) + x.v4 = round(x.v4, u64(x.mem[24:32])) + b = b[32-x.n:] + x.n = 0 + } + + if len(b) >= 32 { + // One or more full blocks left. + b = writeBlocks(x, b) + } + + // Store any remaining partial block. + copy(x.mem[:], b) + x.n = len(b) + + return +} + +func (x *xxh) Sum(b []byte) []byte { + s := x.Sum64() + return append( + b, + byte(s>>56), + byte(s>>48), + byte(s>>40), + byte(s>>32), + byte(s>>24), + byte(s>>16), + byte(s>>8), + byte(s), + ) +} + +func (x *xxh) Sum64() uint64 { + var h uint64 + + if x.total >= 32 { + v1, v2, v3, v4 := x.v1, x.v2, x.v3, x.v4 + h = rol1(v1) + rol7(v2) + rol12(v3) + rol18(v4) + h = mergeRound(h, v1) + h = mergeRound(h, v2) + h = mergeRound(h, v3) + h = mergeRound(h, v4) + } else { + h = x.v3 + prime5 + } + + h += uint64(x.total) + + i, end := 0, x.n + for ; i+8 <= end; i += 8 { + k1 := round(0, u64(x.mem[i:i+8])) + h ^= k1 + h = rol27(h)*prime1 + prime4 + } + if i+4 <= end { + h ^= uint64(u32(x.mem[i:i+4])) * prime1 + h = rol23(h)*prime2 + prime3 + i += 4 + } + for i < end { + h ^= uint64(x.mem[i]) * prime5 + h = rol11(h) * prime1 + i++ + } + + h ^= h >> 33 + h *= prime2 + h ^= h >> 29 + h *= prime3 + h ^= h >> 32 + + return h +} + +func u64(b []byte) uint64 { return binary.LittleEndian.Uint64(b) } +func u32(b []byte) uint32 { return binary.LittleEndian.Uint32(b) } + +func round(acc, input uint64) uint64 { + acc += input * prime2 + acc = rol31(acc) + acc *= prime1 + return acc +} + +func mergeRound(acc, val uint64) uint64 { + val = round(0, val) + acc ^= val + acc = acc*prime1 + prime4 + return acc +} diff --git a/vendor/github.com/cespare/xxhash/xxhash_amd64.go b/vendor/github.com/cespare/xxhash/xxhash_amd64.go new file mode 100644 index 00000000..d6176526 --- /dev/null +++ b/vendor/github.com/cespare/xxhash/xxhash_amd64.go @@ -0,0 +1,12 @@ +// +build !appengine +// +build gc +// +build !purego + +package xxhash + +// Sum64 computes the 64-bit xxHash digest of b. +// +//go:noescape +func Sum64(b []byte) uint64 + +func writeBlocks(x *xxh, b []byte) []byte diff --git a/vendor/github.com/cespare/xxhash/xxhash_amd64.s b/vendor/github.com/cespare/xxhash/xxhash_amd64.s new file mode 100644 index 00000000..757f2011 --- /dev/null +++ b/vendor/github.com/cespare/xxhash/xxhash_amd64.s @@ -0,0 +1,233 @@ +// +build !appengine +// +build gc +// +build !purego + +#include "textflag.h" + +// Register allocation: +// AX h +// CX pointer to advance through b +// DX n +// BX loop end +// R8 v1, k1 +// R9 v2 +// R10 v3 +// R11 v4 +// R12 tmp +// R13 prime1v +// R14 prime2v +// R15 prime4v + +// round reads from and advances the buffer pointer in CX. +// It assumes that R13 has prime1v and R14 has prime2v. +#define round(r) \ + MOVQ (CX), R12 \ + ADDQ $8, CX \ + IMULQ R14, R12 \ + ADDQ R12, r \ + ROLQ $31, r \ + IMULQ R13, r + +// mergeRound applies a merge round on the two registers acc and val. +// It assumes that R13 has prime1v, R14 has prime2v, and R15 has prime4v. +#define mergeRound(acc, val) \ + IMULQ R14, val \ + ROLQ $31, val \ + IMULQ R13, val \ + XORQ val, acc \ + IMULQ R13, acc \ + ADDQ R15, acc + +// func Sum64(b []byte) uint64 +TEXT ·Sum64(SB), NOSPLIT, $0-32 + // Load fixed primes. + MOVQ ·prime1v(SB), R13 + MOVQ ·prime2v(SB), R14 + MOVQ ·prime4v(SB), R15 + + // Load slice. + MOVQ b_base+0(FP), CX + MOVQ b_len+8(FP), DX + LEAQ (CX)(DX*1), BX + + // The first loop limit will be len(b)-32. + SUBQ $32, BX + + // Check whether we have at least one block. + CMPQ DX, $32 + JLT noBlocks + + // Set up initial state (v1, v2, v3, v4). + MOVQ R13, R8 + ADDQ R14, R8 + MOVQ R14, R9 + XORQ R10, R10 + XORQ R11, R11 + SUBQ R13, R11 + + // Loop until CX > BX. +blockLoop: + round(R8) + round(R9) + round(R10) + round(R11) + + CMPQ CX, BX + JLE blockLoop + + MOVQ R8, AX + ROLQ $1, AX + MOVQ R9, R12 + ROLQ $7, R12 + ADDQ R12, AX + MOVQ R10, R12 + ROLQ $12, R12 + ADDQ R12, AX + MOVQ R11, R12 + ROLQ $18, R12 + ADDQ R12, AX + + mergeRound(AX, R8) + mergeRound(AX, R9) + mergeRound(AX, R10) + mergeRound(AX, R11) + + JMP afterBlocks + +noBlocks: + MOVQ ·prime5v(SB), AX + +afterBlocks: + ADDQ DX, AX + + // Right now BX has len(b)-32, and we want to loop until CX > len(b)-8. + ADDQ $24, BX + + CMPQ CX, BX + JG fourByte + +wordLoop: + // Calculate k1. + MOVQ (CX), R8 + ADDQ $8, CX + IMULQ R14, R8 + ROLQ $31, R8 + IMULQ R13, R8 + + XORQ R8, AX + ROLQ $27, AX + IMULQ R13, AX + ADDQ R15, AX + + CMPQ CX, BX + JLE wordLoop + +fourByte: + ADDQ $4, BX + CMPQ CX, BX + JG singles + + MOVL (CX), R8 + ADDQ $4, CX + IMULQ R13, R8 + XORQ R8, AX + + ROLQ $23, AX + IMULQ R14, AX + ADDQ ·prime3v(SB), AX + +singles: + ADDQ $4, BX + CMPQ CX, BX + JGE finalize + +singlesLoop: + MOVBQZX (CX), R12 + ADDQ $1, CX + IMULQ ·prime5v(SB), R12 + XORQ R12, AX + + ROLQ $11, AX + IMULQ R13, AX + + CMPQ CX, BX + JL singlesLoop + +finalize: + MOVQ AX, R12 + SHRQ $33, R12 + XORQ R12, AX + IMULQ R14, AX + MOVQ AX, R12 + SHRQ $29, R12 + XORQ R12, AX + IMULQ ·prime3v(SB), AX + MOVQ AX, R12 + SHRQ $32, R12 + XORQ R12, AX + + MOVQ AX, ret+24(FP) + RET + +// writeBlocks uses the same registers as above except that it uses AX to store +// the x pointer. + +// func writeBlocks(x *xxh, b []byte) []byte +TEXT ·writeBlocks(SB), NOSPLIT, $0-56 + // Load fixed primes needed for round. + MOVQ ·prime1v(SB), R13 + MOVQ ·prime2v(SB), R14 + + // Load slice. + MOVQ b_base+8(FP), CX + MOVQ CX, ret_base+32(FP) // initialize return base pointer; see NOTE below + MOVQ b_len+16(FP), DX + LEAQ (CX)(DX*1), BX + SUBQ $32, BX + + // Load vN from x. + MOVQ x+0(FP), AX + MOVQ 0(AX), R8 // v1 + MOVQ 8(AX), R9 // v2 + MOVQ 16(AX), R10 // v3 + MOVQ 24(AX), R11 // v4 + + // We don't need to check the loop condition here; this function is + // always called with at least one block of data to process. +blockLoop: + round(R8) + round(R9) + round(R10) + round(R11) + + CMPQ CX, BX + JLE blockLoop + + // Copy vN back to x. + MOVQ R8, 0(AX) + MOVQ R9, 8(AX) + MOVQ R10, 16(AX) + MOVQ R11, 24(AX) + + // Construct return slice. + // NOTE: It's important that we don't construct a slice that has a base + // pointer off the end of the original slice, as in Go 1.7+ this will + // cause runtime crashes. (See discussion in, for example, + // https://github.com/golang/go/issues/16772.) + // Therefore, we calculate the length/cap first, and if they're zero, we + // keep the old base. This is what the compiler does as well if you + // write code like + // b = b[len(b):] + + // New length is 32 - (CX - BX) -> BX+32 - CX. + ADDQ $32, BX + SUBQ CX, BX + JZ afterSetBase + + MOVQ CX, ret_base+32(FP) + +afterSetBase: + MOVQ BX, ret_len+40(FP) + MOVQ BX, ret_cap+48(FP) // set cap == len + + RET diff --git a/vendor/github.com/cespare/xxhash/xxhash_other.go b/vendor/github.com/cespare/xxhash/xxhash_other.go new file mode 100644 index 00000000..c68d13f8 --- /dev/null +++ b/vendor/github.com/cespare/xxhash/xxhash_other.go @@ -0,0 +1,75 @@ +// +build !amd64 appengine !gc purego + +package xxhash + +// Sum64 computes the 64-bit xxHash digest of b. +func Sum64(b []byte) uint64 { + // A simpler version would be + // x := New() + // x.Write(b) + // return x.Sum64() + // but this is faster, particularly for small inputs. + + n := len(b) + var h uint64 + + if n >= 32 { + v1 := prime1v + prime2 + v2 := prime2 + v3 := uint64(0) + v4 := -prime1v + for len(b) >= 32 { + v1 = round(v1, u64(b[0:8:len(b)])) + v2 = round(v2, u64(b[8:16:len(b)])) + v3 = round(v3, u64(b[16:24:len(b)])) + v4 = round(v4, u64(b[24:32:len(b)])) + b = b[32:len(b):len(b)] + } + h = rol1(v1) + rol7(v2) + rol12(v3) + rol18(v4) + h = mergeRound(h, v1) + h = mergeRound(h, v2) + h = mergeRound(h, v3) + h = mergeRound(h, v4) + } else { + h = prime5 + } + + h += uint64(n) + + i, end := 0, len(b) + for ; i+8 <= end; i += 8 { + k1 := round(0, u64(b[i:i+8:len(b)])) + h ^= k1 + h = rol27(h)*prime1 + prime4 + } + if i+4 <= end { + h ^= uint64(u32(b[i:i+4:len(b)])) * prime1 + h = rol23(h)*prime2 + prime3 + i += 4 + } + for ; i < end; i++ { + h ^= uint64(b[i]) * prime5 + h = rol11(h) * prime1 + } + + h ^= h >> 33 + h *= prime2 + h ^= h >> 29 + h *= prime3 + h ^= h >> 32 + + return h +} + +func writeBlocks(x *xxh, b []byte) []byte { + v1, v2, v3, v4 := x.v1, x.v2, x.v3, x.v4 + for len(b) >= 32 { + v1 = round(v1, u64(b[0:8:len(b)])) + v2 = round(v2, u64(b[8:16:len(b)])) + v3 = round(v3, u64(b[16:24:len(b)])) + v4 = round(v4, u64(b[24:32:len(b)])) + b = b[32:len(b):len(b)] + } + x.v1, x.v2, x.v3, x.v4 = v1, v2, v3, v4 + return b +} diff --git a/vendor/github.com/cespare/xxhash/xxhash_safe.go b/vendor/github.com/cespare/xxhash/xxhash_safe.go new file mode 100644 index 00000000..dfa15ab7 --- /dev/null +++ b/vendor/github.com/cespare/xxhash/xxhash_safe.go @@ -0,0 +1,10 @@ +// +build appengine + +// This file contains the safe implementations of otherwise unsafe-using code. + +package xxhash + +// Sum64String computes the 64-bit xxHash digest of s. +func Sum64String(s string) uint64 { + return Sum64([]byte(s)) +} diff --git a/vendor/github.com/cespare/xxhash/xxhash_unsafe.go b/vendor/github.com/cespare/xxhash/xxhash_unsafe.go new file mode 100644 index 00000000..d2b64e8b --- /dev/null +++ b/vendor/github.com/cespare/xxhash/xxhash_unsafe.go @@ -0,0 +1,30 @@ +// +build !appengine + +// This file encapsulates usage of unsafe. +// xxhash_safe.go contains the safe implementations. + +package xxhash + +import ( + "reflect" + "unsafe" +) + +// Sum64String computes the 64-bit xxHash digest of s. +// It may be faster than Sum64([]byte(s)) by avoiding a copy. +// +// TODO(caleb): Consider removing this if an optimization is ever added to make +// it unnecessary: https://golang.org/issue/2205. +// +// TODO(caleb): We still have a function call; we could instead write Go/asm +// copies of Sum64 for strings to squeeze out a bit more speed. +func Sum64String(s string) uint64 { + // See https://groups.google.com/d/msg/golang-nuts/dcjzJy-bSpw/tcZYBzQqAQAJ + // for some discussion about this unsafe conversion. + var b []byte + bh := (*reflect.SliceHeader)(unsafe.Pointer(&b)) + bh.Data = (*reflect.StringHeader)(unsafe.Pointer(&s)).Data + bh.Len = len(s) + bh.Cap = len(s) + return Sum64(b) +} diff --git a/vendor/github.com/coreos/go-oidc/.gitignore b/vendor/github.com/coreos/go-oidc/.gitignore new file mode 100644 index 00000000..c96f2f47 --- /dev/null +++ b/vendor/github.com/coreos/go-oidc/.gitignore @@ -0,0 +1,2 @@ +/bin +/gopath diff --git a/vendor/github.com/coreos/go-oidc/.travis.yml b/vendor/github.com/coreos/go-oidc/.travis.yml new file mode 100644 index 00000000..f2f3c9c8 --- /dev/null +++ b/vendor/github.com/coreos/go-oidc/.travis.yml @@ -0,0 +1,16 @@ +language: go + +go: + - 1.7.5 + - 1.8 + +install: + - go get -v -t github.com/coreos/go-oidc/... + - go get golang.org/x/tools/cmd/cover + - go get github.com/golang/lint/golint + +script: + - ./test + +notifications: + email: false diff --git a/vendor/github.com/coreos/go-oidc/CONTRIBUTING.md b/vendor/github.com/coreos/go-oidc/CONTRIBUTING.md new file mode 100644 index 00000000..6662073a --- /dev/null +++ b/vendor/github.com/coreos/go-oidc/CONTRIBUTING.md @@ -0,0 +1,71 @@ +# How to Contribute + +CoreOS projects are [Apache 2.0 licensed](LICENSE) and accept contributions via +GitHub pull requests. This document outlines some of the conventions on +development workflow, commit message formatting, contact points and other +resources to make it easier to get your contribution accepted. + +# Certificate of Origin + +By contributing to this project you agree to the Developer Certificate of +Origin (DCO). This document was created by the Linux Kernel community and is a +simple statement that you, as a contributor, have the legal right to make the +contribution. See the [DCO](DCO) file for details. + +# Email and Chat + +The project currently uses the general CoreOS email list and IRC channel: +- Email: [coreos-dev](https://groups.google.com/forum/#!forum/coreos-dev) +- IRC: #[coreos](irc://irc.freenode.org:6667/#coreos) IRC channel on freenode.org + +Please avoid emailing maintainers found in the MAINTAINERS file directly. They +are very busy and read the mailing lists. + +## Getting Started + +- Fork the repository on GitHub +- Read the [README](README.md) for build and test instructions +- Play with the project, submit bugs, submit patches! + +## Contribution Flow + +This is a rough outline of what a contributor's workflow looks like: + +- Create a topic branch from where you want to base your work (usually master). +- Make commits of logical units. +- Make sure your commit messages are in the proper format (see below). +- Push your changes to a topic branch in your fork of the repository. +- Make sure the tests pass, and add any new tests as appropriate. +- Submit a pull request to the original repository. + +Thanks for your contributions! + +### Format of the Commit Message + +We follow a rough convention for commit messages that is designed to answer two +questions: what changed and why. The subject line should feature the what and +the body of the commit should describe the why. + +``` +scripts: add the test-cluster command + +this uses tmux to setup a test cluster that you can easily kill and +start for debugging. + +Fixes #38 +``` + +The format can be described more formally as follows: + +``` +: + + + +