diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index ea71d92a08..4b7511266f 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,2 +1,3 @@
* @BupycHuk
+/api-tests/ @BupycHuk
/data/iatemplates/ @BupycHuk @adivinho
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1f22bd55e2..60b0b76379 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -5,6 +5,8 @@ on:
# run every Sunday to re-populate caches after they are cleaned on Saturday
- cron: "0 12 * * 0"
push:
+ paths-ignore:
+ - 'api-tests/**'
branches:
- PMM-2.0
- release-*
diff --git a/.gitignore b/.gitignore
index 8af57e90b9..73b25f7785 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,12 @@
-/.idea/
-/.vscode/
-/bin/
+.idea/
+.vscode/
+bin/
fuzzdata/
cover.out
crosscover.out
packages.png
*.zip
+*.test
+pmm-api-tests-output.txt
+pmm-api-tests-junit-report.xml
+
diff --git a/api-tests/.golangci-required.yml b/api-tests/.golangci-required.yml
new file mode 100644
index 0000000000..35995b4bfa
--- /dev/null
+++ b/api-tests/.golangci-required.yml
@@ -0,0 +1,27 @@
+---
+# The most valuable linters; they are required to pass for PR to be merged.
+
+linters-settings:
+ depguard:
+ list-type: blacklist
+ include-go-root: true
+ packages:
+ # use "github.com/pkg/errors" instead
+ - errors
+ # use "github.com/golang/protobuf/proto" instead
+ - github.com/gogo/protobuf/proto
+
+ goimports:
+ local-prefixes: github.com/Percona-Lab/pmm-api-tests
+
+linters:
+ disable-all: true
+ enable:
+ - depguard
+ - goimports
+ - ineffassign
+ - govet
+ - staticcheck
+
+issues:
+ exclude-use-default: false
diff --git a/api-tests/.golangci.yml b/api-tests/.golangci.yml
new file mode 100644
index 0000000000..ef41e1b641
--- /dev/null
+++ b/api-tests/.golangci.yml
@@ -0,0 +1,39 @@
+---
+linters-settings:
+ depguard:
+ list-type: blacklist
+ include-go-root: true
+ packages:
+ # use "github.com/pkg/errors" instead
+ - errors
+ # use "github.com/golang/protobuf/proto" instead
+ - github.com/gogo/protobuf/proto
+
+ goimports:
+ local-prefixes: github.com/Percona-Lab/pmm-api-tests
+
+ lll:
+ line-length: 170
+ tab-width: 4
+
+ unused:
+ check-exported: true
+
+ unparam:
+ check-exported: true
+
+linters:
+ enable-all: true
+ disable:
+ - wsl # too annoying
+ - lll # too annoying
+ - unused # very annoying false positive: https://github.com/golangci/golangci-lint/issues/791
+ - goerr113 # we use different approach for errors
+ - testpackage # senseless
+ - exhaustivestruct # too annoying
+
+issues:
+ exclude-use-default: false
+ exclude:
+ # gas: Duplicated errcheck checks
+ - 'G104: Errors unhandled'
diff --git a/api-tests/Dockerfile b/api-tests/Dockerfile
new file mode 100644
index 0000000000..ecded92b81
--- /dev/null
+++ b/api-tests/Dockerfile
@@ -0,0 +1,8 @@
+FROM golang:1.16
+
+RUN mkdir -p $GOPATH/src/github.com/percona/pmm-managed/api-tests
+
+WORKDIR $GOPATH/src/github.com/percona/pmm-managed/api-tests/
+COPY . $GOPATH/src/github.com/percona/pmm-managed/api-tests/
+
+CMD make init run-race
diff --git a/api-tests/Makefile b/api-tests/Makefile
new file mode 100644
index 0000000000..628d45ccd5
--- /dev/null
+++ b/api-tests/Makefile
@@ -0,0 +1,46 @@
+BASE_PATH = $(shell pwd)
+BIN_PATH := $(BASE_PATH)/bin
+
+export PATH := $(BIN_PATH):$(PATH)
+
+all: build
+
+init: ## Installs development tools
+ go build -modfile=tools/go.mod -o $(BIN_PATH)/goimports golang.org/x/tools/cmd/goimports
+ go build -modfile=tools/go.mod -o $(BIN_PATH)/golangci-lint github.com/golangci/golangci-lint/cmd/golangci-lint
+ go build -modfile=tools/go.mod -o $(BIN_PATH)/go-junit-report github.com/jstemmer/go-junit-report
+ go build -modfile=tools/go.mod -o $(BIN_PATH)/reviewdog github.com/reviewdog/reviewdog/cmd/reviewdog
+
+build:
+ go install -v ./...
+ go test -c -v ./inventory
+ go test -c -v ./management
+ go test -c -v ./server
+
+dev-test: ## Run test on dev env. Use `PMM_KUBECONFIG=/path/to/kubeconfig.yaml make dev-test` to run tests for DBaaS.
+ go test -count=1 -p 1 -v ./... -pmm.server-insecure-tls
+
+run:
+ go test -count=1 -p 1 -v ./... 2>&1 | tee pmm-api-tests-output.txt
+ cat pmm-api-tests-output.txt | $(BIN_PATH)/go-junit-report > pmm-api-tests-junit-report.xml
+
+run-race:
+ go test -count=1 -p 1 -v -race ./... 2>&1 | tee pmm-api-tests-output.txt
+ cat pmm-api-tests-output.txt | $(BIN_PATH)/go-junit-report > pmm-api-tests-junit-report.xml
+
+FILES = $(shell find . -type f -name '*.go')
+
+format: ## Format source code.
+ gofmt -w -s $(FILES)
+ $(BIN_PATH)/goimports -local github.com/Percona-Lab/pmm-api-tests -l -w $(FILES)
+
+clean:
+ rm -f ./pmm-api-tests-output.txt
+ rm -f ./pmm-api-tests-junit-report.xml
+
+check-all: ## Run golang ci linter to check new changes from master.
+ $(BIN_PATH)/golangci-lint run -c=.golangci.yml --new-from-rev=master
+
+ci-reviewdog: ## Runs reviewdog checks.
+ $(BIN_PATH)/golangci-lint run -c=.golangci-required.yml --out-format=line-number | $(BIN_PATH)/reviewdog -f=golangci-lint -level=error -reporter=github-pr-check
+ $(BIN_PATH)/golangci-lint run -c=.golangci.yml --out-format=line-number | $(BIN_PATH)/reviewdog -f=golangci-lint -level=error -reporter=github-pr-review
diff --git a/api-tests/README.md b/api-tests/README.md
new file mode 100644
index 0000000000..f5ff12a393
--- /dev/null
+++ b/api-tests/README.md
@@ -0,0 +1,51 @@
+# pmm-api-tests
+
+[](https://travis-ci.com/Percona-Lab/pmm-api-tests)
+
+API tests for PMM 2.x
+
+# Setup Instructions
+
+Make sure you have Go 1.16.x installed on your systems, execute the following steps
+to setup API-tests in your local systems.
+
+1. Run PMM Server.
+2. Navigate to the tests root folder: `cd ~/go/src/github.com/percona/pmm-managed/api-tests`
+
+# Usage
+
+Run the tests using the following command:
+
+```
+go test ./... -pmm.server-url **pmm-server-url** -v
+```
+
+where `pmm-server-url` should be pointing to pmm-server.
+
+# Docker
+
+Build Docker image using the following command:
+
+```
+docker build -t IMAGENAME .
+```
+
+Run Docker container using the following command:
+
+```
+docker run -e PMM_SERVER_URL=**pmm-server-url** IMAGENAME
+```
+
+where `PMM_SERVER_URL` should be pointing to pmm-server.
+
+If pmm-server located locally:
+
+- Use --network=host while running docker container or add both containers to the same docker network.
+- Use the insecure url if you default to a self-generated certificate.
+
+# Contributing
+
+All tests should follow these rules:
+
+- Tests can work in parallel and in real system, so take into account that there might be records in database.
+- Always revert changes made by test.
diff --git a/api-tests/docker-compose.yml b/api-tests/docker-compose.yml
new file mode 100644
index 0000000000..9d3cbbfda8
--- /dev/null
+++ b/api-tests/docker-compose.yml
@@ -0,0 +1,86 @@
+---
+# FIXME This file is not used yet; see https://jira.percona.com/browse/PMM-5106
+
+version: '3.7'
+
+services:
+ pmm-server:
+ image: ${PMM_SERVER_IMAGE:-public.ecr.aws/e7j3v3n0/pmm-server:dev-latest}
+ container_name: pmm-agent_pmm-server
+ ports:
+ - 127.0.0.1:80:80
+ - 127.0.0.1:443:443
+ environment:
+ - PMM_DEBUG=1
+ - PERCONA_TEST_CHECKS_INTERVAL=10s
+ # for local development
+ # - PERCONA_TEST_CHECKS_FILE=/srv/checks/custom-checks.yml
+ # for check-dev
+ - PERCONA_TEST_SAAS_HOST=check-dev.percona.com:443
+ - PERCONA_TEST_CHECKS_PUBLIC_KEY=RWTg+ZmCCjt7O8eWeAmTLAqW+1ozUbpRSKSwNTmO+exlS5KEIPYWuYdX
+ volumes:
+ - ./testdata/checks:/srv/checks
+
+ test_db:
+ image: aleksi/test_db:1.1.0
+ container_name: pmm-agent_test_db
+ volumes:
+ - test_db_mysql:/test_db/mysql/world:ro
+ - test_db_postgres:/test_db/postgresql/world:ro
+
+ # It is essential to have an extra directory `/slowlogs/` between host and container;
+ # and to not have a trailing slash at `./testdata/mysql`.
+ # Otherwise, MySQL in Docker for Mac completely locks during/after slowlog rotation tests.
+ mysql:
+ image: ${MYSQL_IMAGE:-percona:5.7}
+ container_name: pmm-agent_mysql
+ command: >
+ --sql-mode="ANSI_QUOTES"
+ --performance-schema --innodb_monitor_enable=all
+ --slow_query_log --slow_query_log_file=/mysql/slowlogs/slow.log --long_query_time=0
+ ports:
+ - 127.0.0.1:3306:3306
+ environment:
+ - MYSQL_ROOT_PASSWORD=root-password
+ - MYSQL_USER=pmm-agent
+ - MYSQL_PASSWORD=pmm-agent-password
+ - UMASK=0777 # for slowlog file
+ volumes:
+ - test_db_mysql:/docker-entrypoint-initdb.d/:ro
+ - ./testdata/mysql:/mysql
+
+ mongo:
+ image: ${MONGO_IMAGE:-percona/percona-server-mongodb:4.2}
+ container_name: pmm-agent_mongo
+ command: --profile 2
+ ports:
+ - 127.0.0.1:27017:27017
+ environment:
+ - MONGO_INITDB_ROOT_USERNAME=root
+ - MONGO_INITDB_ROOT_PASSWORD=root-password
+
+ postgres:
+ image: ${POSTGRES_IMAGE:-postgres:11}
+ container_name: pmm-agent_postgres
+ command: >
+ -c shared_preload_libraries=pg_stat_statements
+ -c track_activity_query_size=2048
+ -c pg_stat_statements.max=10000
+ -c pg_stat_statements.track=all
+ -c pg_stat_statements.save=off
+ -c track_io_timing=on
+ ports:
+ - 127.0.0.1:5432:5432
+ environment:
+ - POSTGRES_USER=pmm-agent
+ - POSTGRES_PASSWORD=pmm-agent-password
+ volumes:
+ - test_db_postgres:/docker-entrypoint-initdb.d/
+
+ sysbench:
+ image: perconalab/sysbench
+ container_name: pmm-agent_sysbench
+
+volumes:
+ test_db_mysql:
+ test_db_postgres:
diff --git a/api-tests/go.mod b/api-tests/go.mod
new file mode 100644
index 0000000000..dd50350e63
--- /dev/null
+++ b/api-tests/go.mod
@@ -0,0 +1,25 @@
+module github.com/percona/pmm-managed/api-tests
+
+go 1.16
+
+// Use for local development, but do not commit:
+// replace github.com/percona/pmm => ../../pmm
+
+// Update with:
+// go get -v github.com/percona/pmm@PMM-2.0
+
+require (
+ github.com/AlekSi/pointer v1.1.0
+ github.com/brianvoe/gofakeit/v6 v6.2.2
+ github.com/davecgh/go-spew v1.1.1
+ github.com/go-openapi/runtime v0.19.20
+ github.com/go-openapi/spec v0.19.9 // indirect
+ github.com/percona-platform/saas v0.0.0-20210122115803-1b32ca1828e1
+ github.com/percona/pmm v0.0.0-20210707115905-36eb37dae44c
+ github.com/prometheus/client_golang v1.9.0
+ github.com/sirupsen/logrus v1.6.0
+ github.com/stretchr/testify v1.6.1
+ golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e
+ google.golang.org/grpc v1.35.0
+ gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
+)
diff --git a/api-tests/go.sum b/api-tests/go.sum
new file mode 100644
index 0000000000..02eb9dcaf3
--- /dev/null
+++ b/api-tests/go.sum
@@ -0,0 +1,672 @@
+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=
+github.com/AlekSi/pointer v1.1.0 h1:SSDMPcXD9jSl8FPy9cRzoRaMJtm9g9ggGTxecRUbQoI=
+github.com/AlekSi/pointer v1.1.0/go.mod h1:y7BvfRI3wXPWKXEBhU71nbnIEEZX0QTSB2Bj48UJIZE=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
+github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
+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-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/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/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
+github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
+github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+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-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+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/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
+github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
+github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY=
+github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
+github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
+github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
+github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
+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/brianvoe/gofakeit v3.18.0+incompatible h1:wDOmHc9DLG4nRjUVVaxA+CEglKOW72Y5+4WNxUIkjM8=
+github.com/brianvoe/gofakeit v3.18.0+incompatible/go.mod h1:kfwdRA90vvNhPutZWfH7WPaDzUjz+CZFqG+rPkOjGOc=
+github.com/brianvoe/gofakeit/v6 v6.2.2 h1:EcE/d5MiDA2xhg6Uc03Xh2OR6w2Sd8dpbuJXO99bcSc=
+github.com/brianvoe/gofakeit/v6 v6.2.2/go.mod h1:palrJUk4Fyw38zIFB/uBZqsgzW5VsNllhHKKwAebzew=
+github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
+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/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/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/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
+github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
+github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
+github.com/coreos/go-semver v0.2.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/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+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.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/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 v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
+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/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+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/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+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-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/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/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
+github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
+github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
+github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
+github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk=
+github.com/go-openapi/analysis v0.19.4/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk=
+github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU=
+github.com/go-openapi/analysis v0.19.10 h1:5BHISBAXOc/aJK25irLZnx2D3s6WyYaY9D4gmuz9fdE=
+github.com/go-openapi/analysis v0.19.10/go.mod h1:qmhS3VNFxBlquFJ0RGoDtylO9y4pgTAUNE9AEEMdlJQ=
+github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
+github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
+github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94=
+github.com/go-openapi/errors v0.19.3/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94=
+github.com/go-openapi/errors v0.19.6 h1:xZMThgv5SQ7SMbWtKFkCf9bBdvR2iEyw9k3zGZONuys=
+github.com/go-openapi/errors v0.19.6/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
+github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
+github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
+github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
+github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w=
+github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
+github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
+github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
+github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
+github.com/go-openapi/jsonreference v0.19.4 h1:3Vw+rh13uq2JFNxgnMTGE1rnoieU9FmyE1gvnyylsYg=
+github.com/go-openapi/jsonreference v0.19.4/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
+github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
+github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
+github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
+github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs=
+github.com/go-openapi/loads v0.19.3/go.mod h1:YVfqhUCdahYwR3f3iiwQLhicVRvLlU/WO5WPaZvcvSI=
+github.com/go-openapi/loads v0.19.5 h1:jZVYWawIQiA1NBnHla28ktg6hrcfTHsCE+3QLVRBIls=
+github.com/go-openapi/loads v0.19.5/go.mod h1:dswLCAdonkRufe/gSUC3gN8nTSaB9uaS2es0x5/IbjY=
+github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA=
+github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64=
+github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4=
+github.com/go-openapi/runtime v0.19.15/go.mod h1:dhGWCTKRXlAfGnQG0ONViOZpjfg0m2gUt9nTQPQZuoo=
+github.com/go-openapi/runtime v0.19.20 h1:J/t+QIjbcoq8WJvjGxRKiFBhqUE8slS9SbmD0Oi/raQ=
+github.com/go-openapi/runtime v0.19.20/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk=
+github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
+github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
+github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY=
+github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
+github.com/go-openapi/spec v0.19.6/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk=
+github.com/go-openapi/spec v0.19.8/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk=
+github.com/go-openapi/spec v0.19.9 h1:9z9cbFuZJ7AcvOHKIY+f6Aevb4vObNDkTEyoMfO7rAc=
+github.com/go-openapi/spec v0.19.9/go.mod h1:vqK/dIdLGCosfvYsQV3WfC7N3TiZSnGY2RZKoFK7X28=
+github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
+github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
+github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY=
+github.com/go-openapi/strfmt v0.19.2/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU=
+github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU=
+github.com/go-openapi/strfmt v0.19.4/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk=
+github.com/go-openapi/strfmt v0.19.5 h1:0utjKrw+BAh8s57XE9Xz8DUBsVvPmRUB6styvl9wWIM=
+github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk=
+github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
+github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
+github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/go-openapi/swag v0.19.7/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY=
+github.com/go-openapi/swag v0.19.9 h1:1IxuqvBUU3S2Bi4YC7tlP9SJF1gVpCvqN0T2Qof4azE=
+github.com/go-openapi/swag v0.19.9/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY=
+github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
+github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA=
+github.com/go-openapi/validate v0.19.3/go.mod h1:90Vh6jjkTn+OT1Eefm0ZixWNFjhtOH7vS9k0lo6zwJo=
+github.com/go-openapi/validate v0.19.10 h1:tG3SZ5DC5KF4cyt7nqLVcQXGj5A7mpaYkAcNPlDK+Yk=
+github.com/go-openapi/validate v0.19.10/go.mod h1:RKEZTUWDkxKQxN2jDT7ZnZi2bhZlbNMAuKvKB+IaGx8=
+github.com/go-sql-driver/mysql v1.4.0/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/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0=
+github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY=
+github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg=
+github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
+github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
+github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs=
+github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=
+github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=
+github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk=
+github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28=
+github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo=
+github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk=
+github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw=
+github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360=
+github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg=
+github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE=
+github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8=
+github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
+github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
+github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=
+github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=
+github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ=
+github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=
+github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
+github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
+github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
+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.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/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
+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/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+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/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+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/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+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.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/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.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
+github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/grpc-ecosystem/grpc-gateway v1.15.0 h1:ntPNC9TD/6l2XDenJZe6T5lSMg95thpV9sGAqHX4WU8=
+github.com/grpc-ecosystem/grpc-gateway v1.15.0/go.mod h1:vO11I9oWA+KsxmfFQPhLnnIb1VDE24M+pdxZFiuZcA8=
+github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
+github.com/hashicorp/consul/sdk v0.3.0/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-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-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/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/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+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.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
+github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+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 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.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
+github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+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/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
+github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
+github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
+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/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
+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/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
+github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+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.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
+github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
+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/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.7.1 h1:mdxE1MF9o53iCb2Ghj1VfWvh7ZOwHpnVG/xwXrV90U8=
+github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
+github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
+github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
+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/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
+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/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/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/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8=
+github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+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-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/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
+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/mwitkow/go-proto-validators v0.3.2 h1:qRlmpTzm2pstMKKzTdvwPCF5QfBNURSlAgN/R+qbKos=
+github.com/mwitkow/go-proto-validators v0.3.2/go.mod h1:ej0Qp0qMgHN/KtDyUt+Q1/tA7a5VarXUOUxD+oeD30w=
+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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+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/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
+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/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
+github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
+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/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/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
+github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
+github.com/percona-platform/saas v0.0.0-20210122115803-1b32ca1828e1 h1:VyKdL2wWYqwV79Sa4LHmLkQMjoSF+5uusQPBr4VoHWw=
+github.com/percona-platform/saas v0.0.0-20210122115803-1b32ca1828e1/go.mod h1:jJRyGMxxDJaSiU7AaHNS+8j1TFQQhX6lcYp8s0t8Knc=
+github.com/percona/pmm v0.0.0-20210707115905-36eb37dae44c h1:6UtqOTbcZ02ekP8kIkbY2VEFR8h/j8ddby2z47dlfzw=
+github.com/percona/pmm v0.0.0-20210707115905-36eb37dae44c/go.mod h1:Cm2JKvJMlMimtAhmF/1BUvz3qVJZ2O2zxQXRvtQh93Q=
+github.com/percona/promconfig v0.2.1 h1:LBbCDSQRfy0aTHFJMgrVQIE2WvmPkMTkIoznTfBAvj8=
+github.com/percona/promconfig v0.2.1/go.mod h1:Y2uXi5QNk71+ceJHuI9poank+0S1kjxd3K105fXKVkg=
+github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
+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 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/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
+github.com/prometheus/client_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.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
+github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
+github.com/prometheus/client_golang v1.9.0 h1:Rrch9mh17XcxvEu9D9DEpb4isxjGBtcevQjKvxPRQIU=
+github.com/prometheus/client_golang v1.9.0/go.mod h1:FqZLKOZnGdFAhOK4nqGHa7D66IdsO+O441Eve7ptJDU=
+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.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.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
+github.com/prometheus/common v0.15.0 h1:4fgOnadei3EZvgRwxJ7RMpG1k1pOZth5Pc13tyspaKM=
+github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/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.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
+github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
+github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4=
+github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
+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.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+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/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
+github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.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.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
+github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+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/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+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/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 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/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+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/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/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
+github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+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.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
+go.mongodb.org/mongo-driver v1.3.0/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE=
+go.mongodb.org/mongo-driver v1.3.4/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE=
+go.mongodb.org/mongo-driver v1.3.5 h1:S0ZOruh4YGHjD7JoN7mIsTrNjnQbOjrmgrx6l6pZN7I=
+go.mongodb.org/mongo-driver v1.3.5/go.mod h1:Ual6Gkco7ZGQw8wE1t4tLnvBsf6yVSM60qW6TgOeJ5c=
+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.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.starlark.net v0.0.0-20201210151846-e81fc95f7bd5/go.mod h1:vxxlMsgCAPH7BR2LtxjJC4WhhZhCGd/b01+CIpj8H4k=
+go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+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/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
+go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
+go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/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-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/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-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/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-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-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-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/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-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+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-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-20190412183630-56d357773e84/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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/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-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200722175500-76b94024e4b6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e h1:AyodaIpKjppX+cBfTASF2E1US3H2JFBj920Ot3rtDjs=
+golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
+golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+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-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-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-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/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-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-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+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/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.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/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-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+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.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.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.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.35.0 h1:TwIQcH3es+MojMVojxxfQ3l3OF2KzlRxML2xZq0kRo8=
+google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/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/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
+gopkg.in/reform.v1 v1.5.0/go.mod h1:AIv0CbDRJ0ljQwptGeaIXfpDRo02uJwTq92aMFELEeU=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+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.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-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
+gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+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-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+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/api-tests/helpers.go b/api-tests/helpers.go
new file mode 100644
index 0000000000..eb929900d8
--- /dev/null
+++ b/api-tests/helpers.go
@@ -0,0 +1,230 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package pmmapitests
+
+import (
+ "context"
+ "fmt"
+ "math/rand"
+ "reflect"
+ "testing"
+
+ "github.com/percona/pmm/api/inventorypb/json/client"
+ "github.com/percona/pmm/api/inventorypb/json/client/agents"
+ "github.com/percona/pmm/api/inventorypb/json/client/nodes"
+ "github.com/percona/pmm/api/inventorypb/json/client/services"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+)
+
+type ErrorResponse interface {
+ Code() int
+}
+
+// A minimal subset of *testing.T that we use that is also should be implemented by *expectedFailureTestingT.
+type TestingT interface {
+ Helper()
+ Name() string
+ Errorf(format string, args ...interface{})
+ FailNow()
+}
+
+// TestString returns semi-random string that can be used as a test data.
+func TestString(t TestingT, name string) string {
+ t.Helper()
+
+ n := rand.Int() //nolint:gosec
+ return fmt.Sprintf("pmm-api-tests/%s/%s/%s/%d", Hostname, t.Name(), name, n)
+}
+
+// AssertAPIErrorf check that actual API error equals expected.
+func AssertAPIErrorf(t TestingT, actual error, httpStatus int, grpcCode codes.Code, format string, a ...interface{}) {
+ t.Helper()
+
+ require.Error(t, actual)
+
+ require.Implementsf(t, new(ErrorResponse), actual, "Wrong response type. Expected %T, got %T.\nError message: %v", new(ErrorResponse), actual, actual)
+
+ assert.Equal(t, httpStatus, actual.(ErrorResponse).Code())
+
+ // Have to use reflect because there are a lot of types with the same structure and different names.
+ payload := reflect.ValueOf(actual).Elem().FieldByName("Payload")
+ require.True(t, payload.IsValid(), "Wrong response structure. There is no field Payload.")
+
+ codeField := payload.Elem().FieldByName("Code")
+ require.True(t, codeField.IsValid(), "Wrong response structure. There is no field Code in Payload.")
+ assert.Equal(t, int64(grpcCode), codeField.Int(), "gRPC status codes are not equal")
+
+ errorField := payload.Elem().FieldByName("Error")
+ require.True(t, errorField.IsValid(), "Wrong response structure. There is no field Error in Payload.")
+ if len(a) > 0 {
+ format = fmt.Sprintf(format, a...)
+ }
+ assert.Equal(t, format, errorField.String())
+}
+
+func ExpectFailure(t *testing.T, link string) *expectedFailureTestingT {
+ return &expectedFailureTestingT{
+ t: t,
+ link: link,
+ }
+}
+
+// expectedFailureTestingT expects that test will fail.
+// if test is failed we skip it
+// if it doesn't we call Fail
+type expectedFailureTestingT struct {
+ t *testing.T
+ errors []string
+ failed bool
+ link string
+}
+
+func (tt *expectedFailureTestingT) Helper() { tt.t.Helper() }
+func (tt *expectedFailureTestingT) Name() string { return tt.t.Name() }
+
+func (tt *expectedFailureTestingT) Errorf(format string, args ...interface{}) {
+ tt.errors = append(tt.errors, fmt.Sprintf(format, args...))
+ tt.failed = true
+}
+
+func (tt *expectedFailureTestingT) FailNow() {
+ tt.failed = true
+
+ // We have to set unexported testing.T.finished = true to make everything work,
+ // but we can't call tt.t.FailNow() as it calls Fail().
+ tt.t.SkipNow()
+}
+
+func (tt *expectedFailureTestingT) Check() {
+ tt.t.Helper()
+
+ if tt.failed {
+ for _, v := range tt.errors {
+ tt.t.Log(v)
+ }
+ tt.t.Skipf("Expected failure: %s", tt.link)
+ return
+ }
+
+ tt.t.Fatalf("%s expected to fail, but didn't: %s", tt.Name(), tt.link)
+}
+
+func RemoveNodes(t TestingT, nodeIDs ...string) {
+ t.Helper()
+
+ for _, nodeID := range nodeIDs {
+ params := &nodes.RemoveNodeParams{
+ Body: nodes.RemoveNodeBody{
+ NodeID: nodeID,
+ },
+ Context: context.Background(),
+ }
+ res, err := client.Default.Nodes.RemoveNode(params)
+ assert.NoError(t, err)
+ assert.NotNil(t, res)
+ }
+}
+
+func RemoveServices(t TestingT, serviceIDs ...string) {
+ t.Helper()
+
+ for _, serviceID := range serviceIDs {
+ params := &services.RemoveServiceParams{
+ Body: services.RemoveServiceBody{
+ ServiceID: serviceID,
+ Force: true,
+ },
+ Context: context.Background(),
+ }
+ res, err := client.Default.Services.RemoveService(params)
+ assert.NoError(t, err)
+ assert.NotNil(t, res)
+ }
+}
+
+func RemoveAgents(t TestingT, agentIDs ...string) {
+ t.Helper()
+
+ for _, agentID := range agentIDs {
+ params := &agents.RemoveAgentParams{
+ Body: agents.RemoveAgentBody{
+ AgentID: agentID,
+ },
+ Context: context.Background(),
+ }
+ res, err := client.Default.Agents.RemoveAgent(params)
+ assert.NoError(t, err)
+ assert.NotNil(t, res)
+ }
+}
+
+func AddGenericNode(t TestingT, nodeName string) *nodes.AddGenericNodeOKBodyGeneric {
+ t.Helper()
+
+ params := &nodes.AddGenericNodeParams{
+ Body: nodes.AddGenericNodeBody{
+ NodeName: nodeName,
+ Address: "10.10.10.10",
+ },
+ Context: Context,
+ }
+ res, err := client.Default.Nodes.AddGenericNode(params)
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ require.NotNil(t, res.Payload)
+ require.NotNil(t, res.Payload.Generic)
+ return res.Payload.Generic
+}
+
+func AddRemoteNode(t TestingT, nodeName string) *nodes.AddRemoteNodeOKBody {
+ t.Helper()
+
+ params := &nodes.AddRemoteNodeParams{
+ Body: nodes.AddRemoteNodeBody{
+ NodeName: nodeName,
+ Address: "10.10.10.10",
+ },
+ Context: Context,
+ }
+ res, err := client.Default.Nodes.AddRemoteNode(params)
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ return res.Payload
+}
+
+func AddPMMAgent(t TestingT, nodeID string) *agents.AddPMMAgentOKBody {
+ t.Helper()
+
+ res, err := client.Default.Agents.AddPMMAgent(&agents.AddPMMAgentParams{
+ Body: agents.AddPMMAgentBody{
+ RunsOnNodeID: nodeID,
+ },
+ Context: Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ return res.Payload
+}
+
+// check interfaces
+var (
+ _ assert.TestingT = (*expectedFailureTestingT)(nil)
+ _ require.TestingT = (*expectedFailureTestingT)(nil)
+ _ TestingT = (*expectedFailureTestingT)(nil)
+)
diff --git a/api-tests/init.go b/api-tests/init.go
new file mode 100644
index 0000000000..4d4712503c
--- /dev/null
+++ b/api-tests/init.go
@@ -0,0 +1,241 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package pmmapitests
+
+import (
+ "context"
+ "crypto/tls"
+ "flag"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "math/rand"
+ "net/http"
+ "net/url"
+ "os"
+ "os/signal"
+ "syscall"
+ "testing"
+ "time"
+
+ "github.com/brianvoe/gofakeit/v6"
+ "github.com/go-openapi/runtime"
+ httptransport "github.com/go-openapi/runtime/client"
+ "github.com/percona/pmm/api/alertmanager/amclient"
+ inventoryClient "github.com/percona/pmm/api/inventorypb/json/client"
+ backupsClient "github.com/percona/pmm/api/managementpb/backup/json/client"
+ dbaasClient "github.com/percona/pmm/api/managementpb/dbaas/json/client"
+ channelsClient "github.com/percona/pmm/api/managementpb/ia/json/client"
+ managementClient "github.com/percona/pmm/api/managementpb/json/client"
+ serverClient "github.com/percona/pmm/api/serverpb/json/client"
+ "github.com/percona/pmm/utils/tlsconfig"
+ "github.com/sirupsen/logrus"
+ "golang.org/x/sys/unix"
+)
+
+//nolint:gochecknoglobals
+var (
+ // Context is canceled on SIGTERM or SIGINT. Tests should cleanup and exit.
+ Context context.Context
+
+ // BaseURL contains PMM Server base URL like https://admin:admin@127.0.0.1:8443/.
+ BaseURL *url.URL
+
+ // Hostname contains local hostname that is used for generating test data.
+ Hostname string
+
+ // True if -debug or -trace flag is passed.
+ Debug bool
+
+ // RunUpdateTest is true if PMM Server update should be tested.
+ RunUpdateTest bool
+
+ // RunSTTTests is true if STT tests should be run.
+ RunSTTTests bool
+
+ // RunIATests is true if IA tests should be run.
+ RunIATests bool
+
+ // Kubeconfig contains kubeconfig.
+ Kubeconfig string
+)
+
+// ErrFromNginx is an error type for nginx HTML response.
+type ErrFromNginx string
+
+// Error implements error interface.
+func (e *ErrFromNginx) Error() string {
+ return "response from nginx: " + string(*e)
+}
+
+// GoString implements fmt.GoStringer interface.
+func (e *ErrFromNginx) GoString() string {
+ return fmt.Sprintf("ErrFromNginx(%q)", string(*e))
+}
+
+// Transport returns configured Swagger transport for given URL.
+func Transport(baseURL *url.URL, insecureTLS bool) *httptransport.Runtime {
+ transport := httptransport.New(baseURL.Host, baseURL.Path, []string{baseURL.Scheme})
+ if u := baseURL.User; u != nil {
+ password, _ := u.Password()
+ transport.DefaultAuthentication = httptransport.BasicAuth(u.Username(), password)
+ }
+ transport.SetLogger(logrus.WithField("component", "client"))
+ transport.SetDebug(logrus.GetLevel() >= logrus.DebugLevel)
+ transport.Context = context.Background() // not Context - do not cancel the whole transport
+
+ // set error handlers for nginx responses if pmm-managed is down
+ errorConsumer := runtime.ConsumerFunc(func(reader io.Reader, data interface{}) error {
+ b, _ := ioutil.ReadAll(reader)
+ err := ErrFromNginx(string(b))
+ return &err
+ })
+ transport.Consumers = map[string]runtime.Consumer{
+ runtime.JSONMime: runtime.JSONConsumer(),
+ runtime.HTMLMime: errorConsumer,
+ runtime.TextMime: errorConsumer,
+ runtime.DefaultMime: errorConsumer,
+ }
+
+ // disable HTTP/2, set TLS config
+ httpTransport := transport.Transport.(*http.Transport)
+ httpTransport.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{}
+ if baseURL.Scheme == "https" {
+ httpTransport.TLSClientConfig = tlsconfig.Get()
+ httpTransport.TLSClientConfig.ServerName = baseURL.Hostname()
+ httpTransport.TLSClientConfig.InsecureSkipVerify = insecureTLS
+ }
+
+ return transport
+}
+
+//nolint:gochecknoinits
+func init() {
+ seed := time.Now().UnixNano()
+ rand.Seed(seed)
+ gofakeit.SetGlobalFaker(gofakeit.NewCustom(NewConcurrentRand(seed)))
+ gofakeit.Seed(seed)
+
+ debugF := flag.Bool("pmm.debug", false, "Enable debug output [PMM_DEBUG].")
+ traceF := flag.Bool("pmm.trace", false, "Enable trace output [PMM_TRACE].")
+ serverURLF := flag.String("pmm.server-url", "https://admin:admin@127.0.0.1:443/", "PMM Server URL [PMM_SERVER_URL].")
+ serverInsecureTLSF := flag.Bool("pmm.server-insecure-tls", false, "Skip PMM Server TLS certificate validation [PMM_SERVER_INSECURE_TLS].")
+ runUpdateTestF := flag.Bool("pmm.run-update-test", false, "Run PMM Server update test [PMM_RUN_UPDATE_TEST].")
+ kubeconfigF := flag.String("pmm.kubeconfig", "", "Pass kubeconfig file to run DBaaS tests.")
+
+ // FIXME we should rethink it once https://jira.percona.com/browse/PMM-5106 is implemented
+ runSTTTestsF := flag.Bool("pmm.run-stt-tests", false, "Run STT tests that require connected clients [PMM_RUN_STT_TESTS].")
+
+ // TODO remove once IA is out of beta: https://jira.percona.com/browse/PMM-7001
+ runIATestsF := flag.Bool("pmm.run-ia-tests", false, "Run IA tests that require connected clients [PMM_RUN_IA_TESTS].")
+
+ testing.Init()
+ flag.Parse()
+
+ for envVar, f := range map[string]*flag.Flag{
+ "PMM_DEBUG": flag.Lookup("pmm.debug"),
+ "PMM_TRACE": flag.Lookup("pmm.trace"),
+ "PMM_SERVER_URL": flag.Lookup("pmm.server-url"),
+ "PMM_SERVER_INSECURE_TLS": flag.Lookup("pmm.server-insecure-tls"),
+ "PMM_RUN_UPDATE_TEST": flag.Lookup("pmm.run-update-test"),
+ "PMM_RUN_STT_TESTS": flag.Lookup("pmm.run-stt-tests"),
+ "PMM_KUBECONFIG": flag.Lookup("pmm.kubeconfig"),
+ } {
+ env, ok := os.LookupEnv(envVar)
+ if ok {
+ err := f.Value.Set(env)
+ if err != nil {
+ logrus.Fatalf("Invalid ENV variable %s: %s", envVar, env)
+ }
+ }
+ }
+
+ if *debugF {
+ logrus.SetLevel(logrus.DebugLevel)
+ }
+ if *traceF {
+ logrus.SetLevel(logrus.TraceLevel)
+ logrus.SetReportCaller(true)
+ }
+ Debug = *debugF || *traceF
+ RunUpdateTest = *runUpdateTestF
+ RunSTTTests = *runSTTTestsF
+ RunIATests = *runIATestsF
+
+ var cancel context.CancelFunc
+ Context, cancel = context.WithCancel(context.Background())
+
+ // handle termination signals
+ signals := make(chan os.Signal, 1)
+ signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
+ go func() {
+ s := <-signals
+ signal.Stop(signals)
+ logrus.Warnf("Got %s, shutting down...", unix.SignalName(s.(syscall.Signal)))
+ cancel()
+ }()
+
+ var err error
+ BaseURL, err = url.Parse(*serverURLF)
+ if err != nil {
+ logrus.Fatalf("Failed to parse PMM Server URL: %s.", err)
+ }
+ if BaseURL.Host == "" || BaseURL.Scheme == "" {
+ logrus.Fatalf("Invalid PMM Server URL: %s", BaseURL.String())
+ }
+ if BaseURL.Path == "" {
+ BaseURL.Path = "/"
+ }
+ logrus.Debugf("PMM Server URL: %s.", BaseURL)
+
+ Hostname, err = os.Hostname()
+ if err != nil {
+ logrus.Fatalf("Failed to detect hostname: %s", err)
+ }
+
+ if *kubeconfigF != "" {
+ data, err := ioutil.ReadFile(*kubeconfigF)
+ if err != nil {
+ logrus.Fatalf("Failed to read kubeconfig: %s", err)
+ }
+ Kubeconfig = string(data)
+ }
+
+ transport := Transport(BaseURL, *serverInsecureTLSF)
+ alertmanagerTransport := Transport(BaseURL, *serverInsecureTLSF)
+ alertmanagerTransport.BasePath = "/alertmanager/api/v2"
+ transport.Consumers["application/zip"] = runtime.ByteStreamConsumer()
+ inventoryClient.Default = inventoryClient.New(transport, nil)
+ managementClient.Default = managementClient.New(transport, nil)
+ dbaasClient.Default = dbaasClient.New(transport, nil)
+ serverClient.Default = serverClient.New(transport, nil)
+ amclient.Default = amclient.New(alertmanagerTransport, nil)
+ channelsClient.Default = channelsClient.New(transport, nil)
+ backupsClient.Default = backupsClient.New(transport, nil)
+
+ // do not run tests if server is not available
+ _, err = serverClient.Default.Server.Readiness(nil)
+ if err != nil {
+ panic(err)
+ }
+}
+
+// check interfaces
+var (
+ _ error = (*ErrFromNginx)(nil)
+ _ fmt.GoStringer = (*ErrFromNginx)(nil)
+)
diff --git a/api-tests/inventory/agents_azure_database_exporter_test.go b/api-tests/inventory/agents_azure_database_exporter_test.go
new file mode 100644
index 0000000000..b98595d4bf
--- /dev/null
+++ b/api-tests/inventory/agents_azure_database_exporter_test.go
@@ -0,0 +1,314 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package inventory
+
+import (
+ "testing"
+
+ "github.com/percona/pmm/api/inventorypb/json/client"
+ "github.com/percona/pmm/api/inventorypb/json/client/agents"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestAzureDatabaseExporter(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ node := addRemoteAzureDatabaseNode(t, pmmapitests.TestString(t, "Remote node for Azure database exporter"))
+ nodeID := node.RemoteAzureDatabase.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, genericNodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ azureDatabaseExporter := addAzureDatabaseExporter(t, agents.AddAzureDatabaseExporterBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ AzureDatabaseResourceType: "mysql",
+ AzureSubscriptionID: "azure_subscription_id",
+ CustomLabels: map[string]string{
+ "custom_label_azure_database_exporter": "azure_database_exporter",
+ },
+ SkipConnectionCheck: true,
+ })
+ agentID := azureDatabaseExporter.AzureDatabaseExporter.AgentID
+ defer pmmapitests.RemoveAgents(t, agentID)
+
+ getAgentRes, err := client.Default.Agents.GetAgent(&agents.GetAgentParams{
+ Body: agents.GetAgentBody{AgentID: agentID},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, &agents.GetAgentOK{
+ Payload: &agents.GetAgentOKBody{
+ AzureDatabaseExporter: &agents.GetAgentOKBodyAzureDatabaseExporter{
+ NodeID: nodeID,
+ AgentID: agentID,
+ AzureDatabaseSubscriptionID: "azure_subscription_id",
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "custom_label_azure_database_exporter": "azure_database_exporter",
+ },
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, getAgentRes)
+
+ // Test change API.
+ changeAzureDatabaseExporterOK, err := client.Default.Agents.ChangeAzureDatabaseExporter(&agents.ChangeAzureDatabaseExporterParams{
+ Body: agents.ChangeAzureDatabaseExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeAzureDatabaseExporterParamsBodyCommon{
+ Disable: true,
+ RemoveCustomLabels: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeAzureDatabaseExporterOK{
+ Payload: &agents.ChangeAzureDatabaseExporterOKBody{
+ AzureDatabaseExporter: &agents.ChangeAzureDatabaseExporterOKBodyAzureDatabaseExporter{
+ NodeID: nodeID,
+ AgentID: agentID,
+ PMMAgentID: pmmAgentID,
+ AzureDatabaseSubscriptionID: "azure_subscription_id",
+ Disabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, changeAzureDatabaseExporterOK)
+
+ changeAzureDatabaseExporterOK, err = client.Default.Agents.ChangeAzureDatabaseExporter(&agents.ChangeAzureDatabaseExporterParams{
+ Body: agents.ChangeAzureDatabaseExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeAzureDatabaseExporterParamsBodyCommon{
+ Enable: true,
+ CustomLabels: map[string]string{
+ "new_label": "azure_database_exporter",
+ },
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeAzureDatabaseExporterOK{
+ Payload: &agents.ChangeAzureDatabaseExporterOKBody{
+ AzureDatabaseExporter: &agents.ChangeAzureDatabaseExporterOKBodyAzureDatabaseExporter{
+ NodeID: nodeID,
+ AgentID: agentID,
+ PMMAgentID: pmmAgentID,
+ AzureDatabaseSubscriptionID: "azure_subscription_id",
+ Disabled: false,
+ CustomLabels: map[string]string{
+ "new_label": "azure_database_exporter",
+ },
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, changeAzureDatabaseExporterOK)
+ })
+
+ t.Run("AddNodeIDEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, genericNodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ res, err := client.Default.Agents.AddAzureDatabaseExporter(&agents.AddAzureDatabaseExporterParams{
+ Body: agents.AddAzureDatabaseExporterBody{
+ NodeID: "",
+ PMMAgentID: pmmAgentID,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field NodeId: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveNodes(t, res.Payload.AzureDatabaseExporter.AgentID)
+ }
+ })
+
+ t.Run("NotExistNodeID", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, genericNodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ res, err := client.Default.Agents.AddAzureDatabaseExporter(&agents.AddAzureDatabaseExporterParams{
+ Body: agents.AddAzureDatabaseExporterBody{
+ NodeID: "pmm-node-id",
+ PMMAgentID: pmmAgentID,
+ AzureDatabaseResourceType: "mysql",
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Node with ID \"pmm-node-id\" not found.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.AzureDatabaseExporter.AgentID)
+ }
+ })
+
+ t.Run("NotExistPMMAgentID", func(t *testing.T) {
+ t.Parallel()
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ res, err := client.Default.Agents.AddAzureDatabaseExporter(&agents.AddAzureDatabaseExporterParams{
+ Body: agents.AddAzureDatabaseExporterBody{
+ NodeID: "nodeID",
+ PMMAgentID: "pmm-not-exist-server",
+ AzureDatabaseResourceType: "mysql",
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Agent with ID \"pmm-not-exist-server\" not found.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.AzureDatabaseExporter.AgentID)
+ }
+ })
+
+ t.Run("With PushMetrics", func(t *testing.T) {
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ node := addRemoteAzureDatabaseNode(t, pmmapitests.TestString(t, "Remote node for Azure database exporter"))
+ nodeID := node.RemoteAzureDatabase.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, genericNodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ azureDatabaseExporter := addAzureDatabaseExporter(t, agents.AddAzureDatabaseExporterBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ AzureSubscriptionID: "azure_subscription_id",
+ CustomLabels: map[string]string{
+ "custom_label_azure_database_exporter": "azure_database_exporter",
+ },
+ SkipConnectionCheck: true,
+ AzureDatabaseResourceType: "mysql",
+ })
+ agentID := azureDatabaseExporter.AzureDatabaseExporter.AgentID
+ defer pmmapitests.RemoveAgents(t, agentID)
+
+ getAgentRes, err := client.Default.Agents.GetAgent(&agents.GetAgentParams{
+ Body: agents.GetAgentBody{AgentID: agentID},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ assert.Equal(t, &agents.GetAgentOK{
+ Payload: &agents.GetAgentOKBody{
+ AzureDatabaseExporter: &agents.GetAgentOKBodyAzureDatabaseExporter{
+ NodeID: nodeID,
+ AgentID: agentID,
+ PMMAgentID: pmmAgentID,
+ AzureDatabaseSubscriptionID: "azure_subscription_id",
+ CustomLabels: map[string]string{
+ "custom_label_azure_database_exporter": "azure_database_exporter",
+ },
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, getAgentRes)
+
+ // Test change API.
+ changeAzureDatabaseExporterOK, err := client.Default.Agents.ChangeAzureDatabaseExporter(&agents.ChangeAzureDatabaseExporterParams{
+ Body: agents.ChangeAzureDatabaseExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeAzureDatabaseExporterParamsBodyCommon{
+ EnablePushMetrics: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeAzureDatabaseExporterOK{
+ Payload: &agents.ChangeAzureDatabaseExporterOKBody{
+ AzureDatabaseExporter: &agents.ChangeAzureDatabaseExporterOKBodyAzureDatabaseExporter{
+ NodeID: nodeID,
+ AgentID: agentID,
+ PMMAgentID: pmmAgentID,
+ AzureDatabaseSubscriptionID: "azure_subscription_id",
+ CustomLabels: map[string]string{
+ "custom_label_azure_database_exporter": "azure_database_exporter",
+ },
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, changeAzureDatabaseExporterOK)
+
+ changeAzureDatabaseExporterOK, err = client.Default.Agents.ChangeAzureDatabaseExporter(&agents.ChangeAzureDatabaseExporterParams{
+ Body: agents.ChangeAzureDatabaseExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeAzureDatabaseExporterParamsBodyCommon{
+ DisablePushMetrics: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeAzureDatabaseExporterOK{
+ Payload: &agents.ChangeAzureDatabaseExporterOKBody{
+ AzureDatabaseExporter: &agents.ChangeAzureDatabaseExporterOKBodyAzureDatabaseExporter{
+ NodeID: nodeID,
+ AgentID: agentID,
+ PMMAgentID: pmmAgentID,
+ AzureDatabaseSubscriptionID: "azure_subscription_id",
+ CustomLabels: map[string]string{
+ "custom_label_azure_database_exporter": "azure_database_exporter",
+ },
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, changeAzureDatabaseExporterOK)
+ _, err = client.Default.Agents.ChangeAzureDatabaseExporter(&agents.ChangeAzureDatabaseExporterParams{
+ Body: agents.ChangeAzureDatabaseExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeAzureDatabaseExporterParamsBodyCommon{
+ EnablePushMetrics: true,
+ DisablePushMetrics: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "expected one of param: enable_push_metrics or disable_push_metrics")
+ })
+}
diff --git a/api-tests/inventory/agents_external_exporter_test.go b/api-tests/inventory/agents_external_exporter_test.go
new file mode 100644
index 0000000000..e9fd112719
--- /dev/null
+++ b/api-tests/inventory/agents_external_exporter_test.go
@@ -0,0 +1,428 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package inventory
+
+import (
+ "testing"
+
+ "github.com/percona/pmm/api/inventorypb/json/client"
+ "github.com/percona/pmm/api/inventorypb/json/client/agents"
+ "github.com/percona/pmm/api/inventorypb/json/client/services"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestExternalExporter(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ service := addExternalService(t, services.AddExternalServiceBody{
+ NodeID: genericNodeID,
+ ServiceName: pmmapitests.TestString(t, "External Service for External Exporter test"),
+ Group: "external",
+ })
+ serviceID := service.External.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ ExternalExporter := addExternalExporter(t, agents.AddExternalExporterBody{
+ RunsOnNodeID: genericNodeID,
+ ServiceID: serviceID,
+ ListenPort: 12345,
+ CustomLabels: map[string]string{
+ "custom_label_for_external_exporter": "external_exporter",
+ },
+ })
+ agentID := ExternalExporter.ExternalExporter.AgentID
+ defer pmmapitests.RemoveAgents(t, agentID)
+
+ getAgentRes, err := client.Default.Agents.GetAgent(&agents.GetAgentParams{
+ Body: agents.GetAgentBody{AgentID: agentID},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, &agents.GetAgentOKBody{
+ ExternalExporter: &agents.GetAgentOKBodyExternalExporter{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ RunsOnNodeID: genericNodeID,
+ Scheme: "http",
+ MetricsPath: "/metrics",
+ ListenPort: 12345,
+ CustomLabels: map[string]string{
+ "custom_label_for_external_exporter": "external_exporter",
+ },
+ },
+ }, getAgentRes.Payload)
+ })
+
+ t.Run("Advanced", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ node := pmmapitests.AddRemoteNode(t, pmmapitests.TestString(t, "Remote node for external exporter"))
+ nodeID := node.Remote.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ service := addExternalService(t, services.AddExternalServiceBody{
+ NodeID: nodeID,
+ ServiceName: pmmapitests.TestString(t, "External Service for External Exporter test"),
+ Group: "external",
+ })
+ serviceID := service.External.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ ExternalExporter := addExternalExporter(t, agents.AddExternalExporterBody{
+ RunsOnNodeID: genericNodeID,
+ ServiceID: serviceID,
+ Username: "username",
+ Password: "password",
+ Scheme: "https",
+ MetricsPath: "/metrics-hr",
+ ListenPort: 12345,
+ CustomLabels: map[string]string{
+ "custom_label_external_exporter": "external_exporter",
+ },
+ })
+ agentID := ExternalExporter.ExternalExporter.AgentID
+ defer pmmapitests.RemoveAgents(t, agentID)
+
+ getAgentRes, err := client.Default.Agents.GetAgent(&agents.GetAgentParams{
+ Body: agents.GetAgentBody{AgentID: agentID},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, &agents.GetAgentOKBody{
+ ExternalExporter: &agents.GetAgentOKBodyExternalExporter{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ RunsOnNodeID: genericNodeID,
+ Username: "username",
+ Scheme: "https",
+ MetricsPath: "/metrics-hr",
+ ListenPort: 12345,
+ CustomLabels: map[string]string{
+ "custom_label_external_exporter": "external_exporter",
+ },
+ },
+ }, getAgentRes.Payload)
+
+ // Test change API.
+ changeExternalExporterOK, err := client.Default.Agents.ChangeExternalExporter(&agents.ChangeExternalExporterParams{
+ Body: agents.ChangeExternalExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeExternalExporterParamsBodyCommon{
+ Disable: true,
+ RemoveCustomLabels: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeExternalExporterOKBody{
+ ExternalExporter: &agents.ChangeExternalExporterOKBodyExternalExporter{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ RunsOnNodeID: genericNodeID,
+ Username: "username",
+ Scheme: "https",
+ MetricsPath: "/metrics-hr",
+ ListenPort: 12345,
+ Disabled: true,
+ },
+ }, changeExternalExporterOK.Payload)
+
+ changeExternalExporterOK, err = client.Default.Agents.ChangeExternalExporter(&agents.ChangeExternalExporterParams{
+ Body: agents.ChangeExternalExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeExternalExporterParamsBodyCommon{
+ Enable: true,
+ CustomLabels: map[string]string{
+ "new_label": "external_exporter",
+ },
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeExternalExporterOKBody{
+ ExternalExporter: &agents.ChangeExternalExporterOKBodyExternalExporter{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ RunsOnNodeID: genericNodeID,
+ Username: "username",
+ Scheme: "https",
+ MetricsPath: "/metrics-hr",
+ ListenPort: 12345,
+ Disabled: false,
+ CustomLabels: map[string]string{
+ "new_label": "external_exporter",
+ },
+ },
+ }, changeExternalExporterOK.Payload)
+ })
+
+ t.Run("AddServiceIDEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ res, err := client.Default.Agents.AddExternalExporter(&agents.AddExternalExporterParams{
+ Body: agents.AddExternalExporterBody{
+ ServiceID: "",
+ RunsOnNodeID: genericNodeID,
+ ListenPort: 12345,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Empty Service ID.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveNodes(t, res.Payload.ExternalExporter.AgentID)
+ }
+ })
+
+ t.Run("AddListenPortEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ service := addExternalService(t, services.AddExternalServiceBody{
+ NodeID: genericNodeID,
+ ServiceName: pmmapitests.TestString(t, "External Service for agent"),
+ Group: "external",
+ })
+ serviceID := service.External.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ res, err := client.Default.Agents.AddExternalExporter(&agents.AddExternalExporterParams{
+ Body: agents.AddExternalExporterBody{
+ ServiceID: serviceID,
+ RunsOnNodeID: genericNodeID,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field ListenPort: value '0' must be greater than '0'")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveNodes(t, res.Payload.ExternalExporter.AgentID)
+ }
+ })
+
+ t.Run("AddRunsOnNodeIDEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ service := addExternalService(t, services.AddExternalServiceBody{
+ NodeID: genericNodeID,
+ ServiceName: pmmapitests.TestString(t, "External Service for agent"),
+ Group: "external",
+ })
+ serviceID := service.External.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ res, err := client.Default.Agents.AddExternalExporter(&agents.AddExternalExporterParams{
+ Body: agents.AddExternalExporterBody{
+ ServiceID: serviceID,
+ RunsOnNodeID: "",
+ ListenPort: 12345,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field RunsOnNodeId: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.ExternalExporter.AgentID)
+ }
+ })
+
+ t.Run("NotExistServiceID", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ res, err := client.Default.Agents.AddExternalExporter(&agents.AddExternalExporterParams{
+ Body: agents.AddExternalExporterBody{
+ ServiceID: "pmm-service-id",
+ RunsOnNodeID: genericNodeID,
+ ListenPort: 12345,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Service with ID \"pmm-service-id\" not found.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.ExternalExporter.AgentID)
+ }
+ })
+
+ t.Run("NotExistNodeID", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ service := addExternalService(t, services.AddExternalServiceBody{
+ NodeID: genericNodeID,
+ ServiceName: pmmapitests.TestString(t, "External Service for not exists node ID"),
+ Group: "external",
+ })
+ serviceID := service.External.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ res, err := client.Default.Agents.AddExternalExporter(&agents.AddExternalExporterParams{
+ Body: agents.AddExternalExporterBody{
+ ServiceID: serviceID,
+ RunsOnNodeID: "pmm-not-exist-server",
+ ListenPort: 12345,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Node with ID \"pmm-not-exist-server\" not found.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.ExternalExporter.AgentID)
+ }
+ })
+
+ t.Run("WithPushMetrics", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+ pmmAgent := pmmapitests.AddPMMAgent(t, genericNodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ service := addExternalService(t, services.AddExternalServiceBody{
+ NodeID: genericNodeID,
+ ServiceName: pmmapitests.TestString(t, "External Service for External Exporter test"),
+ })
+ serviceID := service.External.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ ExternalExporter := addExternalExporter(t, agents.AddExternalExporterBody{
+ RunsOnNodeID: genericNodeID,
+ ServiceID: serviceID,
+ ListenPort: 12345,
+ CustomLabels: map[string]string{
+ "custom_label_for_external_exporter": "external_exporter",
+ },
+ PushMetrics: true,
+ })
+ agentID := ExternalExporter.ExternalExporter.AgentID
+ defer pmmapitests.RemoveAgents(t, agentID)
+
+ getAgentRes, err := client.Default.Agents.GetAgent(&agents.GetAgentParams{
+ Body: agents.GetAgentBody{AgentID: agentID},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, &agents.GetAgentOKBody{
+ ExternalExporter: &agents.GetAgentOKBodyExternalExporter{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ RunsOnNodeID: genericNodeID,
+ Scheme: "http",
+ MetricsPath: "/metrics",
+ ListenPort: 12345,
+ CustomLabels: map[string]string{
+ "custom_label_for_external_exporter": "external_exporter",
+ },
+ PushMetricsEnabled: true,
+ },
+ }, getAgentRes.Payload)
+
+ // Test change API.
+ changeExternalExporterOK, err := client.Default.Agents.ChangeExternalExporter(&agents.ChangeExternalExporterParams{
+ Body: agents.ChangeExternalExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeExternalExporterParamsBodyCommon{
+ DisablePushMetrics: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeExternalExporterOKBody{
+ ExternalExporter: &agents.ChangeExternalExporterOKBodyExternalExporter{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ RunsOnNodeID: genericNodeID,
+ Scheme: "http",
+ MetricsPath: "/metrics",
+ ListenPort: 12345,
+ CustomLabels: map[string]string{
+ "custom_label_for_external_exporter": "external_exporter",
+ },
+ PushMetricsEnabled: false,
+ },
+ }, changeExternalExporterOK.Payload)
+
+ changeExternalExporterOK, err = client.Default.Agents.ChangeExternalExporter(&agents.ChangeExternalExporterParams{
+ Body: agents.ChangeExternalExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeExternalExporterParamsBodyCommon{
+ EnablePushMetrics: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeExternalExporterOKBody{
+ ExternalExporter: &agents.ChangeExternalExporterOKBodyExternalExporter{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ RunsOnNodeID: genericNodeID,
+ Scheme: "http",
+ MetricsPath: "/metrics",
+ ListenPort: 12345,
+ CustomLabels: map[string]string{
+ "custom_label_for_external_exporter": "external_exporter",
+ },
+ PushMetricsEnabled: true,
+ },
+ }, changeExternalExporterOK.Payload)
+
+ _, err = client.Default.Agents.ChangeExternalExporter(&agents.ChangeExternalExporterParams{
+ Body: agents.ChangeExternalExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeExternalExporterParamsBodyCommon{
+ EnablePushMetrics: true,
+ DisablePushMetrics: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "expected one of param: enable_push_metrics or disable_push_metrics")
+ })
+}
diff --git a/api-tests/inventory/agents_mongodb_exporter_test.go b/api-tests/inventory/agents_mongodb_exporter_test.go
new file mode 100644
index 0000000000..7b605a0558
--- /dev/null
+++ b/api-tests/inventory/agents_mongodb_exporter_test.go
@@ -0,0 +1,382 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package inventory
+
+import (
+ "testing"
+
+ "github.com/percona/pmm/api/inventorypb/json/client"
+ "github.com/percona/pmm/api/inventorypb/json/client/agents"
+ "github.com/percona/pmm/api/inventorypb/json/client/services"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestMongoDBExporter(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ node := pmmapitests.AddRemoteNode(t, pmmapitests.TestString(t, "Remote node for Node exporter"))
+ nodeID := node.Remote.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ service := addMongoDBService(t, services.AddMongoDBServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 3306,
+ ServiceName: pmmapitests.TestString(t, "MongoDB Service for MongoDBExporter test"),
+ })
+ serviceID := service.Mongodb.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, nodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ mongoDBExporter := addMongoDBExporter(t, agents.AddMongoDBExporterBody{
+ ServiceID: serviceID,
+ Username: "username",
+ Password: "password",
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "new_label": "mongodb_exporter",
+ },
+
+ SkipConnectionCheck: true,
+ })
+ agentID := mongoDBExporter.MongodbExporter.AgentID
+ defer pmmapitests.RemoveAgents(t, agentID)
+
+ getAgentRes, err := client.Default.Agents.GetAgent(&agents.GetAgentParams{
+ Body: agents.GetAgentBody{AgentID: agentID},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, &agents.GetAgentOK{
+ Payload: &agents.GetAgentOKBody{
+ MongodbExporter: &agents.GetAgentOKBodyMongodbExporter{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "new_label": "mongodb_exporter",
+ },
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, getAgentRes)
+
+ // Test change API.
+ changeMongoDBExporterOK, err := client.Default.Agents.ChangeMongoDBExporter(&agents.ChangeMongoDBExporterParams{
+ Body: agents.ChangeMongoDBExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeMongoDBExporterParamsBodyCommon{
+ Disable: true,
+ RemoveCustomLabels: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeMongoDBExporterOK{
+ Payload: &agents.ChangeMongoDBExporterOKBody{
+ MongodbExporter: &agents.ChangeMongoDBExporterOKBodyMongodbExporter{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ Disabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, changeMongoDBExporterOK)
+
+ changeMongoDBExporterOK, err = client.Default.Agents.ChangeMongoDBExporter(&agents.ChangeMongoDBExporterParams{
+ Body: agents.ChangeMongoDBExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeMongoDBExporterParamsBodyCommon{
+ Enable: true,
+ CustomLabels: map[string]string{
+ "new_label": "mongodb_exporter",
+ },
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeMongoDBExporterOK{
+ Payload: &agents.ChangeMongoDBExporterOKBody{
+ MongodbExporter: &agents.ChangeMongoDBExporterOKBodyMongodbExporter{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ Disabled: false,
+ CustomLabels: map[string]string{
+ "new_label": "mongodb_exporter",
+ },
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, changeMongoDBExporterOK)
+ })
+
+ t.Run("AddServiceIDEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, genericNodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ res, err := client.Default.Agents.AddMongoDBExporter(&agents.AddMongoDBExporterParams{
+ Body: agents.AddMongoDBExporterBody{
+ ServiceID: "",
+ PMMAgentID: pmmAgentID,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field ServiceId: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.MongodbExporter.AgentID)
+ }
+ })
+
+ t.Run("AddPMMAgentIDEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ service := addMongoDBService(t, services.AddMongoDBServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 3306,
+ ServiceName: pmmapitests.TestString(t, "MongoDB Service for agent"),
+ })
+ serviceID := service.Mongodb.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ res, err := client.Default.Agents.AddMongoDBExporter(&agents.AddMongoDBExporterParams{
+ Body: agents.AddMongoDBExporterBody{
+ ServiceID: serviceID,
+ PMMAgentID: "",
+ Username: "username",
+ Password: "password",
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field PmmAgentId: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.MongodbExporter.AgentID)
+ }
+ })
+
+ t.Run("NotExistServiceID", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, genericNodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ res, err := client.Default.Agents.AddMongoDBExporter(&agents.AddMongoDBExporterParams{
+ Body: agents.AddMongoDBExporterBody{
+ ServiceID: "pmm-service-id",
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ Password: "password",
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Service with ID \"pmm-service-id\" not found.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.MongodbExporter.AgentID)
+ }
+ })
+
+ t.Run("NotExistPMMAgentID", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ service := addMongoDBService(t, services.AddMongoDBServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 3306,
+ ServiceName: pmmapitests.TestString(t, "MongoDB Service for not exists node ID"),
+ })
+ serviceID := service.Mongodb.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ res, err := client.Default.Agents.AddMongoDBExporter(&agents.AddMongoDBExporterParams{
+ Body: agents.AddMongoDBExporterBody{
+ ServiceID: serviceID,
+ PMMAgentID: "pmm-not-exist-server",
+ Username: "username",
+ Password: "password",
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Agent with ID \"pmm-not-exist-server\" not found.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.MongodbExporter.AgentID)
+ }
+ })
+
+ t.Run("With PushMetrics", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ node := pmmapitests.AddRemoteNode(t, pmmapitests.TestString(t, "Remote node for Node exporter"))
+ nodeID := node.Remote.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ service := addMongoDBService(t, services.AddMongoDBServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 3306,
+ ServiceName: pmmapitests.TestString(t, "MongoDB Service for MongoDBExporter test"),
+ })
+ serviceID := service.Mongodb.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, nodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ mongoDBExporter := addMongoDBExporter(t, agents.AddMongoDBExporterBody{
+ ServiceID: serviceID,
+ Username: "username",
+ Password: "password",
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "new_label": "mongodb_exporter",
+ },
+
+ SkipConnectionCheck: true,
+ PushMetrics: true,
+ })
+ agentID := mongoDBExporter.MongodbExporter.AgentID
+ defer pmmapitests.RemoveAgents(t, agentID)
+
+ getAgentRes, err := client.Default.Agents.GetAgent(&agents.GetAgentParams{
+ Body: agents.GetAgentBody{AgentID: agentID},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, &agents.GetAgentOK{
+ Payload: &agents.GetAgentOKBody{
+ MongodbExporter: &agents.GetAgentOKBodyMongodbExporter{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "new_label": "mongodb_exporter",
+ },
+ PushMetricsEnabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, getAgentRes)
+
+ // Test change API.
+ changeMongoDBExporterOK, err := client.Default.Agents.ChangeMongoDBExporter(&agents.ChangeMongoDBExporterParams{
+ Body: agents.ChangeMongoDBExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeMongoDBExporterParamsBodyCommon{
+ DisablePushMetrics: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeMongoDBExporterOK{
+ Payload: &agents.ChangeMongoDBExporterOKBody{
+ MongodbExporter: &agents.ChangeMongoDBExporterOKBodyMongodbExporter{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "new_label": "mongodb_exporter",
+ },
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, changeMongoDBExporterOK)
+
+ changeMongoDBExporterOK, err = client.Default.Agents.ChangeMongoDBExporter(&agents.ChangeMongoDBExporterParams{
+ Body: agents.ChangeMongoDBExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeMongoDBExporterParamsBodyCommon{
+ EnablePushMetrics: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeMongoDBExporterOK{
+ Payload: &agents.ChangeMongoDBExporterOKBody{
+ MongodbExporter: &agents.ChangeMongoDBExporterOKBodyMongodbExporter{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "new_label": "mongodb_exporter",
+ },
+ PushMetricsEnabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, changeMongoDBExporterOK)
+
+ _, err = client.Default.Agents.ChangeMongoDBExporter(&agents.ChangeMongoDBExporterParams{
+ Body: agents.ChangeMongoDBExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeMongoDBExporterParamsBodyCommon{
+ EnablePushMetrics: true,
+ DisablePushMetrics: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "expected one of param: enable_push_metrics or disable_push_metrics")
+ })
+}
diff --git a/api-tests/inventory/agents_mysqld_exporter_test.go b/api-tests/inventory/agents_mysqld_exporter_test.go
new file mode 100644
index 0000000000..16359fc869
--- /dev/null
+++ b/api-tests/inventory/agents_mysqld_exporter_test.go
@@ -0,0 +1,439 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package inventory
+
+import (
+ "testing"
+
+ "github.com/percona/pmm/api/inventorypb/json/client"
+ "github.com/percona/pmm/api/inventorypb/json/client/agents"
+ "github.com/percona/pmm/api/inventorypb/json/client/services"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestMySQLdExporter(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ node := pmmapitests.AddRemoteNode(t, pmmapitests.TestString(t, "Remote node for Node exporter"))
+ nodeID := node.Remote.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ service := addMySQLService(t, services.AddMySQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 3306,
+ ServiceName: pmmapitests.TestString(t, "MySQL Service for MySQLdExporter test"),
+ })
+ serviceID := service.Mysql.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, nodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ mySqldExporter := addMySQLdExporter(t, agents.AddMySQLdExporterBody{
+ ServiceID: serviceID,
+ Username: "username",
+ Password: "password",
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "custom_label_mysql_exporter": "mysql_exporter",
+ },
+
+ SkipConnectionCheck: true,
+ TablestatsGroupTableLimit: 2000,
+ })
+ assert.EqualValues(t, 0, mySqldExporter.TableCount)
+ assert.EqualValues(t, 2000, mySqldExporter.MysqldExporter.TablestatsGroupTableLimit)
+ agentID := mySqldExporter.MysqldExporter.AgentID
+ defer pmmapitests.RemoveAgents(t, agentID)
+
+ getAgentRes, err := client.Default.Agents.GetAgent(&agents.GetAgentParams{
+ Body: agents.GetAgentBody{AgentID: agentID},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, &agents.GetAgentOKBody{
+ MysqldExporter: &agents.GetAgentOKBodyMysqldExporter{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "custom_label_mysql_exporter": "mysql_exporter",
+ },
+ TablestatsGroupTableLimit: 2000,
+ Status: &AgentStatusUnknown,
+ },
+ }, getAgentRes.Payload)
+
+ // Test change API.
+ changeMySQLdExporterOK, err := client.Default.Agents.ChangeMySQLdExporter(&agents.ChangeMySQLdExporterParams{
+ Body: agents.ChangeMySQLdExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeMySQLdExporterParamsBodyCommon{
+ Disable: true,
+ RemoveCustomLabels: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeMySQLdExporterOKBody{
+ MysqldExporter: &agents.ChangeMySQLdExporterOKBodyMysqldExporter{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ Disabled: true,
+ TablestatsGroupTableLimit: 2000,
+ Status: &AgentStatusUnknown,
+ },
+ }, changeMySQLdExporterOK.Payload)
+
+ changeMySQLdExporterOK, err = client.Default.Agents.ChangeMySQLdExporter(&agents.ChangeMySQLdExporterParams{
+ Body: agents.ChangeMySQLdExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeMySQLdExporterParamsBodyCommon{
+ Enable: true,
+ CustomLabels: map[string]string{
+ "new_label": "mysql_exporter",
+ },
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeMySQLdExporterOKBody{
+ MysqldExporter: &agents.ChangeMySQLdExporterOKBodyMysqldExporter{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ Disabled: false,
+ CustomLabels: map[string]string{
+ "new_label": "mysql_exporter",
+ },
+ TablestatsGroupTableLimit: 2000,
+ Status: &AgentStatusUnknown,
+ },
+ }, changeMySQLdExporterOK.Payload)
+ })
+
+ t.Run("WithRealPMMAgent", func(t *testing.T) {
+ t.Skip("Skipping until we know there are connected agents in the new environment")
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ node := pmmapitests.AddRemoteNode(t, pmmapitests.TestString(t, "Remote node for Node exporter"))
+ nodeID := node.Remote.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ service := addMySQLService(t, services.AddMySQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 3306,
+ ServiceName: pmmapitests.TestString(t, "MySQL Service for MySQLdExporter test"),
+ })
+ serviceID := service.Mysql.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ res, err := client.Default.Agents.ListAgents(&agents.ListAgentsParams{Context: pmmapitests.Context})
+ require.NoError(t, err)
+ require.NotNil(t, res)
+ require.NotZerof(t, len(res.Payload.PMMAgent), "There should be at least one service")
+
+ pmmAgentID := ""
+ for _, agent := range res.Payload.PMMAgent {
+ if agent.Connected {
+ pmmAgentID = agent.AgentID
+ break
+ }
+ }
+ if pmmAgentID == "" {
+ t.Skip("There are no connected agents")
+ }
+
+ mySqldExporter := addMySQLdExporter(t, agents.AddMySQLdExporterBody{
+ ServiceID: serviceID,
+ Username: "pmm-agent", // from pmm-agent docker-compose.yml
+ Password: "pmm-agent-password", // from pmm-agent docker-compose.yml
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "custom_label_mysql_exporter": "mysql_exporter",
+ },
+
+ TablestatsGroupTableLimit: 2000,
+ })
+ assert.Greater(t, mySqldExporter.TableCount, int32(0))
+ assert.EqualValues(t, 2000, mySqldExporter.MysqldExporter.TablestatsGroupTableLimit)
+ agentID := mySqldExporter.MysqldExporter.AgentID
+ defer pmmapitests.RemoveAgents(t, agentID)
+ })
+
+ t.Run("AddServiceIDEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, genericNodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ res, err := client.Default.Agents.AddMySQLdExporter(&agents.AddMySQLdExporterParams{
+ Body: agents.AddMySQLdExporterBody{
+ ServiceID: "",
+ PMMAgentID: pmmAgentID,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field ServiceId: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveNodes(t, res.Payload.MysqldExporter.AgentID)
+ }
+ })
+
+ t.Run("AddPMMAgentIDEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ service := addMySQLService(t, services.AddMySQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 3306,
+ ServiceName: pmmapitests.TestString(t, "MySQL Service for agent"),
+ })
+ serviceID := service.Mysql.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ res, err := client.Default.Agents.AddMySQLdExporter(&agents.AddMySQLdExporterParams{
+ Body: agents.AddMySQLdExporterBody{
+ ServiceID: serviceID,
+ PMMAgentID: "",
+ Username: "username",
+ Password: "password",
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field PmmAgentId: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.MysqldExporter.AgentID)
+ }
+ })
+
+ t.Run("NotExistServiceID", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, genericNodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ res, err := client.Default.Agents.AddMySQLdExporter(&agents.AddMySQLdExporterParams{
+ Body: agents.AddMySQLdExporterBody{
+ ServiceID: "pmm-service-id",
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ Password: "password",
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Service with ID \"pmm-service-id\" not found.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.MysqldExporter.AgentID)
+ }
+ })
+
+ t.Run("NotExistPMMAgentID", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ service := addMySQLService(t, services.AddMySQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 3306,
+ ServiceName: pmmapitests.TestString(t, "MySQL Service for not exists node ID"),
+ })
+ serviceID := service.Mysql.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ res, err := client.Default.Agents.AddMySQLdExporter(&agents.AddMySQLdExporterParams{
+ Body: agents.AddMySQLdExporterBody{
+ ServiceID: serviceID,
+ PMMAgentID: "pmm-not-exist-server",
+ Username: "username",
+ Password: "password",
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Agent with ID \"pmm-not-exist-server\" not found.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.MysqldExporter.AgentID)
+ }
+ })
+
+ t.Run("With PushMetrics", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ node := pmmapitests.AddRemoteNode(t, pmmapitests.TestString(t, "Remote node for Node exporter"))
+ nodeID := node.Remote.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ service := addMySQLService(t, services.AddMySQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 3306,
+ ServiceName: pmmapitests.TestString(t, "MySQL Service for MySQLdExporter test"),
+ })
+ serviceID := service.Mysql.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, nodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ mySqldExporter := addMySQLdExporter(t, agents.AddMySQLdExporterBody{
+ ServiceID: serviceID,
+ Username: "username",
+ Password: "password",
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "custom_label_mysql_exporter": "mysql_exporter",
+ },
+
+ SkipConnectionCheck: true,
+ TablestatsGroupTableLimit: 2000,
+ PushMetrics: true,
+ })
+ assert.EqualValues(t, 0, mySqldExporter.TableCount)
+ assert.EqualValues(t, 2000, mySqldExporter.MysqldExporter.TablestatsGroupTableLimit)
+ agentID := mySqldExporter.MysqldExporter.AgentID
+ defer pmmapitests.RemoveAgents(t, agentID)
+
+ getAgentRes, err := client.Default.Agents.GetAgent(&agents.GetAgentParams{
+ Body: agents.GetAgentBody{AgentID: agentID},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, &agents.GetAgentOKBody{
+ MysqldExporter: &agents.GetAgentOKBodyMysqldExporter{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "custom_label_mysql_exporter": "mysql_exporter",
+ },
+ TablestatsGroupTableLimit: 2000,
+ PushMetricsEnabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ }, getAgentRes.Payload)
+
+ // Test change API.
+ changeMySQLdExporterOK, err := client.Default.Agents.ChangeMySQLdExporter(&agents.ChangeMySQLdExporterParams{
+ Body: agents.ChangeMySQLdExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeMySQLdExporterParamsBodyCommon{
+ DisablePushMetrics: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeMySQLdExporterOKBody{
+ MysqldExporter: &agents.ChangeMySQLdExporterOKBodyMysqldExporter{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "custom_label_mysql_exporter": "mysql_exporter",
+ },
+ TablestatsGroupTableLimit: 2000,
+ Status: &AgentStatusUnknown,
+ },
+ }, changeMySQLdExporterOK.Payload)
+
+ changeMySQLdExporterOK, err = client.Default.Agents.ChangeMySQLdExporter(&agents.ChangeMySQLdExporterParams{
+ Body: agents.ChangeMySQLdExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeMySQLdExporterParamsBodyCommon{
+ EnablePushMetrics: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeMySQLdExporterOKBody{
+ MysqldExporter: &agents.ChangeMySQLdExporterOKBodyMysqldExporter{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "custom_label_mysql_exporter": "mysql_exporter",
+ },
+ TablestatsGroupTableLimit: 2000,
+ PushMetricsEnabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ }, changeMySQLdExporterOK.Payload)
+ _, err = client.Default.Agents.ChangeMySQLdExporter(&agents.ChangeMySQLdExporterParams{
+ Body: agents.ChangeMySQLdExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeMySQLdExporterParamsBodyCommon{
+ Enable: true,
+ CustomLabels: map[string]string{
+ "new_label": "mysql_exporter",
+ },
+ EnablePushMetrics: true,
+ DisablePushMetrics: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "expected one of param: enable_push_metrics or disable_push_metrics")
+ })
+}
diff --git a/api-tests/inventory/agents_node_exporter_test.go b/api-tests/inventory/agents_node_exporter_test.go
new file mode 100644
index 0000000000..74fe437057
--- /dev/null
+++ b/api-tests/inventory/agents_node_exporter_test.go
@@ -0,0 +1,248 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package inventory
+
+import (
+ "testing"
+
+ "github.com/percona/pmm/api/inventorypb/json/client"
+ "github.com/percona/pmm/api/inventorypb/json/client/agents"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestNodeExporter(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ t.Parallel()
+
+ node := pmmapitests.AddRemoteNode(t, pmmapitests.TestString(t, "Remote node for Node exporter"))
+ nodeID := node.Remote.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, nodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ customLabels := map[string]string{
+ "custom_label_node_exporter": "node_exporter",
+ }
+ res := addNodeExporter(t, pmmAgentID, customLabels)
+ agentID := res.Payload.NodeExporter.AgentID
+ defer pmmapitests.RemoveAgents(t, agentID)
+
+ getAgentRes, err := client.Default.Agents.GetAgent(&agents.GetAgentParams{
+ Body: agents.GetAgentBody{AgentID: agentID},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, &agents.GetAgentOK{
+ Payload: &agents.GetAgentOKBody{
+ NodeExporter: &agents.GetAgentOKBodyNodeExporter{
+ AgentID: agentID,
+ PMMAgentID: pmmAgentID,
+ Disabled: false,
+ CustomLabels: customLabels,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, getAgentRes)
+
+ // Test change API.
+ changeNodeExporterOK, err := client.Default.Agents.ChangeNodeExporter(&agents.ChangeNodeExporterParams{
+ Body: agents.ChangeNodeExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeNodeExporterParamsBodyCommon{
+ Disable: true,
+ RemoveCustomLabels: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeNodeExporterOK{
+ Payload: &agents.ChangeNodeExporterOKBody{
+ NodeExporter: &agents.ChangeNodeExporterOKBodyNodeExporter{
+ AgentID: agentID,
+ PMMAgentID: pmmAgentID,
+ Disabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, changeNodeExporterOK)
+
+ changeNodeExporterOK, err = client.Default.Agents.ChangeNodeExporter(&agents.ChangeNodeExporterParams{
+ Body: agents.ChangeNodeExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeNodeExporterParamsBodyCommon{
+ Enable: true,
+ CustomLabels: map[string]string{
+ "new_label": "node_exporter",
+ },
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeNodeExporterOK{
+ Payload: &agents.ChangeNodeExporterOKBody{
+ NodeExporter: &agents.ChangeNodeExporterOKBodyNodeExporter{
+ AgentID: agentID,
+ PMMAgentID: pmmAgentID,
+ Disabled: false,
+ CustomLabels: map[string]string{
+ "new_label": "node_exporter",
+ },
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, changeNodeExporterOK)
+ })
+
+ t.Run("AddPMMAgentIDEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ res, err := client.Default.Agents.AddNodeExporter(&agents.AddNodeExporterParams{
+ Body: agents.AddNodeExporterBody{PMMAgentID: ""},
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field PmmAgentId: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveNodes(t, res.Payload.NodeExporter.AgentID)
+ }
+ })
+
+ t.Run("NotExistPmmAgentID", func(t *testing.T) {
+ t.Parallel()
+
+ res, err := client.Default.Agents.AddNodeExporter(&agents.AddNodeExporterParams{
+ Body: agents.AddNodeExporterBody{PMMAgentID: "pmm-node-exporter-node"},
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Agent with ID \"pmm-node-exporter-node\" not found.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveNodes(t, res.Payload.NodeExporter.AgentID)
+ }
+ })
+
+ t.Run("With PushMetrics", func(t *testing.T) {
+ t.Parallel()
+
+ node := pmmapitests.AddRemoteNode(t, pmmapitests.TestString(t, "Remote node for Node exporter"))
+ nodeID := node.Remote.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, nodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ customLabels := map[string]string{
+ "custom_label_node_exporter": "node_exporter",
+ }
+ res, err := client.Default.Agents.AddNodeExporter(&agents.AddNodeExporterParams{
+ Body: agents.AddNodeExporterBody{
+ PMMAgentID: pmmAgentID,
+ CustomLabels: customLabels,
+ PushMetrics: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ require.NotNil(t, res.Payload.NodeExporter)
+ require.Equal(t, pmmAgentID, res.Payload.NodeExporter.PMMAgentID)
+ agentID := res.Payload.NodeExporter.AgentID
+ defer pmmapitests.RemoveAgents(t, agentID)
+
+ getAgentRes, err := client.Default.Agents.GetAgent(&agents.GetAgentParams{
+ Body: agents.GetAgentBody{AgentID: agentID},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, &agents.GetAgentOK{
+ Payload: &agents.GetAgentOKBody{
+ NodeExporter: &agents.GetAgentOKBodyNodeExporter{
+ AgentID: agentID,
+ PMMAgentID: pmmAgentID,
+ Disabled: false,
+ CustomLabels: customLabels,
+ PushMetricsEnabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, getAgentRes)
+
+ // Test change API.
+ changeNodeExporterOK, err := client.Default.Agents.ChangeNodeExporter(&agents.ChangeNodeExporterParams{
+ Body: agents.ChangeNodeExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeNodeExporterParamsBodyCommon{
+ DisablePushMetrics: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeNodeExporterOK{
+ Payload: &agents.ChangeNodeExporterOKBody{
+ NodeExporter: &agents.ChangeNodeExporterOKBodyNodeExporter{
+ AgentID: agentID,
+ PMMAgentID: pmmAgentID,
+ Disabled: false,
+ CustomLabels: customLabels,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, changeNodeExporterOK)
+
+ changeNodeExporterOK, err = client.Default.Agents.ChangeNodeExporter(&agents.ChangeNodeExporterParams{
+ Body: agents.ChangeNodeExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeNodeExporterParamsBodyCommon{
+ EnablePushMetrics: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeNodeExporterOK{
+ Payload: &agents.ChangeNodeExporterOKBody{
+ NodeExporter: &agents.ChangeNodeExporterOKBodyNodeExporter{
+ AgentID: agentID,
+ PMMAgentID: pmmAgentID,
+ Disabled: false,
+ CustomLabels: customLabels,
+ PushMetricsEnabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, changeNodeExporterOK)
+ _, err = client.Default.Agents.ChangeNodeExporter(&agents.ChangeNodeExporterParams{
+ Body: agents.ChangeNodeExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeNodeExporterParamsBodyCommon{
+ EnablePushMetrics: true,
+ DisablePushMetrics: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "expected one of param: enable_push_metrics or disable_push_metrics")
+ })
+}
diff --git a/api-tests/inventory/agents_postgres_exporter_test.go b/api-tests/inventory/agents_postgres_exporter_test.go
new file mode 100644
index 0000000000..8d4e9211f6
--- /dev/null
+++ b/api-tests/inventory/agents_postgres_exporter_test.go
@@ -0,0 +1,386 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package inventory
+
+import (
+ "testing"
+
+ "github.com/percona/pmm/api/inventorypb/json/client"
+ "github.com/percona/pmm/api/inventorypb/json/client/agents"
+ "github.com/percona/pmm/api/inventorypb/json/client/services"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestPostgresExporter(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ node := pmmapitests.AddRemoteNode(t, pmmapitests.TestString(t, "Remote node for Node exporter"))
+ nodeID := node.Remote.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ service := addPostgreSQLService(t, services.AddPostgreSQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 5432,
+ ServiceName: pmmapitests.TestString(t, "PostgreSQL Service for PostgresExporter test"),
+ })
+ serviceID := service.Postgresql.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, nodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ PostgresExporter := addPostgresExporter(t, agents.AddPostgresExporterBody{
+ ServiceID: serviceID,
+ Username: "username",
+ Password: "password",
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "custom_label_postgres_exporter": "postgres_exporter",
+ },
+
+ SkipConnectionCheck: true,
+ })
+ agentID := PostgresExporter.PostgresExporter.AgentID
+ defer pmmapitests.RemoveAgents(t, agentID)
+
+ getAgentRes, err := client.Default.Agents.GetAgent(&agents.GetAgentParams{
+ Body: agents.GetAgentBody{AgentID: agentID},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, &agents.GetAgentOK{
+ Payload: &agents.GetAgentOKBody{
+ PostgresExporter: &agents.GetAgentOKBodyPostgresExporter{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "custom_label_postgres_exporter": "postgres_exporter",
+ },
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, getAgentRes)
+
+ // Test change API.
+ changePostgresExporterOK, err := client.Default.Agents.ChangePostgresExporter(&agents.ChangePostgresExporterParams{
+ Body: agents.ChangePostgresExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangePostgresExporterParamsBodyCommon{
+ Disable: true,
+ RemoveCustomLabels: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangePostgresExporterOK{
+ Payload: &agents.ChangePostgresExporterOKBody{
+ PostgresExporter: &agents.ChangePostgresExporterOKBodyPostgresExporter{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ Disabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, changePostgresExporterOK)
+
+ changePostgresExporterOK, err = client.Default.Agents.ChangePostgresExporter(&agents.ChangePostgresExporterParams{
+ Body: agents.ChangePostgresExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangePostgresExporterParamsBodyCommon{
+ Enable: true,
+ CustomLabels: map[string]string{
+ "new_label": "postgres_exporter",
+ },
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangePostgresExporterOK{
+ Payload: &agents.ChangePostgresExporterOKBody{
+ PostgresExporter: &agents.ChangePostgresExporterOKBodyPostgresExporter{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ Disabled: false,
+ CustomLabels: map[string]string{
+ "new_label": "postgres_exporter",
+ },
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, changePostgresExporterOK)
+ })
+
+ t.Run("AddServiceIDEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, genericNodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ res, err := client.Default.Agents.AddPostgresExporter(&agents.AddPostgresExporterParams{
+ Body: agents.AddPostgresExporterBody{
+ ServiceID: "",
+ PMMAgentID: pmmAgentID,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field ServiceId: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveNodes(t, res.Payload.PostgresExporter.AgentID)
+ }
+ })
+
+ t.Run("AddPMMAgentIDEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ service := addPostgreSQLService(t, services.AddPostgreSQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 5432,
+ ServiceName: pmmapitests.TestString(t, "PostgreSQL Service for agent"),
+ })
+ serviceID := service.Postgresql.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ res, err := client.Default.Agents.AddPostgresExporter(&agents.AddPostgresExporterParams{
+ Body: agents.AddPostgresExporterBody{
+ ServiceID: serviceID,
+ PMMAgentID: "",
+ Username: "username",
+ Password: "password",
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field PmmAgentId: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.PostgresExporter.AgentID)
+ }
+ })
+
+ t.Run("NotExistServiceID", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, genericNodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ res, err := client.Default.Agents.AddPostgresExporter(&agents.AddPostgresExporterParams{
+ Body: agents.AddPostgresExporterBody{
+ ServiceID: "pmm-service-id",
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ Password: "password",
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Service with ID \"pmm-service-id\" not found.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.PostgresExporter.AgentID)
+ }
+ })
+
+ t.Run("NotExistPMMAgentID", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ service := addPostgreSQLService(t, services.AddPostgreSQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 5432,
+ ServiceName: pmmapitests.TestString(t, "PostgreSQL Service for not exists node ID"),
+ })
+ serviceID := service.Postgresql.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ res, err := client.Default.Agents.AddPostgresExporter(&agents.AddPostgresExporterParams{
+ Body: agents.AddPostgresExporterBody{
+ ServiceID: serviceID,
+ PMMAgentID: "pmm-not-exist-server",
+ Username: "username",
+ Password: "password",
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Agent with ID \"pmm-not-exist-server\" not found.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.PostgresExporter.AgentID)
+ }
+ })
+
+ t.Run("With PushMetrics", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ node := pmmapitests.AddRemoteNode(t, pmmapitests.TestString(t, "Remote node for Node exporter"))
+ nodeID := node.Remote.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ service := addPostgreSQLService(t, services.AddPostgreSQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 5432,
+ ServiceName: pmmapitests.TestString(t, "PostgreSQL Service for PostgresExporter test"),
+ })
+ serviceID := service.Postgresql.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, nodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ PostgresExporter := addPostgresExporter(t, agents.AddPostgresExporterBody{
+ ServiceID: serviceID,
+ Username: "username",
+ Password: "password",
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "custom_label_postgres_exporter": "postgres_exporter",
+ },
+
+ SkipConnectionCheck: true,
+ PushMetrics: true,
+ })
+ agentID := PostgresExporter.PostgresExporter.AgentID
+ defer pmmapitests.RemoveAgents(t, agentID)
+
+ getAgentRes, err := client.Default.Agents.GetAgent(&agents.GetAgentParams{
+ Body: agents.GetAgentBody{AgentID: agentID},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, &agents.GetAgentOK{
+ Payload: &agents.GetAgentOKBody{
+ PostgresExporter: &agents.GetAgentOKBodyPostgresExporter{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "custom_label_postgres_exporter": "postgres_exporter",
+ },
+ PushMetricsEnabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, getAgentRes)
+
+ // Test change API.
+ changePostgresExporterOK, err := client.Default.Agents.ChangePostgresExporter(&agents.ChangePostgresExporterParams{
+ Body: agents.ChangePostgresExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangePostgresExporterParamsBodyCommon{
+ DisablePushMetrics: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangePostgresExporterOK{
+ Payload: &agents.ChangePostgresExporterOKBody{
+ PostgresExporter: &agents.ChangePostgresExporterOKBodyPostgresExporter{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "custom_label_postgres_exporter": "postgres_exporter",
+ },
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, changePostgresExporterOK)
+
+ changePostgresExporterOK, err = client.Default.Agents.ChangePostgresExporter(&agents.ChangePostgresExporterParams{
+ Body: agents.ChangePostgresExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangePostgresExporterParamsBodyCommon{
+ EnablePushMetrics: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangePostgresExporterOK{
+ Payload: &agents.ChangePostgresExporterOKBody{
+ PostgresExporter: &agents.ChangePostgresExporterOKBodyPostgresExporter{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "custom_label_postgres_exporter": "postgres_exporter",
+ },
+ PushMetricsEnabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, changePostgresExporterOK)
+
+ _, err = client.Default.Agents.ChangePostgresExporter(&agents.ChangePostgresExporterParams{
+ Body: agents.ChangePostgresExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangePostgresExporterParamsBodyCommon{
+ Enable: true,
+ CustomLabels: map[string]string{
+ "new_label": "postgres_exporter",
+ },
+ EnablePushMetrics: true,
+ DisablePushMetrics: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "expected one of param: enable_push_metrics or disable_push_metrics")
+ })
+}
diff --git a/api-tests/inventory/agents_proxysql_exporter_test.go b/api-tests/inventory/agents_proxysql_exporter_test.go
new file mode 100644
index 0000000000..b8da643e7e
--- /dev/null
+++ b/api-tests/inventory/agents_proxysql_exporter_test.go
@@ -0,0 +1,377 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package inventory
+
+import (
+ "testing"
+
+ "github.com/percona/pmm/api/inventorypb/json/client"
+ "github.com/percona/pmm/api/inventorypb/json/client/agents"
+ "github.com/percona/pmm/api/inventorypb/json/client/services"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestProxySQLExporter(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ node := pmmapitests.AddRemoteNode(t, pmmapitests.TestString(t, "Remote node for Node exporter"))
+ nodeID := node.Remote.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ service := addProxySQLService(t, services.AddProxySQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 5432,
+ ServiceName: pmmapitests.TestString(t, "ProxySQL Service for ProxySQLExporter test"),
+ })
+ serviceID := service.Proxysql.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, nodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ ProxySQLExporter := addProxySQLExporter(t, agents.AddProxySQLExporterBody{
+ ServiceID: serviceID,
+ Username: "username",
+ Password: "password",
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "custom_label_proxysql_exporter": "proxysql_exporter",
+ },
+
+ SkipConnectionCheck: true,
+ })
+ agentID := ProxySQLExporter.ProxysqlExporter.AgentID
+ defer pmmapitests.RemoveAgents(t, agentID)
+
+ getAgentRes, err := client.Default.Agents.GetAgent(&agents.GetAgentParams{
+ Body: agents.GetAgentBody{AgentID: agentID},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, &agents.GetAgentOK{
+ Payload: &agents.GetAgentOKBody{
+ ProxysqlExporter: &agents.GetAgentOKBodyProxysqlExporter{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "custom_label_proxysql_exporter": "proxysql_exporter",
+ },
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, getAgentRes)
+
+ // Test change API.
+ changeProxySQLExporterOK, err := client.Default.Agents.ChangeProxySQLExporter(&agents.ChangeProxySQLExporterParams{
+ Body: agents.ChangeProxySQLExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeProxySQLExporterParamsBodyCommon{
+ Disable: true,
+ RemoveCustomLabels: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeProxySQLExporterOK{
+ Payload: &agents.ChangeProxySQLExporterOKBody{
+ ProxysqlExporter: &agents.ChangeProxySQLExporterOKBodyProxysqlExporter{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ Disabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, changeProxySQLExporterOK)
+
+ changeProxySQLExporterOK, err = client.Default.Agents.ChangeProxySQLExporter(&agents.ChangeProxySQLExporterParams{
+ Body: agents.ChangeProxySQLExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeProxySQLExporterParamsBodyCommon{
+ Enable: true,
+ CustomLabels: map[string]string{
+ "new_label": "proxysql_exporter",
+ },
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeProxySQLExporterOK{
+ Payload: &agents.ChangeProxySQLExporterOKBody{
+ ProxysqlExporter: &agents.ChangeProxySQLExporterOKBodyProxysqlExporter{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ Disabled: false,
+ CustomLabels: map[string]string{
+ "new_label": "proxysql_exporter",
+ },
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, changeProxySQLExporterOK)
+ })
+
+ t.Run("AddServiceIDEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, genericNodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ res, err := client.Default.Agents.AddProxySQLExporter(&agents.AddProxySQLExporterParams{
+ Body: agents.AddProxySQLExporterBody{
+ ServiceID: "",
+ PMMAgentID: pmmAgentID,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field ServiceId: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveNodes(t, res.Payload.ProxysqlExporter.AgentID)
+ }
+ })
+
+ t.Run("AddPMMAgentIDEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ service := addProxySQLService(t, services.AddProxySQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 5432,
+ ServiceName: pmmapitests.TestString(t, "ProxySQL Service for agent"),
+ })
+ serviceID := service.Proxysql.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ res, err := client.Default.Agents.AddProxySQLExporter(&agents.AddProxySQLExporterParams{
+ Body: agents.AddProxySQLExporterBody{
+ ServiceID: serviceID,
+ PMMAgentID: "",
+ Username: "username",
+ Password: "password",
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field PmmAgentId: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.ProxysqlExporter.AgentID)
+ }
+ })
+
+ t.Run("NotExistServiceID", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, genericNodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ res, err := client.Default.Agents.AddProxySQLExporter(&agents.AddProxySQLExporterParams{
+ Body: agents.AddProxySQLExporterBody{
+ ServiceID: "pmm-service-id",
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ Password: "password",
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Service with ID \"pmm-service-id\" not found.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.ProxysqlExporter.AgentID)
+ }
+ })
+
+ t.Run("NotExistPMMAgentID", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ service := addProxySQLService(t, services.AddProxySQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 5432,
+ ServiceName: pmmapitests.TestString(t, "ProxySQL Service for not exists node ID"),
+ })
+ serviceID := service.Proxysql.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ res, err := client.Default.Agents.AddProxySQLExporter(&agents.AddProxySQLExporterParams{
+ Body: agents.AddProxySQLExporterBody{
+ ServiceID: serviceID,
+ PMMAgentID: "pmm-not-exist-server",
+ Username: "username",
+ Password: "password",
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Agent with ID \"pmm-not-exist-server\" not found.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.ProxysqlExporter.AgentID)
+ }
+ })
+ t.Run("With PushMetrics", func(t *testing.T) {
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ node := pmmapitests.AddRemoteNode(t, pmmapitests.TestString(t, "Remote node for Node exporter"))
+ nodeID := node.Remote.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ service := addProxySQLService(t, services.AddProxySQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 5432,
+ ServiceName: pmmapitests.TestString(t, "ProxySQL Service for ProxySQLExporter test"),
+ })
+ serviceID := service.Proxysql.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, nodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ ProxySQLExporter := addProxySQLExporter(t, agents.AddProxySQLExporterBody{
+ ServiceID: serviceID,
+ Username: "username",
+ Password: "password",
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "custom_label_proxysql_exporter": "proxysql_exporter",
+ },
+
+ SkipConnectionCheck: true,
+ })
+ agentID := ProxySQLExporter.ProxysqlExporter.AgentID
+ defer pmmapitests.RemoveAgents(t, agentID)
+
+ getAgentRes, err := client.Default.Agents.GetAgent(&agents.GetAgentParams{
+ Body: agents.GetAgentBody{AgentID: agentID},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, &agents.GetAgentOK{
+ Payload: &agents.GetAgentOKBody{
+ ProxysqlExporter: &agents.GetAgentOKBodyProxysqlExporter{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "custom_label_proxysql_exporter": "proxysql_exporter",
+ },
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, getAgentRes)
+
+ // Test change API.
+ changeProxySQLExporterOK, err := client.Default.Agents.ChangeProxySQLExporter(&agents.ChangeProxySQLExporterParams{
+ Body: agents.ChangeProxySQLExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeProxySQLExporterParamsBodyCommon{
+ EnablePushMetrics: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeProxySQLExporterOK{
+ Payload: &agents.ChangeProxySQLExporterOKBody{
+ ProxysqlExporter: &agents.ChangeProxySQLExporterOKBodyProxysqlExporter{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "custom_label_proxysql_exporter": "proxysql_exporter",
+ },
+ PushMetricsEnabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, changeProxySQLExporterOK)
+
+ changeProxySQLExporterOK, err = client.Default.Agents.ChangeProxySQLExporter(&agents.ChangeProxySQLExporterParams{
+ Body: agents.ChangeProxySQLExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeProxySQLExporterParamsBodyCommon{
+ DisablePushMetrics: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeProxySQLExporterOK{
+ Payload: &agents.ChangeProxySQLExporterOKBody{
+ ProxysqlExporter: &agents.ChangeProxySQLExporterOKBodyProxysqlExporter{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "custom_label_proxysql_exporter": "proxysql_exporter",
+ },
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, changeProxySQLExporterOK)
+
+ _, err = client.Default.Agents.ChangeProxySQLExporter(&agents.ChangeProxySQLExporterParams{
+ Body: agents.ChangeProxySQLExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeProxySQLExporterParamsBodyCommon{
+ EnablePushMetrics: true,
+ DisablePushMetrics: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "expected one of param: enable_push_metrics or disable_push_metrics")
+ })
+}
diff --git a/api-tests/inventory/agents_rds_exporter_test.go b/api-tests/inventory/agents_rds_exporter_test.go
new file mode 100644
index 0000000000..4ae6b18517
--- /dev/null
+++ b/api-tests/inventory/agents_rds_exporter_test.go
@@ -0,0 +1,320 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package inventory
+
+import (
+ "testing"
+
+ "github.com/percona/pmm/api/inventorypb/json/client"
+ "github.com/percona/pmm/api/inventorypb/json/client/agents"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestRDSExporter(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ node := addRemoteRDSNode(t, pmmapitests.TestString(t, "Remote node for RDS exporter"))
+ nodeID := node.RemoteRDS.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, genericNodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ rdsExporter := addRDSExporter(t, agents.AddRDSExporterBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "custom_label_rds_exporter": "rds_exporter",
+ },
+ SkipConnectionCheck: true,
+ DisableBasicMetrics: true,
+ DisableEnhancedMetrics: true,
+ })
+ agentID := rdsExporter.RDSExporter.AgentID
+ defer pmmapitests.RemoveAgents(t, agentID)
+
+ getAgentRes, err := client.Default.Agents.GetAgent(&agents.GetAgentParams{
+ Body: agents.GetAgentBody{AgentID: agentID},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ assert.Equal(t, &agents.GetAgentOK{
+ Payload: &agents.GetAgentOKBody{
+ RDSExporter: &agents.GetAgentOKBodyRDSExporter{
+ NodeID: nodeID,
+ AgentID: agentID,
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "custom_label_rds_exporter": "rds_exporter",
+ },
+ BasicMetricsDisabled: true,
+ EnhancedMetricsDisabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, getAgentRes)
+
+ // Test change API.
+ changeRDSExporterOK, err := client.Default.Agents.ChangeRDSExporter(&agents.ChangeRDSExporterParams{
+ Body: agents.ChangeRDSExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeRDSExporterParamsBodyCommon{
+ Disable: true,
+ RemoveCustomLabels: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeRDSExporterOK{
+ Payload: &agents.ChangeRDSExporterOKBody{
+ RDSExporter: &agents.ChangeRDSExporterOKBodyRDSExporter{
+ NodeID: nodeID,
+ AgentID: agentID,
+ PMMAgentID: pmmAgentID,
+ Disabled: true,
+ BasicMetricsDisabled: true,
+ EnhancedMetricsDisabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, changeRDSExporterOK)
+
+ changeRDSExporterOK, err = client.Default.Agents.ChangeRDSExporter(&agents.ChangeRDSExporterParams{
+ Body: agents.ChangeRDSExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeRDSExporterParamsBodyCommon{
+ Enable: true,
+ CustomLabels: map[string]string{
+ "new_label": "rds_exporter",
+ },
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeRDSExporterOK{
+ Payload: &agents.ChangeRDSExporterOKBody{
+ RDSExporter: &agents.ChangeRDSExporterOKBodyRDSExporter{
+ NodeID: nodeID,
+ AgentID: agentID,
+ PMMAgentID: pmmAgentID,
+ Disabled: false,
+ CustomLabels: map[string]string{
+ "new_label": "rds_exporter",
+ },
+ BasicMetricsDisabled: true,
+ EnhancedMetricsDisabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, changeRDSExporterOK)
+ })
+
+ t.Run("AddNodeIDEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, genericNodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ res, err := client.Default.Agents.AddRDSExporter(&agents.AddRDSExporterParams{
+ Body: agents.AddRDSExporterBody{
+ NodeID: "",
+ PMMAgentID: pmmAgentID,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field NodeId: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveNodes(t, res.Payload.RDSExporter.AgentID)
+ }
+ })
+
+ t.Run("NotExistNodeID", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, genericNodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ res, err := client.Default.Agents.AddRDSExporter(&agents.AddRDSExporterParams{
+ Body: agents.AddRDSExporterBody{
+ NodeID: "pmm-node-id",
+ PMMAgentID: pmmAgentID,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Node with ID \"pmm-node-id\" not found.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.RDSExporter.AgentID)
+ }
+ })
+
+ t.Run("NotExistPMMAgentID", func(t *testing.T) {
+ t.Parallel()
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ res, err := client.Default.Agents.AddRDSExporter(&agents.AddRDSExporterParams{
+ Body: agents.AddRDSExporterBody{
+ NodeID: "nodeID",
+ PMMAgentID: "pmm-not-exist-server",
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Agent with ID \"pmm-not-exist-server\" not found.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.RDSExporter.AgentID)
+ }
+ })
+
+ t.Run("With PushMetrics", func(t *testing.T) {
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ node := addRemoteRDSNode(t, pmmapitests.TestString(t, "Remote node for RDS exporter"))
+ nodeID := node.RemoteRDS.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, genericNodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ rdsExporter := addRDSExporter(t, agents.AddRDSExporterBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "custom_label_rds_exporter": "rds_exporter",
+ },
+ SkipConnectionCheck: true,
+ DisableBasicMetrics: true,
+ DisableEnhancedMetrics: true,
+ })
+ agentID := rdsExporter.RDSExporter.AgentID
+ defer pmmapitests.RemoveAgents(t, agentID)
+
+ getAgentRes, err := client.Default.Agents.GetAgent(&agents.GetAgentParams{
+ Body: agents.GetAgentBody{AgentID: agentID},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ assert.Equal(t, &agents.GetAgentOK{
+ Payload: &agents.GetAgentOKBody{
+ RDSExporter: &agents.GetAgentOKBodyRDSExporter{
+ NodeID: nodeID,
+ AgentID: agentID,
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "custom_label_rds_exporter": "rds_exporter",
+ },
+ BasicMetricsDisabled: true,
+ EnhancedMetricsDisabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, getAgentRes)
+
+ // Test change API.
+ changeRDSExporterOK, err := client.Default.Agents.ChangeRDSExporter(&agents.ChangeRDSExporterParams{
+ Body: agents.ChangeRDSExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeRDSExporterParamsBodyCommon{
+ EnablePushMetrics: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeRDSExporterOK{
+ Payload: &agents.ChangeRDSExporterOKBody{
+ RDSExporter: &agents.ChangeRDSExporterOKBodyRDSExporter{
+ NodeID: nodeID,
+ AgentID: agentID,
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "custom_label_rds_exporter": "rds_exporter",
+ },
+ BasicMetricsDisabled: true,
+ EnhancedMetricsDisabled: true,
+ PushMetricsEnabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, changeRDSExporterOK)
+
+ changeRDSExporterOK, err = client.Default.Agents.ChangeRDSExporter(&agents.ChangeRDSExporterParams{
+ Body: agents.ChangeRDSExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeRDSExporterParamsBodyCommon{
+ DisablePushMetrics: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeRDSExporterOK{
+ Payload: &agents.ChangeRDSExporterOKBody{
+ RDSExporter: &agents.ChangeRDSExporterOKBodyRDSExporter{
+ NodeID: nodeID,
+ AgentID: agentID,
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "custom_label_rds_exporter": "rds_exporter",
+ },
+ BasicMetricsDisabled: true,
+ EnhancedMetricsDisabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, changeRDSExporterOK)
+ _, err = client.Default.Agents.ChangeRDSExporter(&agents.ChangeRDSExporterParams{
+ Body: agents.ChangeRDSExporterBody{
+ AgentID: agentID,
+ Common: &agents.ChangeRDSExporterParamsBodyCommon{
+ EnablePushMetrics: true,
+ DisablePushMetrics: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "expected one of param: enable_push_metrics or disable_push_metrics")
+ })
+}
diff --git a/api-tests/inventory/agents_test.go b/api-tests/inventory/agents_test.go
new file mode 100644
index 0000000000..0e52912d87
--- /dev/null
+++ b/api-tests/inventory/agents_test.go
@@ -0,0 +1,1197 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package inventory
+
+import (
+ "context"
+ "net/http"
+ "testing"
+
+ "github.com/AlekSi/pointer"
+ "github.com/percona/pmm/api/inventorypb"
+ "github.com/percona/pmm/api/inventorypb/json/client"
+ "github.com/percona/pmm/api/inventorypb/json/client/agents"
+ "github.com/percona/pmm/api/inventorypb/json/client/services"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+// AgentStatusUnknown means agent is not connected and we don't know anything about its status.
+var AgentStatusUnknown = inventorypb.AgentStatus_name[int32(inventorypb.AgentStatus_UNKNOWN)]
+
+func TestAgents(t *testing.T) {
+ t.Run("List", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "Generic node for agents list")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ node := pmmapitests.AddRemoteNode(t, pmmapitests.TestString(t, "Remote node for agents list"))
+ nodeID := node.Remote.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ service := addMySQLService(t, services.AddMySQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 3306,
+ ServiceName: pmmapitests.TestString(t, "MySQL Service for agent"),
+ })
+ serviceID := service.Mysql.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, nodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ mySqldExporter := addMySQLdExporter(t, agents.AddMySQLdExporterBody{
+ ServiceID: serviceID,
+ Username: "username",
+ Password: "password",
+ PMMAgentID: pmmAgentID,
+
+ SkipConnectionCheck: true,
+ })
+ mySqldExporterID := mySqldExporter.MysqldExporter.AgentID
+ defer pmmapitests.RemoveAgents(t, mySqldExporterID)
+
+ res, err := client.Default.Agents.ListAgents(&agents.ListAgentsParams{Context: pmmapitests.Context})
+ require.NoError(t, err)
+ require.NotNil(t, res)
+ require.NotZerof(t, len(res.Payload.MysqldExporter), "There should be at least one service")
+
+ assertMySQLExporterExists(t, res, mySqldExporterID)
+ assertPMMAgentExists(t, res, pmmAgentID)
+ })
+
+ t.Run("FilterList", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "Generic node for agents filters")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ node := pmmapitests.AddRemoteNode(t, pmmapitests.TestString(t, "Remote node for agents filters"))
+ nodeID := node.Remote.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ service := addMySQLService(t, services.AddMySQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 3306,
+ ServiceName: pmmapitests.TestString(t, "MySQL Service for filter test"),
+ })
+ serviceID := service.Mysql.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, nodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ mySqldExporter := addMySQLdExporter(t, agents.AddMySQLdExporterBody{
+ ServiceID: serviceID,
+ Username: "username",
+ Password: "password",
+ PMMAgentID: pmmAgentID,
+
+ SkipConnectionCheck: true,
+ })
+ mySqldExporterID := mySqldExporter.MysqldExporter.AgentID
+ defer pmmapitests.RemoveAgents(t, mySqldExporterID)
+
+ nodeExporter, err := client.Default.Agents.AddNodeExporter(&agents.AddNodeExporterParams{
+ Body: agents.AddNodeExporterBody{
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "custom_label_node_exporter": "node_exporter",
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, nodeExporter)
+ nodeExporterID := nodeExporter.Payload.NodeExporter.AgentID
+ defer pmmapitests.RemoveAgents(t, nodeExporterID)
+
+ // Filter by pmm agent ID.
+ res, err := client.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Body: agents.ListAgentsBody{PMMAgentID: pmmAgentID},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ require.NotNil(t, res)
+ require.NotZerof(t, len(res.Payload.MysqldExporter), "There should be at least one agent")
+ assertMySQLExporterExists(t, res, mySqldExporterID)
+ assertNodeExporterExists(t, res, nodeExporterID)
+ assertPMMAgentNotExists(t, res, pmmAgentID)
+
+ // Filter by node ID.
+ res, err = client.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Body: agents.ListAgentsBody{NodeID: nodeID},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ require.NotNil(t, res)
+ require.NotZerof(t, len(res.Payload.NodeExporter), "There should be at least one node exporter")
+ assertMySQLExporterNotExists(t, res, mySqldExporterID)
+ assertPMMAgentNotExists(t, res, pmmAgentID)
+ assertNodeExporterExists(t, res, nodeExporterID)
+
+ // Filter by service ID.
+ res, err = client.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Body: agents.ListAgentsBody{ServiceID: serviceID},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ require.NotNil(t, res)
+ require.NotZerof(t, len(res.Payload.MysqldExporter), "There should be at least one mysql exporter")
+ assertMySQLExporterExists(t, res, mySqldExporterID)
+ assertPMMAgentNotExists(t, res, pmmAgentID)
+ assertNodeExporterNotExists(t, res, nodeExporterID)
+
+ // Filter by service ID.
+ res, err = client.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Body: agents.ListAgentsBody{AgentType: pointer.ToString(agents.ListAgentsBodyAgentTypeMYSQLDEXPORTER)},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ require.NotNil(t, res)
+ require.NotZerof(t, len(res.Payload.MysqldExporter), "There should be at least one mysql exporter")
+ assertMySQLExporterExists(t, res, mySqldExporterID)
+ assertPMMAgentNotExists(t, res, pmmAgentID)
+ assertNodeExporterNotExists(t, res, nodeExporterID)
+ })
+
+ t.Run("TwoOrMoreFilters", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, genericNodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ res, err := client.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Body: agents.ListAgentsBody{
+ PMMAgentID: pmmAgentID,
+ NodeID: genericNodeID,
+ ServiceID: "some-service-id",
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "expected at most one param: pmm_agent_id, node_id or service_id")
+ assert.Nil(t, res)
+ })
+
+ t.Run("AddWithInvalidType", func(t *testing.T) {
+ t.Parallel()
+
+ nodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, nodeID)
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ pmmAgentID := pmmapitests.AddPMMAgent(t, nodeID).PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ serviceID := addMySQLService(t, services.AddMySQLServiceBody{
+ NodeID: nodeID,
+ Address: "localhost",
+ Port: 3306,
+ ServiceName: pmmapitests.TestString(t, ""),
+ }).Mysql.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ _, err := client.Default.Agents.AddMongoDBExporter(&agents.AddMongoDBExporterParams{
+ Body: agents.AddMongoDBExporterBody{
+ ServiceID: serviceID,
+ Username: "username",
+ Password: "password",
+ PMMAgentID: pmmAgentID,
+ SkipConnectionCheck: true,
+ },
+ Context: pmmapitests.Context,
+ })
+
+ pmmapitests.AssertAPIErrorf(t, err, http.StatusBadRequest, codes.FailedPrecondition, "invalid combination of service type mysql and agent type mongodb_exporter")
+
+ })
+}
+
+func TestPMMAgent(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ t.Parallel()
+
+ node := pmmapitests.AddRemoteNode(t, pmmapitests.TestString(t, "Remote node for PMM-agent"))
+ nodeID := node.Remote.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ res := pmmapitests.AddPMMAgent(t, nodeID)
+ require.Equal(t, nodeID, res.PMMAgent.RunsOnNodeID)
+ agentID := res.PMMAgent.AgentID
+
+ getAgentRes, err := client.Default.Agents.GetAgent(&agents.GetAgentParams{
+ Body: agents.GetAgentBody{AgentID: agentID},
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.GetAgentOK{
+ Payload: &agents.GetAgentOKBody{
+ PMMAgent: &agents.GetAgentOKBodyPMMAgent{
+ AgentID: agentID,
+ RunsOnNodeID: nodeID,
+ },
+ },
+ }, getAgentRes)
+
+ params := &agents.RemoveAgentParams{
+ Body: agents.RemoveAgentBody{
+ AgentID: agentID,
+ },
+ Context: context.Background(),
+ }
+ removeAgentOK, err := client.Default.Agents.RemoveAgent(params)
+ assert.NoError(t, err)
+ assert.NotNil(t, removeAgentOK)
+ })
+
+ t.Run("AddNodeIDEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ res, err := client.Default.Agents.AddPMMAgent(&agents.AddPMMAgentParams{
+ Body: agents.AddPMMAgentBody{RunsOnNodeID: ""},
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field RunsOnNodeId: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveNodes(t, res.Payload.PMMAgent.AgentID)
+ }
+ })
+
+ t.Run("Remove pmm-agent with agents", func(t *testing.T) {
+ t.Parallel()
+
+ node := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "Generic node for PMM-agent"))
+ nodeID := node.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ service := addMySQLService(t, services.AddMySQLServiceBody{
+ NodeID: nodeID,
+ Address: "localhost",
+ Port: 3306,
+ ServiceName: pmmapitests.TestString(t, "MySQL Service for remove pmm-agent test"),
+ })
+ serviceID := service.Mysql.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ pmmAgentOKBody := pmmapitests.AddPMMAgent(t, nodeID)
+ require.Equal(t, nodeID, pmmAgentOKBody.PMMAgent.RunsOnNodeID)
+ pmmAgentID := pmmAgentOKBody.PMMAgent.AgentID
+
+ nodeExporterOK := addNodeExporter(t, pmmAgentID, map[string]string{})
+ nodeExporterID := nodeExporterOK.Payload.NodeExporter.AgentID
+
+ mySqldExporter := addMySQLdExporter(t, agents.AddMySQLdExporterBody{
+ ServiceID: serviceID,
+ Username: "username",
+ Password: "password",
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "custom_label_mysql_exporter": "mysql_exporter",
+ },
+
+ SkipConnectionCheck: true,
+ })
+ mySqldExporterID := mySqldExporter.MysqldExporter.AgentID
+
+ params := &agents.RemoveAgentParams{
+ Body: agents.RemoveAgentBody{
+ AgentID: pmmAgentID,
+ },
+ Context: context.Background(),
+ }
+ res, err := client.Default.Agents.RemoveAgent(params)
+ assert.Nil(t, res)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.FailedPrecondition, `pmm-agent with ID %q has agents.`, pmmAgentID)
+
+ // Check that agents aren't removed.
+ getAgentRes, err := client.Default.Agents.GetAgent(&agents.GetAgentParams{
+ Body: agents.GetAgentBody{AgentID: pmmAgentID},
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.GetAgentOK{
+ Payload: &agents.GetAgentOKBody{
+ PMMAgent: &agents.GetAgentOKBodyPMMAgent{
+ AgentID: pmmAgentID,
+ RunsOnNodeID: nodeID,
+ },
+ },
+ }, getAgentRes)
+
+ listAgentsOK, err := client.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Body: agents.ListAgentsBody{
+ PMMAgentID: pmmAgentID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ListAgentsOKBody{
+ NodeExporter: []*agents.NodeExporterItems0{
+ {
+ PMMAgentID: pmmAgentID,
+ AgentID: nodeExporterID,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ MysqldExporter: []*agents.MysqldExporterItems0{
+ {
+ PMMAgentID: pmmAgentID,
+ AgentID: mySqldExporterID,
+ ServiceID: serviceID,
+ Username: "username",
+ CustomLabels: map[string]string{
+ "custom_label_mysql_exporter": "mysql_exporter",
+ },
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, listAgentsOK.Payload)
+
+ // Remove with force flag.
+ params = &agents.RemoveAgentParams{
+ Body: agents.RemoveAgentBody{
+ AgentID: pmmAgentID,
+ Force: true,
+ },
+ Context: context.Background(),
+ }
+ res, err = client.Default.Agents.RemoveAgent(params)
+ assert.NoError(t, err)
+ assert.NotNil(t, res)
+
+ // Check that agents are removed.
+ getAgentRes, err = client.Default.Agents.GetAgent(&agents.GetAgentParams{
+ Body: agents.GetAgentBody{AgentID: pmmAgentID},
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Agent with ID %q not found.", pmmAgentID)
+ assert.Nil(t, getAgentRes)
+
+ listAgentsOK, err = client.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Body: agents.ListAgentsBody{
+ PMMAgentID: pmmAgentID,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Agent with ID %q not found.", pmmAgentID)
+ assert.Nil(t, listAgentsOK)
+ })
+
+ t.Run("Remove not-exist agent", func(t *testing.T) {
+ t.Parallel()
+
+ agentID := "not-exist-pmm-agent"
+ params := &agents.RemoveAgentParams{
+ Body: agents.RemoveAgentBody{
+ AgentID: agentID,
+ },
+ Context: context.Background(),
+ }
+ res, err := client.Default.Agents.RemoveAgent(params)
+ assert.Nil(t, res)
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, `Agent with ID %q not found.`, agentID)
+ })
+
+ t.Run("Remove with empty params", func(t *testing.T) {
+ t.Parallel()
+
+ removeResp, err := client.Default.Agents.RemoveAgent(&agents.RemoveAgentParams{
+ Body: agents.RemoveAgentBody{},
+ Context: context.Background(),
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field AgentId: value '' must not be an empty string")
+ assert.Nil(t, removeResp)
+ })
+
+ t.Run("Remove pmm-agent on PMM Server", func(t *testing.T) {
+ t.Parallel()
+
+ removeResp, err := client.Default.Agents.RemoveAgent(&agents.RemoveAgentParams{
+ Body: agents.RemoveAgentBody{
+ AgentID: "pmm-server",
+ Force: true,
+ },
+ Context: context.Background(),
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 403, codes.PermissionDenied, "pmm-agent on PMM Server can't be removed.")
+ assert.Nil(t, removeResp)
+ })
+}
+
+func TestQanAgentExporter(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "Test Generic Node for Qan Agent")).NodeID
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ service := addMySQLService(t, services.AddMySQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 3306,
+ ServiceName: pmmapitests.TestString(t, "MySQL Service for QanAgent test"),
+ })
+ serviceID := service.Mysql.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, genericNodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ res, err := client.Default.Agents.AddQANMySQLPerfSchemaAgent(
+ &agents.AddQANMySQLPerfSchemaAgentParams{
+ Body: agents.AddQANMySQLPerfSchemaAgentBody{
+ ServiceID: serviceID,
+ Username: "username",
+ Password: "password",
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "new_label": "QANMysqlPerfschemaAgent",
+ },
+
+ SkipConnectionCheck: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ agentID := res.Payload.QANMysqlPerfschemaAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, agentID)
+
+ getAgentRes, err := client.Default.Agents.GetAgent(&agents.GetAgentParams{
+ Body: agents.GetAgentBody{AgentID: agentID},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, &agents.GetAgentOK{
+ Payload: &agents.GetAgentOKBody{
+ QANMysqlPerfschemaAgent: &agents.GetAgentOKBodyQANMysqlPerfschemaAgent{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "new_label": "QANMysqlPerfschemaAgent",
+ },
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, getAgentRes)
+
+ // Test change API.
+ changeQANMySQLPerfSchemaAgentOK, err := client.Default.Agents.ChangeQANMySQLPerfSchemaAgent(&agents.ChangeQANMySQLPerfSchemaAgentParams{
+ Body: agents.ChangeQANMySQLPerfSchemaAgentBody{
+ AgentID: agentID,
+ Common: &agents.ChangeQANMySQLPerfSchemaAgentParamsBodyCommon{
+ Disable: true,
+ RemoveCustomLabels: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeQANMySQLPerfSchemaAgentOK{
+ Payload: &agents.ChangeQANMySQLPerfSchemaAgentOKBody{
+ QANMysqlPerfschemaAgent: &agents.ChangeQANMySQLPerfSchemaAgentOKBodyQANMysqlPerfschemaAgent{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ Disabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, changeQANMySQLPerfSchemaAgentOK)
+
+ changeQANMySQLPerfSchemaAgentOK, err = client.Default.Agents.ChangeQANMySQLPerfSchemaAgent(&agents.ChangeQANMySQLPerfSchemaAgentParams{
+ Body: agents.ChangeQANMySQLPerfSchemaAgentBody{
+ AgentID: agentID,
+ Common: &agents.ChangeQANMySQLPerfSchemaAgentParamsBodyCommon{
+ Enable: true,
+ CustomLabels: map[string]string{
+ "new_label": "QANMysqlPerfschemaAgent",
+ },
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeQANMySQLPerfSchemaAgentOK{
+ Payload: &agents.ChangeQANMySQLPerfSchemaAgentOKBody{
+ QANMysqlPerfschemaAgent: &agents.ChangeQANMySQLPerfSchemaAgentOKBodyQANMysqlPerfschemaAgent{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ Disabled: false,
+ CustomLabels: map[string]string{
+ "new_label": "QANMysqlPerfschemaAgent",
+ },
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, changeQANMySQLPerfSchemaAgentOK)
+ })
+
+ t.Run("AddServiceIDEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "Test Generic Node for Qan Agent")).NodeID
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, genericNodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ res, err := client.Default.Agents.AddQANMySQLPerfSchemaAgent(&agents.AddQANMySQLPerfSchemaAgentParams{
+ Body: agents.AddQANMySQLPerfSchemaAgentBody{
+ ServiceID: "",
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ Password: "password",
+
+ SkipConnectionCheck: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field ServiceId: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.QANMysqlPerfschemaAgent.AgentID)
+ }
+ })
+
+ t.Run("AddPMMAgentIDEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "Test Generic Node for Qan Agent")).NodeID
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ service := addMySQLService(t, services.AddMySQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 3306,
+ ServiceName: pmmapitests.TestString(t, "MySQL Service for agent"),
+ })
+ serviceID := service.Mysql.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ res, err := client.Default.Agents.AddQANMySQLPerfSchemaAgent(&agents.AddQANMySQLPerfSchemaAgentParams{
+ Body: agents.AddQANMySQLPerfSchemaAgentBody{
+ ServiceID: serviceID,
+ PMMAgentID: "",
+ Username: "username",
+ Password: "password",
+
+ SkipConnectionCheck: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field PmmAgentId: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.QANMysqlPerfschemaAgent.AgentID)
+ }
+ })
+
+ t.Run("NotExistServiceID", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "Test Generic Node for Qan Agent")).NodeID
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, genericNodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ res, err := client.Default.Agents.AddQANMySQLPerfSchemaAgent(&agents.AddQANMySQLPerfSchemaAgentParams{
+ Body: agents.AddQANMySQLPerfSchemaAgentBody{
+ ServiceID: "pmm-service-id",
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ Password: "password",
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Service with ID \"pmm-service-id\" not found.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.QANMysqlPerfschemaAgent.AgentID)
+ }
+ })
+
+ t.Run("NotExistPMMAgentID", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "Test Generic Node for Qan Agent")).NodeID
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ service := addMySQLService(t, services.AddMySQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 3306,
+ ServiceName: pmmapitests.TestString(t, "MySQL Service for not exists node ID"),
+ })
+ serviceID := service.Mysql.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ res, err := client.Default.Agents.AddQANMySQLPerfSchemaAgent(&agents.AddQANMySQLPerfSchemaAgentParams{
+ Body: agents.AddQANMySQLPerfSchemaAgentBody{
+ ServiceID: serviceID,
+ PMMAgentID: "pmm-not-exist-server",
+ Username: "username",
+ Password: "password",
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Agent with ID \"pmm-not-exist-server\" not found.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.QANMysqlPerfschemaAgent.AgentID)
+ }
+ })
+}
+
+func TestPGStatStatementsQanAgent(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "Test Generic Node for Qan PostgreSQL Agent pg_stat_statements")).NodeID
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ service := addPostgreSQLService(t, services.AddPostgreSQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 5432,
+ ServiceName: pmmapitests.TestString(t, "PostgreSQL Service for QanAgent test"),
+ })
+ serviceID := service.Postgresql.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, genericNodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ res, err := client.Default.Agents.AddQANPostgreSQLPgStatementsAgent(
+ &agents.AddQANPostgreSQLPgStatementsAgentParams{
+ Body: agents.AddQANPostgreSQLPgStatementsAgentBody{
+ ServiceID: serviceID,
+ Username: "username",
+ Password: "password",
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "new_label": "QANPostgreSQLPgStatementsAgent",
+ },
+
+ SkipConnectionCheck: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ agentID := res.Payload.QANPostgresqlPgstatementsAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, agentID)
+
+ getAgentRes, err := client.Default.Agents.GetAgent(&agents.GetAgentParams{
+ Body: agents.GetAgentBody{AgentID: agentID},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, &agents.GetAgentOK{
+ Payload: &agents.GetAgentOKBody{
+ QANPostgresqlPgstatementsAgent: &agents.GetAgentOKBodyQANPostgresqlPgstatementsAgent{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "new_label": "QANPostgreSQLPgStatementsAgent",
+ },
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, getAgentRes)
+
+ // Test change API.
+ changeQANPostgreSQLPgStatementsAgentOK, err := client.Default.Agents.ChangeQANPostgreSQLPgStatementsAgent(&agents.ChangeQANPostgreSQLPgStatementsAgentParams{
+ Body: agents.ChangeQANPostgreSQLPgStatementsAgentBody{
+ AgentID: agentID,
+ Common: &agents.ChangeQANPostgreSQLPgStatementsAgentParamsBodyCommon{
+ Disable: true,
+ RemoveCustomLabels: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeQANPostgreSQLPgStatementsAgentOK{
+ Payload: &agents.ChangeQANPostgreSQLPgStatementsAgentOKBody{
+ QANPostgresqlPgstatementsAgent: &agents.ChangeQANPostgreSQLPgStatementsAgentOKBodyQANPostgresqlPgstatementsAgent{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ Disabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, changeQANPostgreSQLPgStatementsAgentOK)
+
+ changeQANPostgreSQLPgStatementsAgentOK, err = client.Default.Agents.ChangeQANPostgreSQLPgStatementsAgent(&agents.ChangeQANPostgreSQLPgStatementsAgentParams{
+ Body: agents.ChangeQANPostgreSQLPgStatementsAgentBody{
+ AgentID: agentID,
+ Common: &agents.ChangeQANPostgreSQLPgStatementsAgentParamsBodyCommon{
+ Enable: true,
+ CustomLabels: map[string]string{
+ "new_label": "QANPostgreSQLPgStatementsAgent",
+ },
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeQANPostgreSQLPgStatementsAgentOK{
+ Payload: &agents.ChangeQANPostgreSQLPgStatementsAgentOKBody{
+ QANPostgresqlPgstatementsAgent: &agents.ChangeQANPostgreSQLPgStatementsAgentOKBodyQANPostgresqlPgstatementsAgent{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ Disabled: false,
+ CustomLabels: map[string]string{
+ "new_label": "QANPostgreSQLPgStatementsAgent",
+ },
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, changeQANPostgreSQLPgStatementsAgentOK)
+ })
+
+ t.Run("AddServiceIDEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "Test Generic Node for Qan Agent")).NodeID
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, genericNodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ res, err := client.Default.Agents.AddQANPostgreSQLPgStatementsAgent(&agents.AddQANPostgreSQLPgStatementsAgentParams{
+ Body: agents.AddQANPostgreSQLPgStatementsAgentBody{
+ ServiceID: "",
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ Password: "password",
+
+ SkipConnectionCheck: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field ServiceId: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.QANPostgresqlPgstatementsAgent.AgentID)
+ }
+ })
+
+ t.Run("AddPMMAgentIDEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "Test Generic Node for Qan Agent")).NodeID
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ service := addPostgreSQLService(t, services.AddPostgreSQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 5432,
+ ServiceName: pmmapitests.TestString(t, "PostgreSQL Service for agent"),
+ })
+ serviceID := service.Postgresql.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ res, err := client.Default.Agents.AddQANPostgreSQLPgStatementsAgent(&agents.AddQANPostgreSQLPgStatementsAgentParams{
+ Body: agents.AddQANPostgreSQLPgStatementsAgentBody{
+ ServiceID: serviceID,
+ PMMAgentID: "",
+ Username: "username",
+ Password: "password",
+
+ SkipConnectionCheck: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field PmmAgentId: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.QANPostgresqlPgstatementsAgent.AgentID)
+ }
+ })
+
+ t.Run("NotExistServiceID", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "Test Generic Node for Qan Agent")).NodeID
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, genericNodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ res, err := client.Default.Agents.AddQANPostgreSQLPgStatementsAgent(&agents.AddQANPostgreSQLPgStatementsAgentParams{
+ Body: agents.AddQANPostgreSQLPgStatementsAgentBody{
+ ServiceID: "pmm-service-id",
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ Password: "password",
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Service with ID \"pmm-service-id\" not found.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.QANPostgresqlPgstatementsAgent.AgentID)
+ }
+ })
+
+ t.Run("NotExistPMMAgentID", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "Test Generic Node for Qan Agent")).NodeID
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ service := addPostgreSQLService(t, services.AddPostgreSQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 5432,
+ ServiceName: pmmapitests.TestString(t, "PostgreSQL Service for not exists node ID"),
+ })
+ serviceID := service.Postgresql.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ res, err := client.Default.Agents.AddQANPostgreSQLPgStatementsAgent(&agents.AddQANPostgreSQLPgStatementsAgentParams{
+ Body: agents.AddQANPostgreSQLPgStatementsAgentBody{
+ ServiceID: serviceID,
+ PMMAgentID: "pmm-not-exist-server",
+ Username: "username",
+ Password: "password",
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Agent with ID \"pmm-not-exist-server\" not found.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.QANPostgresqlPgstatementsAgent.AgentID)
+ }
+ })
+}
+
+func TestPGStatMonitorQanAgent(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "Test Generic Node for Qan PostgreSQL Agent pg_stat_monitor")).NodeID
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ service := addPostgreSQLService(t, services.AddPostgreSQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 5432,
+ ServiceName: pmmapitests.TestString(t, "PostgreSQL Service for QanAgent test"),
+ })
+ serviceID := service.Postgresql.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, genericNodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ res, err := client.Default.Agents.AddQANPostgreSQLPgStatMonitorAgent(
+ &agents.AddQANPostgreSQLPgStatMonitorAgentParams{
+ Body: agents.AddQANPostgreSQLPgStatMonitorAgentBody{
+ ServiceID: serviceID,
+ Username: "username",
+ Password: "password",
+ PMMAgentID: pmmAgentID,
+ CustomLabels: map[string]string{
+ "new_label": "QANPostgreSQLPgStatMonitorAgent",
+ },
+
+ SkipConnectionCheck: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ agentID := res.Payload.QANPostgresqlPgstatmonitorAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, agentID)
+
+ getAgentRes, err := client.Default.Agents.GetAgent(&agents.GetAgentParams{
+ Body: agents.GetAgentBody{AgentID: agentID},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, &agents.GetAgentOK{
+ Payload: &agents.GetAgentOKBody{
+ QANPostgresqlPgstatmonitorAgent: &agents.GetAgentOKBodyQANPostgresqlPgstatmonitorAgent{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ QueryExamplesDisabled: false,
+ CustomLabels: map[string]string{
+ "new_label": "QANPostgreSQLPgStatMonitorAgent",
+ },
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, getAgentRes)
+
+ // Test change API.
+ changeQANPostgreSQLPgStatMonitorAgentOK, err := client.Default.Agents.ChangeQANPostgreSQLPgStatMonitorAgent(&agents.ChangeQANPostgreSQLPgStatMonitorAgentParams{
+ Body: agents.ChangeQANPostgreSQLPgStatMonitorAgentBody{
+ AgentID: agentID,
+ Common: &agents.ChangeQANPostgreSQLPgStatMonitorAgentParamsBodyCommon{
+ Disable: true,
+ RemoveCustomLabels: true,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeQANPostgreSQLPgStatMonitorAgentOK{
+ Payload: &agents.ChangeQANPostgreSQLPgStatMonitorAgentOKBody{
+ QANPostgresqlPgstatmonitorAgent: &agents.ChangeQANPostgreSQLPgStatMonitorAgentOKBodyQANPostgresqlPgstatmonitorAgent{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ Disabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, changeQANPostgreSQLPgStatMonitorAgentOK)
+
+ changeQANPostgreSQLPgStatMonitorAgentOK, err = client.Default.Agents.ChangeQANPostgreSQLPgStatMonitorAgent(&agents.ChangeQANPostgreSQLPgStatMonitorAgentParams{
+ Body: agents.ChangeQANPostgreSQLPgStatMonitorAgentBody{
+ AgentID: agentID,
+ Common: &agents.ChangeQANPostgreSQLPgStatMonitorAgentParamsBodyCommon{
+ Enable: true,
+ CustomLabels: map[string]string{
+ "new_label": "QANPostgreSQLPgStatMonitorAgent",
+ },
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &agents.ChangeQANPostgreSQLPgStatMonitorAgentOK{
+ Payload: &agents.ChangeQANPostgreSQLPgStatMonitorAgentOKBody{
+ QANPostgresqlPgstatmonitorAgent: &agents.ChangeQANPostgreSQLPgStatMonitorAgentOKBodyQANPostgresqlPgstatmonitorAgent{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ Disabled: false,
+ CustomLabels: map[string]string{
+ "new_label": "QANPostgreSQLPgStatMonitorAgent",
+ },
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, changeQANPostgreSQLPgStatMonitorAgentOK)
+ })
+
+ t.Run("BasicWithDisabledExamples", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "Test Generic Node for Qan PostgreSQL Agent pg_stat_monitor")).NodeID
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ service := addPostgreSQLService(t, services.AddPostgreSQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 5432,
+ ServiceName: pmmapitests.TestString(t, "PostgreSQL Service for QanAgent test"),
+ })
+ serviceID := service.Postgresql.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, genericNodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ res, err := client.Default.Agents.AddQANPostgreSQLPgStatMonitorAgent(
+ &agents.AddQANPostgreSQLPgStatMonitorAgentParams{
+ Body: agents.AddQANPostgreSQLPgStatMonitorAgentBody{
+ ServiceID: serviceID,
+ Username: "username",
+ Password: "password",
+ PMMAgentID: pmmAgentID,
+ DisableQueryExamples: true,
+ CustomLabels: map[string]string{
+ "new_label": "QANPostgreSQLPgStatMonitorAgent",
+ },
+
+ SkipConnectionCheck: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ agentID := res.Payload.QANPostgresqlPgstatmonitorAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, agentID)
+
+ getAgentRes, err := client.Default.Agents.GetAgent(&agents.GetAgentParams{
+ Body: agents.GetAgentBody{AgentID: agentID},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, &agents.GetAgentOK{
+ Payload: &agents.GetAgentOKBody{
+ QANPostgresqlPgstatmonitorAgent: &agents.GetAgentOKBodyQANPostgresqlPgstatmonitorAgent{
+ AgentID: agentID,
+ ServiceID: serviceID,
+ Username: "username",
+ PMMAgentID: pmmAgentID,
+ QueryExamplesDisabled: true,
+ CustomLabels: map[string]string{
+ "new_label": "QANPostgreSQLPgStatMonitorAgent",
+ },
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, getAgentRes)
+ })
+
+ t.Run("AddServiceIDEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "Test Generic Node for Qan Agent")).NodeID
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, genericNodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ res, err := client.Default.Agents.AddQANPostgreSQLPgStatMonitorAgent(&agents.AddQANPostgreSQLPgStatMonitorAgentParams{
+ Body: agents.AddQANPostgreSQLPgStatMonitorAgentBody{
+ ServiceID: "",
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ Password: "password",
+
+ SkipConnectionCheck: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field ServiceId: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.QANPostgresqlPgstatmonitorAgent.AgentID)
+ }
+ })
+
+ t.Run("AddPMMAgentIDEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "Test Generic Node for Qan Agent")).NodeID
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ service := addPostgreSQLService(t, services.AddPostgreSQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 5432,
+ ServiceName: pmmapitests.TestString(t, "PostgreSQL Service for agent"),
+ })
+ serviceID := service.Postgresql.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ res, err := client.Default.Agents.AddQANPostgreSQLPgStatMonitorAgent(&agents.AddQANPostgreSQLPgStatMonitorAgentParams{
+ Body: agents.AddQANPostgreSQLPgStatMonitorAgentBody{
+ ServiceID: serviceID,
+ PMMAgentID: "",
+ Username: "username",
+ Password: "password",
+
+ SkipConnectionCheck: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field PmmAgentId: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.QANPostgresqlPgstatmonitorAgent.AgentID)
+ }
+ })
+
+ t.Run("NotExistServiceID", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "Test Generic Node for Qan Agent")).NodeID
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, genericNodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ res, err := client.Default.Agents.AddQANPostgreSQLPgStatMonitorAgent(&agents.AddQANPostgreSQLPgStatMonitorAgentParams{
+ Body: agents.AddQANPostgreSQLPgStatMonitorAgentBody{
+ ServiceID: "pmm-service-id",
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ Password: "password",
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Service with ID \"pmm-service-id\" not found.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.QANPostgresqlPgstatmonitorAgent.AgentID)
+ }
+ })
+
+ t.Run("NotExistPMMAgentID", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "Test Generic Node for Qan Agent")).NodeID
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ service := addPostgreSQLService(t, services.AddPostgreSQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 5432,
+ ServiceName: pmmapitests.TestString(t, "PostgreSQL Service for not exists node ID"),
+ })
+ serviceID := service.Postgresql.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ res, err := client.Default.Agents.AddQANPostgreSQLPgStatMonitorAgent(&agents.AddQANPostgreSQLPgStatMonitorAgentParams{
+ Body: agents.AddQANPostgreSQLPgStatMonitorAgentBody{
+ ServiceID: serviceID,
+ PMMAgentID: "pmm-not-exist-server",
+ Username: "username",
+ Password: "password",
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Agent with ID \"pmm-not-exist-server\" not found.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveAgents(t, res.Payload.QANPostgresqlPgstatmonitorAgent.AgentID)
+ }
+ })
+}
diff --git a/api-tests/inventory/helpers.go b/api-tests/inventory/helpers.go
new file mode 100644
index 0000000000..52225f99ff
--- /dev/null
+++ b/api-tests/inventory/helpers.go
@@ -0,0 +1,402 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package inventory
+
+import (
+ "github.com/percona/pmm/api/inventorypb/json/client"
+ "github.com/percona/pmm/api/inventorypb/json/client/agents"
+ "github.com/percona/pmm/api/inventorypb/json/client/nodes"
+ "github.com/percona/pmm/api/inventorypb/json/client/services"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func addRemoteRDSNode(t pmmapitests.TestingT, nodeName string) *nodes.AddRemoteRDSNodeOKBody {
+ t.Helper()
+
+ params := &nodes.AddRemoteRDSNodeParams{
+ Body: nodes.AddRemoteRDSNodeBody{
+ NodeName: nodeName,
+ Address: "some-address",
+ Region: "region",
+ },
+
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Nodes.AddRemoteRDSNode(params)
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+
+ return res.Payload
+}
+
+func addRDSExporter(t pmmapitests.TestingT, body agents.AddRDSExporterBody) *agents.AddRDSExporterOKBody {
+ t.Helper()
+
+ res, err := client.Default.Agents.AddRDSExporter(&agents.AddRDSExporterParams{
+ Body: body,
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+
+ return res.Payload
+}
+
+func addRemoteAzureDatabaseNode(t pmmapitests.TestingT, nodeName string) *nodes.AddRemoteAzureDatabaseNodeOKBody {
+ t.Helper()
+
+ params := &nodes.AddRemoteAzureDatabaseNodeParams{
+ Body: nodes.AddRemoteAzureDatabaseNodeBody{
+ NodeName: nodeName,
+ Address: "some-address",
+ Region: "region",
+ },
+
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Nodes.AddRemoteAzureDatabaseNode(params)
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+
+ return res.Payload
+}
+
+func addAzureDatabaseExporter(t pmmapitests.TestingT, body agents.AddAzureDatabaseExporterBody) *agents.AddAzureDatabaseExporterOKBody {
+ t.Helper()
+
+ res, err := client.Default.Agents.AddAzureDatabaseExporter(&agents.AddAzureDatabaseExporterParams{
+ Body: body,
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+
+ return res.Payload
+}
+
+func addMySQLService(t pmmapitests.TestingT, body services.AddMySQLServiceBody) *services.AddMySQLServiceOKBody {
+ t.Helper()
+
+ params := &services.AddMySQLServiceParams{
+ Body: body,
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddMySQLService(params)
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ return res.Payload
+}
+
+func addMongoDBService(t pmmapitests.TestingT, body services.AddMongoDBServiceBody) *services.AddMongoDBServiceOKBody {
+ t.Helper()
+
+ params := &services.AddMongoDBServiceParams{
+ Body: body,
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddMongoDBService(params)
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ return res.Payload
+}
+
+func addPostgreSQLService(t pmmapitests.TestingT, body services.AddPostgreSQLServiceBody) *services.AddPostgreSQLServiceOKBody {
+ t.Helper()
+
+ params := &services.AddPostgreSQLServiceParams{
+ Body: body,
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddPostgreSQLService(params)
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ return res.Payload
+}
+
+func addProxySQLService(t pmmapitests.TestingT, body services.AddProxySQLServiceBody) *services.AddProxySQLServiceOKBody {
+ t.Helper()
+
+ params := &services.AddProxySQLServiceParams{
+ Body: body,
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddProxySQLService(params)
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ return res.Payload
+}
+
+func addExternalService(t pmmapitests.TestingT, body services.AddExternalServiceBody) *services.AddExternalServiceOKBody {
+ t.Helper()
+
+ params := &services.AddExternalServiceParams{
+ Body: body,
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddExternalService(params)
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ return res.Payload
+}
+
+func addHAProxyService(t pmmapitests.TestingT, body services.AddHAProxyServiceBody) *services.AddHAProxyServiceOKBody {
+ t.Helper()
+
+ params := &services.AddHAProxyServiceParams{
+ Body: body,
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddHAProxyService(params)
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ return res.Payload
+}
+
+func addNodeExporter(t pmmapitests.TestingT, pmmAgentID string, customLabels map[string]string) *agents.AddNodeExporterOK {
+ res, err := client.Default.Agents.AddNodeExporter(&agents.AddNodeExporterParams{
+ Body: agents.AddNodeExporterBody{
+ PMMAgentID: pmmAgentID,
+ CustomLabels: customLabels,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ require.NotNil(t, res.Payload.NodeExporter)
+ require.Equal(t, pmmAgentID, res.Payload.NodeExporter.PMMAgentID)
+ return res
+}
+
+func addMySQLdExporter(t pmmapitests.TestingT, body agents.AddMySQLdExporterBody) *agents.AddMySQLdExporterOKBody {
+ t.Helper()
+
+ res, err := client.Default.Agents.AddMySQLdExporter(&agents.AddMySQLdExporterParams{
+ Body: body,
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ return res.Payload
+}
+
+func addMongoDBExporter(t pmmapitests.TestingT, body agents.AddMongoDBExporterBody) *agents.AddMongoDBExporterOKBody {
+ t.Helper()
+
+ res, err := client.Default.Agents.AddMongoDBExporter(&agents.AddMongoDBExporterParams{
+ Body: body,
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ return res.Payload
+}
+
+func addPostgresExporter(t pmmapitests.TestingT, body agents.AddPostgresExporterBody) *agents.AddPostgresExporterOKBody {
+ t.Helper()
+
+ res, err := client.Default.Agents.AddPostgresExporter(&agents.AddPostgresExporterParams{
+ Body: body,
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ return res.Payload
+}
+
+func addProxySQLExporter(t pmmapitests.TestingT, body agents.AddProxySQLExporterBody) *agents.AddProxySQLExporterOKBody {
+ t.Helper()
+
+ res, err := client.Default.Agents.AddProxySQLExporter(&agents.AddProxySQLExporterParams{
+ Body: body,
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ return res.Payload
+}
+
+func addExternalExporter(t pmmapitests.TestingT, body agents.AddExternalExporterBody) *agents.AddExternalExporterOKBody {
+ t.Helper()
+
+ res, err := client.Default.Agents.AddExternalExporter(&agents.AddExternalExporterParams{
+ Body: body,
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ return res.Payload
+}
+
+func assertPostgreSQLServiceExists(t pmmapitests.TestingT, res *services.ListServicesOK, serviceID string) bool {
+ t.Helper()
+
+ return assert.Conditionf(t, func() bool {
+ for _, v := range res.Payload.Postgresql {
+ if v.ServiceID == serviceID {
+ return true
+ }
+ }
+ return false
+ }, "There should be PostgreSQL service with id `%s`", serviceID)
+}
+
+func assertMySQLServiceExists(t pmmapitests.TestingT, res *services.ListServicesOK, serviceID string) bool {
+ t.Helper()
+
+ return assert.Conditionf(t, func() bool {
+ for _, v := range res.Payload.Mysql {
+ if v.ServiceID == serviceID {
+ return true
+ }
+ }
+ return false
+ }, "There should be MySQL service with id `%s`", serviceID)
+}
+
+func assertMySQLServiceNotExist(t pmmapitests.TestingT, res *services.ListServicesOK, serviceID string) bool {
+ t.Helper()
+
+ return assert.Conditionf(t, func() bool {
+ for _, v := range res.Payload.Mysql {
+ if v.ServiceID == serviceID {
+ return false
+ }
+ }
+ return true
+ }, "There should not be MySQL service with id `%s`", serviceID)
+}
+
+func assertExternalServiceExists(t pmmapitests.TestingT, res *services.ListServicesOK, serviceID string) bool {
+ t.Helper()
+
+ return assert.Conditionf(t, func() bool {
+ for _, v := range res.Payload.External {
+ if v.ServiceID == serviceID {
+ return true
+ }
+ }
+ return false
+ }, "There should be External service with id `%s`", serviceID)
+}
+
+func assertExternalServiceNotExist(t pmmapitests.TestingT, res *services.ListServicesOK, serviceID string) bool {
+ t.Helper()
+
+ return assert.Conditionf(t, func() bool {
+ for _, v := range res.Payload.External {
+ if v.ServiceID == serviceID {
+ return false
+ }
+ }
+ return true
+ }, "There should not be External service with id `%s`", serviceID)
+}
+
+func assertHAProxyServiceExists(t pmmapitests.TestingT, res *services.ListServicesOK, serviceID string) bool {
+ t.Helper()
+
+ return assert.Conditionf(t, func() bool {
+ for _, v := range res.Payload.Haproxy {
+ if v.ServiceID == serviceID {
+ return true
+ }
+ }
+ return false
+ }, "There should be HAProxy service with id `%s`", serviceID)
+}
+
+func assertHAProxyServiceNotExist(t pmmapitests.TestingT, res *services.ListServicesOK, serviceID string) bool {
+ t.Helper()
+
+ return assert.Conditionf(t, func() bool {
+ for _, v := range res.Payload.Haproxy {
+ if v.ServiceID == serviceID {
+ return false
+ }
+ }
+ return true
+ }, "There should not be HAProxy service with id `%s`", serviceID)
+}
+
+func assertMySQLExporterExists(t pmmapitests.TestingT, res *agents.ListAgentsOK, mySqldExporterID string) bool {
+ return assert.Conditionf(t, func() bool {
+ for _, v := range res.Payload.MysqldExporter {
+ if v.AgentID == mySqldExporterID {
+ return true
+ }
+ }
+ return false
+ }, "There should be MySQL agent with id `%s`", mySqldExporterID)
+}
+
+func assertMySQLExporterNotExists(t pmmapitests.TestingT, res *agents.ListAgentsOK, mySqldExporterID string) bool {
+ return assert.Conditionf(t, func() bool {
+ for _, v := range res.Payload.MysqldExporter {
+ if v.AgentID == mySqldExporterID {
+ return false
+ }
+ }
+ return true
+ }, "There should be MySQL agent with id `%s`", mySqldExporterID)
+}
+
+func assertPMMAgentExists(t pmmapitests.TestingT, res *agents.ListAgentsOK, pmmAgentID string) bool {
+ return assert.Conditionf(t, func() bool {
+ for _, v := range res.Payload.PMMAgent {
+ if v.AgentID == pmmAgentID {
+ return true
+ }
+ }
+ return false
+ }, "There should be PMM-agent with id `%s`", pmmAgentID)
+}
+
+func assertPMMAgentNotExists(t pmmapitests.TestingT, res *agents.ListAgentsOK, pmmAgentID string) bool {
+ return assert.Conditionf(t, func() bool {
+ for _, v := range res.Payload.PMMAgent {
+ if v.AgentID == pmmAgentID {
+ return false
+ }
+ }
+ return true
+ }, "There should be PMM-agent with id `%s`", pmmAgentID)
+}
+
+func assertNodeExporterExists(t pmmapitests.TestingT, res *agents.ListAgentsOK, nodeExporterID string) bool {
+ return assert.Conditionf(t, func() bool {
+ for _, v := range res.Payload.NodeExporter {
+ if v.AgentID == nodeExporterID {
+ return true
+ }
+ }
+ return false
+ }, "There should be Node exporter with id `%s`", nodeExporterID)
+}
+
+func assertNodeExporterNotExists(t pmmapitests.TestingT, res *agents.ListAgentsOK, nodeExporterID string) bool {
+ return assert.Conditionf(t, func() bool {
+ for _, v := range res.Payload.NodeExporter {
+ if v.AgentID == nodeExporterID {
+ return false
+ }
+ }
+ return true
+ }, "There should be Node exporter with id `%s`", nodeExporterID)
+}
diff --git a/api-tests/inventory/nodes_test.go b/api-tests/inventory/nodes_test.go
new file mode 100644
index 0000000000..27f530bb73
--- /dev/null
+++ b/api-tests/inventory/nodes_test.go
@@ -0,0 +1,516 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package inventory
+
+import (
+ "context"
+ "testing"
+
+ "github.com/AlekSi/pointer"
+ "github.com/percona/pmm/api/inventorypb/json/client"
+ "github.com/percona/pmm/api/inventorypb/json/client/agents"
+ "github.com/percona/pmm/api/inventorypb/json/client/nodes"
+ "github.com/percona/pmm/api/inventorypb/json/client/services"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestNodes(t *testing.T) {
+ t.Run("List", func(t *testing.T) {
+ t.Parallel()
+
+ remoteNode := pmmapitests.AddRemoteNode(t, pmmapitests.TestString(t, "Test Remote Node for List"))
+ remoteNodeID := remoteNode.Remote.NodeID
+ defer pmmapitests.RemoveNodes(t, remoteNodeID)
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "Test Generic Node for List")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ res, err := client.Default.Nodes.ListNodes(nil)
+ require.NoError(t, err)
+ require.NotZerof(t, len(res.Payload.Generic), "There should be at least one node")
+ require.Conditionf(t, func() (success bool) {
+ for _, v := range res.Payload.Generic {
+ if v.NodeID == genericNodeID {
+ return true
+ }
+ }
+ return false
+ }, "There should be generic node with id `%s`", genericNodeID)
+ require.NotZerof(t, len(res.Payload.Remote), "There should be at least one node")
+ require.Conditionf(t, func() (success bool) {
+ for _, v := range res.Payload.Remote {
+ if v.NodeID == remoteNodeID {
+ return true
+ }
+ }
+ return false
+ }, "There should be remote node with id `%s`", remoteNodeID)
+
+ res, err = client.Default.Nodes.ListNodes(&nodes.ListNodesParams{
+ Body: nodes.ListNodesBody{
+ NodeType: pointer.ToString(nodes.ListNodesBodyNodeTypeGENERICNODE),
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ require.NotZerof(t, len(res.Payload.Generic), "There should be at least one generic node")
+ require.Conditionf(t, func() (success bool) {
+ for _, v := range res.Payload.Generic {
+ if v.NodeID == genericNodeID {
+ return true
+ }
+ }
+ return false
+ }, "There should be generic node with id `%s`", genericNodeID)
+ require.Conditionf(t, func() (success bool) {
+ for _, v := range res.Payload.Remote {
+ if v.NodeID == remoteNodeID {
+ return false
+ }
+ }
+ return true
+ }, "There shouldn't be remote node with id `%s`", remoteNodeID)
+ })
+}
+
+func TestGetNode(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ t.Parallel()
+
+ nodeName := pmmapitests.TestString(t, "TestGenericNode")
+ nodeID := pmmapitests.AddGenericNode(t, nodeName).NodeID
+ require.NotEmpty(t, nodeID)
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ expectedResponse := nodes.GetNodeOK{
+ Payload: &nodes.GetNodeOKBody{
+ Generic: &nodes.GetNodeOKBodyGeneric{
+ NodeID: nodeID,
+ NodeName: nodeName,
+ Address: "10.10.10.10",
+ },
+ },
+ }
+
+ params := &nodes.GetNodeParams{
+ Body: nodes.GetNodeBody{NodeID: nodeID},
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Nodes.GetNode(params)
+ assert.NoError(t, err)
+ assert.Equal(t, expectedResponse.Payload, res.Payload)
+ })
+
+ t.Run("NotFound", func(t *testing.T) {
+ t.Parallel()
+
+ params := &nodes.GetNodeParams{
+ Body: nodes.GetNodeBody{NodeID: "pmm-not-found"},
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Nodes.GetNode(params)
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Node with ID \"pmm-not-found\" not found.")
+ assert.Nil(t, res)
+ })
+
+ t.Run("EmptyNodeID", func(t *testing.T) {
+ t.Parallel()
+
+ params := &nodes.GetNodeParams{
+ Body: nodes.GetNodeBody{},
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Nodes.GetNode(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field NodeId: value '' must not be an empty string")
+ assert.Nil(t, res)
+ })
+}
+
+func TestGenericNode(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ t.Parallel()
+
+ nodeName := pmmapitests.TestString(t, "Test Generic Node")
+ params := &nodes.AddGenericNodeParams{
+ Body: nodes.AddGenericNodeBody{
+ NodeName: nodeName,
+ Address: "10.10.10.10",
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Nodes.AddGenericNode(params)
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ require.NotNil(t, res.Payload.Generic)
+ nodeID := res.Payload.Generic.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ // Check node exists in DB.
+ getNodeRes, err := client.Default.Nodes.GetNode(&nodes.GetNodeParams{
+ Body: nodes.GetNodeBody{NodeID: nodeID},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ expectedResponse := &nodes.GetNodeOK{
+ Payload: &nodes.GetNodeOKBody{
+ Generic: &nodes.GetNodeOKBodyGeneric{
+ NodeID: res.Payload.Generic.NodeID,
+ NodeName: nodeName,
+ Address: "10.10.10.10",
+ },
+ },
+ }
+ require.Equal(t, expectedResponse, getNodeRes)
+
+ // Check duplicates.
+ res, err = client.Default.Nodes.AddGenericNode(params)
+ pmmapitests.AssertAPIErrorf(t, err, 409, codes.AlreadyExists, "Node with name %q already exists.", nodeName)
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveNodes(t, res.Payload.Generic.NodeID)
+ }
+ })
+
+ t.Run("AddNameEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ params := &nodes.AddGenericNodeParams{
+ Body: nodes.AddGenericNodeBody{NodeName: ""},
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Nodes.AddGenericNode(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field NodeName: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveNodes(t, res.Payload.Generic.NodeID)
+ }
+ })
+}
+
+func TestContainerNode(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ t.Parallel()
+
+ nodeName := pmmapitests.TestString(t, "Test Container Node")
+ params := &nodes.AddContainerNodeParams{
+ Body: nodes.AddContainerNodeBody{
+ NodeName: nodeName,
+ ContainerID: "docker-id",
+ ContainerName: "docker-name",
+ MachineID: "machine-id",
+ Address: "10.10.1.10",
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Nodes.AddContainerNode(params)
+ require.NoError(t, err)
+ require.NotNil(t, res.Payload.Container)
+ nodeID := res.Payload.Container.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ // Check node exists in DB.
+ getNodeRes, err := client.Default.Nodes.GetNode(&nodes.GetNodeParams{
+ Body: nodes.GetNodeBody{NodeID: nodeID},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ expectedResponse := &nodes.GetNodeOK{
+ Payload: &nodes.GetNodeOKBody{
+ Container: &nodes.GetNodeOKBodyContainer{
+ NodeID: res.Payload.Container.NodeID,
+ NodeName: nodeName,
+ ContainerID: "docker-id",
+ ContainerName: "docker-name",
+ MachineID: "machine-id",
+ Address: "10.10.1.10",
+ },
+ },
+ }
+ require.Equal(t, expectedResponse, getNodeRes)
+
+ // Check duplicates.
+ res, err = client.Default.Nodes.AddContainerNode(params)
+ pmmapitests.AssertAPIErrorf(t, err, 409, codes.AlreadyExists, "Node with name %q already exists.", nodeName)
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveNodes(t, res.Payload.Container.NodeID)
+ }
+ })
+
+ t.Run("AddNameEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ params := &nodes.AddContainerNodeParams{
+ Body: nodes.AddContainerNodeBody{NodeName: ""},
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Nodes.AddContainerNode(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field NodeName: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveNodes(t, res.Payload.Container.NodeID)
+ }
+ })
+}
+
+func TestRemoteNode(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ t.Parallel()
+
+ nodeName := pmmapitests.TestString(t, "Test Remote Node")
+ params := &nodes.AddRemoteNodeParams{
+ Body: nodes.AddRemoteNodeBody{
+ NodeName: nodeName,
+ Az: "eu",
+ Region: "us-west",
+ Address: "10.10.10.10",
+ CustomLabels: map[string]string{"foo": "bar"},
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Nodes.AddRemoteNode(params)
+ require.NoError(t, err)
+ require.NotNil(t, res.Payload.Remote)
+ nodeID := res.Payload.Remote.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ // Check node exists in DB.
+ getNodeRes, err := client.Default.Nodes.GetNode(&nodes.GetNodeParams{
+ Body: nodes.GetNodeBody{NodeID: nodeID},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ expectedResponse := &nodes.GetNodeOK{
+ Payload: &nodes.GetNodeOKBody{
+ Remote: &nodes.GetNodeOKBodyRemote{
+ NodeID: res.Payload.Remote.NodeID,
+ NodeName: nodeName,
+ Az: "eu",
+ Region: "us-west",
+ Address: "10.10.10.10",
+ CustomLabels: map[string]string{"foo": "bar"},
+ },
+ },
+ }
+ require.Equal(t, expectedResponse, getNodeRes)
+
+ // Check duplicates.
+ res, err = client.Default.Nodes.AddRemoteNode(params)
+ pmmapitests.AssertAPIErrorf(t, err, 409, codes.AlreadyExists, "Node with name %q already exists.", nodeName)
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveNodes(t, res.Payload.Remote.NodeID)
+ }
+ })
+
+ t.Run("AddNameEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ params := &nodes.AddRemoteNodeParams{
+ Body: nodes.AddRemoteNodeBody{NodeName: ""},
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Nodes.AddRemoteNode(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field NodeName: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveNodes(t, res.Payload.Remote.NodeID)
+ }
+ })
+}
+
+func TestRemoveNode(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ t.Parallel()
+
+ nodeName := pmmapitests.TestString(t, "Generic Node for basic remove test")
+ node := pmmapitests.AddGenericNode(t, nodeName)
+ nodeID := node.NodeID
+
+ removeResp, err := client.Default.Nodes.RemoveNode(&nodes.RemoveNodeParams{
+ Body: nodes.RemoveNodeBody{
+ NodeID: nodeID,
+ },
+ Context: context.Background(),
+ })
+ assert.NoError(t, err)
+ assert.NotNil(t, removeResp)
+ })
+
+ t.Run("With service", func(t *testing.T) {
+ t.Parallel()
+
+ nodeName := pmmapitests.TestString(t, "Generic Node for remove test")
+ node := pmmapitests.AddGenericNode(t, nodeName)
+
+ serviceName := pmmapitests.TestString(t, "MySQL Service for agent")
+ service := addMySQLService(t, services.AddMySQLServiceBody{
+ NodeID: node.NodeID,
+ Address: "localhost",
+ Port: 3306,
+ ServiceName: serviceName,
+ })
+ serviceID := service.Mysql.ServiceID
+
+ removeResp, err := client.Default.Nodes.RemoveNode(&nodes.RemoveNodeParams{
+ Body: nodes.RemoveNodeBody{
+ NodeID: node.NodeID,
+ },
+ Context: context.Background(),
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.FailedPrecondition, `Node with ID %q has services.`, node.NodeID)
+ assert.Nil(t, removeResp)
+
+ // Check that node and service isn't removed.
+ getServiceResp, err := client.Default.Nodes.GetNode(&nodes.GetNodeParams{
+ Body: nodes.GetNodeBody{NodeID: node.NodeID},
+ Context: pmmapitests.Context,
+ })
+ assert.NotNil(t, getServiceResp)
+ assert.NoError(t, err)
+
+ listAgentsOK, err := client.Default.Services.ListServices(&services.ListServicesParams{
+ Body: services.ListServicesBody{
+ NodeID: node.NodeID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &services.ListServicesOKBody{
+ Mysql: []*services.MysqlItems0{
+ {
+ NodeID: node.NodeID,
+ ServiceID: serviceID,
+ Address: "localhost",
+ Port: 3306,
+ ServiceName: serviceName,
+ },
+ },
+ }, listAgentsOK.Payload)
+
+ // Remove with force flag.
+ params := &nodes.RemoveNodeParams{
+ Body: nodes.RemoveNodeBody{
+ NodeID: node.NodeID,
+ Force: true,
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Nodes.RemoveNode(params)
+ assert.NoError(t, err)
+ assert.NotNil(t, res)
+
+ // Check that the node and agents are removed.
+ getServiceResp, err = client.Default.Nodes.GetNode(&nodes.GetNodeParams{
+ Body: nodes.GetNodeBody{NodeID: node.NodeID},
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Node with ID %q not found.", node.NodeID)
+ assert.Nil(t, getServiceResp)
+
+ listAgentsOK, err = client.Default.Services.ListServices(&services.ListServicesParams{
+ Body: services.ListServicesBody{
+ NodeID: node.NodeID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &services.ListServicesOKBody{}, listAgentsOK.Payload)
+ })
+
+ t.Run("With pmm-agent", func(t *testing.T) {
+ t.Parallel()
+
+ nodeName := pmmapitests.TestString(t, "Generic Node for remove test")
+ node := pmmapitests.AddGenericNode(t, nodeName)
+
+ _ = pmmapitests.AddPMMAgent(t, node.NodeID)
+
+ removeResp, err := client.Default.Nodes.RemoveNode(&nodes.RemoveNodeParams{
+ Body: nodes.RemoveNodeBody{
+ NodeID: node.NodeID,
+ },
+ Context: context.Background(),
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.FailedPrecondition, `Node with ID %q has pmm-agent.`, node.NodeID)
+ assert.Nil(t, removeResp)
+
+ // Remove with force flag.
+ params := &nodes.RemoveNodeParams{
+ Body: nodes.RemoveNodeBody{
+ NodeID: node.NodeID,
+ Force: true,
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Nodes.RemoveNode(params)
+ assert.NoError(t, err)
+ assert.NotNil(t, res)
+
+ // Check that the node and agents are removed.
+ getServiceResp, err := client.Default.Nodes.GetNode(&nodes.GetNodeParams{
+ Body: nodes.GetNodeBody{NodeID: node.NodeID},
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Node with ID %q not found.", node.NodeID)
+ assert.Nil(t, getServiceResp)
+
+ listAgentsOK, err := client.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Body: agents.ListAgentsBody{
+ NodeID: node.NodeID,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Node with ID %q not found.", node.NodeID)
+ assert.Nil(t, listAgentsOK)
+ })
+
+ t.Run("Not-exist node", func(t *testing.T) {
+ t.Parallel()
+ nodeID := "not-exist-node-id"
+ removeResp, err := client.Default.Nodes.RemoveNode(&nodes.RemoveNodeParams{
+ Body: nodes.RemoveNodeBody{
+ NodeID: nodeID,
+ },
+ Context: context.Background(),
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, `Node with ID %q not found.`, nodeID)
+ assert.Nil(t, removeResp)
+ })
+
+ t.Run("Empty params", func(t *testing.T) {
+ t.Parallel()
+ removeResp, err := client.Default.Nodes.RemoveNode(&nodes.RemoveNodeParams{
+ Body: nodes.RemoveNodeBody{},
+ Context: context.Background(),
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field NodeId: value '' must not be an empty string")
+ assert.Nil(t, removeResp)
+ })
+
+ t.Run("PMM Server", func(t *testing.T) {
+ t.Parallel()
+
+ removeResp, err := client.Default.Nodes.RemoveNode(&nodes.RemoveNodeParams{
+ Body: nodes.RemoveNodeBody{
+ NodeID: "pmm-server",
+ Force: true,
+ },
+ Context: context.Background(),
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 403, codes.PermissionDenied, "PMM Server node can't be removed.")
+ assert.Nil(t, removeResp)
+ })
+}
diff --git a/api-tests/inventory/services_test.go b/api-tests/inventory/services_test.go
new file mode 100644
index 0000000000..6853d688c9
--- /dev/null
+++ b/api-tests/inventory/services_test.go
@@ -0,0 +1,1332 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package inventory
+
+import (
+ "context"
+ "testing"
+
+ "github.com/AlekSi/pointer"
+ "github.com/percona/pmm/api/inventorypb/json/client"
+ "github.com/percona/pmm/api/inventorypb/json/client/agents"
+ "github.com/percona/pmm/api/inventorypb/json/client/services"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestServices(t *testing.T) {
+ t.Run("List", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ remoteNodeOKBody := pmmapitests.AddRemoteNode(t, pmmapitests.TestString(t, "Remote node for services test"))
+ remoteNodeID := remoteNodeOKBody.Remote.NodeID
+ defer pmmapitests.RemoveNodes(t, remoteNodeID)
+
+ service := addMySQLService(t, services.AddMySQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 3306,
+ ServiceName: pmmapitests.TestString(t, "Some MySQL Service"),
+ })
+ serviceID := service.Mysql.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ remoteService := addMySQLService(t, services.AddMySQLServiceBody{
+ NodeID: remoteNodeID,
+ Address: "localhost",
+ Port: 3306,
+ ServiceName: pmmapitests.TestString(t, "Some MySQL Service on remote Node"),
+ })
+ remoteServiceID := remoteService.Mysql.ServiceID
+ defer pmmapitests.RemoveServices(t, remoteServiceID)
+
+ postgreSQLService := addPostgreSQLService(t, services.AddPostgreSQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 5432,
+ ServiceName: pmmapitests.TestString(t, "Some MySQL Service on remote Node"),
+ })
+ postgreSQLServiceID := postgreSQLService.Postgresql.ServiceID
+ defer pmmapitests.RemoveServices(t, postgreSQLServiceID)
+
+ externalService := addExternalService(t, services.AddExternalServiceBody{
+ NodeID: genericNodeID,
+ ServiceName: pmmapitests.TestString(t, "Some External Service on remote Node"),
+ Group: "rabbitmq",
+ })
+ externalServiceID := externalService.External.ServiceID
+ defer pmmapitests.RemoveServices(t, externalServiceID)
+
+ haProxyService := addHAProxyService(t, services.AddHAProxyServiceBody{
+ NodeID: genericNodeID,
+ ServiceName: pmmapitests.TestString(t, "Some External Service on remote Node"),
+ })
+ haProxyServiceID := haProxyService.Haproxy.ServiceID
+ defer pmmapitests.RemoveServices(t, haProxyServiceID)
+
+ res, err := client.Default.Services.ListServices(&services.ListServicesParams{Context: pmmapitests.Context})
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ assert.NotZerof(t, len(res.Payload.Mysql), "There should be at least one MySQL service")
+ assert.NotZerof(t, len(res.Payload.Postgresql), "There should be at least one PostgreSQL service")
+ assertMySQLServiceExists(t, res, serviceID)
+ assertMySQLServiceExists(t, res, remoteServiceID)
+ assertPostgreSQLServiceExists(t, res, postgreSQLServiceID)
+ assertExternalServiceExists(t, res, externalServiceID)
+ assertHAProxyServiceExists(t, res, haProxyServiceID)
+
+ // Filter by node ID.
+ res, err = client.Default.Services.ListServices(&services.ListServicesParams{
+ Body: services.ListServicesBody{
+ NodeID: genericNodeID,
+ ServiceType: nil,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ assert.NotZerof(t, len(res.Payload.Mysql), "There should be at least one MySQL service")
+ assert.NotZerof(t, len(res.Payload.Postgresql), "There should be at least one PostgreSQL service")
+ assertMySQLServiceExists(t, res, serviceID)
+ assertMySQLServiceNotExist(t, res, remoteServiceID)
+ assertPostgreSQLServiceExists(t, res, postgreSQLServiceID)
+ assertExternalServiceExists(t, res, externalServiceID)
+ assertHAProxyServiceExists(t, res, haProxyServiceID)
+
+ // Filter by service type.
+ res, err = client.Default.Services.ListServices(&services.ListServicesParams{
+ Body: services.ListServicesBody{
+ ServiceType: pointer.ToString(services.ListServicesBodyServiceTypePOSTGRESQLSERVICE),
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ assert.NotZerof(t, len(res.Payload.Postgresql), "There should be at least one PostgreSQL service")
+ assertMySQLServiceNotExist(t, res, serviceID)
+ assertMySQLServiceNotExist(t, res, remoteServiceID)
+ assertExternalServiceNotExist(t, res, externalServiceID)
+ assertHAProxyServiceNotExist(t, res, haProxyServiceID)
+ assertPostgreSQLServiceExists(t, res, postgreSQLServiceID)
+ })
+
+ t.Run("FilterList", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ remoteNodeOKBody := pmmapitests.AddRemoteNode(t, pmmapitests.TestString(t, "Remote node to check services filter"))
+ remoteNodeID := remoteNodeOKBody.Remote.NodeID
+ defer pmmapitests.RemoveNodes(t, remoteNodeID)
+
+ service := addMySQLService(t, services.AddMySQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 3306,
+ ServiceName: pmmapitests.TestString(t, "Some MySQL Service for filters test"),
+ })
+ serviceID := service.Mysql.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ remoteService := addMySQLService(t, services.AddMySQLServiceBody{
+ NodeID: remoteNodeID,
+ Address: "localhost",
+ Port: 3306,
+ ServiceName: pmmapitests.TestString(t, "Some MySQL Service on remote Node for filters test"),
+ })
+ remoteServiceID := remoteService.Mysql.ServiceID
+ defer pmmapitests.RemoveServices(t, remoteServiceID)
+
+ res, err := client.Default.Services.ListServices(&services.ListServicesParams{
+ Body: services.ListServicesBody{NodeID: remoteNodeID},
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ assert.NotZerof(t, len(res.Payload.Mysql), "There should be at least one node")
+ assertMySQLServiceNotExist(t, res, serviceID)
+ assertMySQLServiceExists(t, res, remoteServiceID)
+ })
+}
+
+func TestGetService(t *testing.T) {
+ t.Run("NotFound", func(t *testing.T) {
+ t.Parallel()
+
+ params := &services.GetServiceParams{
+ Body: services.GetServiceBody{ServiceID: "pmm-not-found"},
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.GetService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Service with ID \"pmm-not-found\" not found.")
+ assert.Nil(t, res)
+ })
+
+ t.Run("EmptyServiceID", func(t *testing.T) {
+ t.Parallel()
+
+ params := &services.GetServiceParams{
+ Body: services.GetServiceBody{ServiceID: ""},
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.GetService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field ServiceId: value '' must not be an empty string")
+ assert.Nil(t, res)
+ })
+}
+
+func TestRemoveService(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ t.Parallel()
+
+ node := pmmapitests.AddRemoteNode(t, pmmapitests.TestString(t, "Remote node for agents list"))
+ nodeID := node.Remote.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ service := addMySQLService(t, services.AddMySQLServiceBody{
+ NodeID: nodeID,
+ Address: "localhost",
+ Port: 3306,
+ ServiceName: pmmapitests.TestString(t, "MySQL Service for agent"),
+ })
+ serviceID := service.Mysql.ServiceID
+
+ params := &services.RemoveServiceParams{
+ Body: services.RemoveServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.RemoveService(params)
+ assert.NoError(t, err)
+ assert.NotNil(t, res)
+ })
+
+ t.Run("Has agents", func(t *testing.T) {
+ t.Parallel()
+
+ node := pmmapitests.AddRemoteNode(t, pmmapitests.TestString(t, "Remote node for agents list"))
+ nodeID := node.Remote.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ service := addMySQLService(t, services.AddMySQLServiceBody{
+ NodeID: nodeID,
+ Address: "localhost",
+ Port: 3306,
+ ServiceName: pmmapitests.TestString(t, "MySQL Service for agent"),
+ })
+ serviceID := service.Mysql.ServiceID
+
+ pmmAgent := pmmapitests.AddPMMAgent(t, nodeID)
+ pmmAgentID := pmmAgent.PMMAgent.AgentID
+ defer pmmapitests.RemoveAgents(t, pmmAgentID)
+
+ _ = addMySQLdExporter(t, agents.AddMySQLdExporterBody{
+ ServiceID: serviceID,
+ Username: "username",
+ Password: "password",
+ PMMAgentID: pmmAgentID,
+
+ SkipConnectionCheck: true,
+ })
+
+ params := &services.RemoveServiceParams{
+ Body: services.RemoveServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.RemoveService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.FailedPrecondition, `Service with ID %q has agents.`, serviceID)
+ assert.Nil(t, res)
+
+ // Remove with force flag.
+ params = &services.RemoveServiceParams{
+ Body: services.RemoveServiceBody{
+ ServiceID: serviceID,
+ Force: true,
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err = client.Default.Services.RemoveService(params)
+ assert.NoError(t, err)
+ assert.NotNil(t, res)
+
+ // Check that the service and agents are removed.
+ getServiceResp, err := client.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{ServiceID: serviceID},
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Service with ID %q not found.", serviceID)
+ assert.Nil(t, getServiceResp)
+
+ listAgentsOK, err := client.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Service with ID %q not found.", serviceID)
+ assert.Nil(t, listAgentsOK)
+ })
+
+ t.Run("Not-exist service", func(t *testing.T) {
+ t.Parallel()
+ serviceID := "not-exist-service-id"
+
+ params := &services.RemoveServiceParams{
+ Body: services.RemoveServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.RemoveService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, `Service with ID %q not found.`, serviceID)
+ assert.Nil(t, res)
+ })
+
+ t.Run("Empty params", func(t *testing.T) {
+ t.Parallel()
+ removeResp, err := client.Default.Services.RemoveService(&services.RemoveServiceParams{
+ Body: services.RemoveServiceBody{},
+ Context: context.Background(),
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field ServiceId: value '' must not be an empty string")
+ assert.Nil(t, removeResp)
+ })
+}
+
+func TestMySQLService(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ serviceName := pmmapitests.TestString(t, "Basic MySQL Service")
+ params := &services.AddMySQLServiceParams{
+ Body: services.AddMySQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 3306,
+ ServiceName: serviceName,
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddMySQLService(params)
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ serviceID := res.Payload.Mysql.ServiceID
+ assert.Equal(t, &services.AddMySQLServiceOK{
+ Payload: &services.AddMySQLServiceOKBody{
+ Mysql: &services.AddMySQLServiceOKBodyMysql{
+ ServiceID: serviceID,
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 3306,
+ ServiceName: serviceName,
+ },
+ },
+ }, res)
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ // Check if the service saved in pmm-managed.
+ serviceRes, err := client.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{ServiceID: serviceID},
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.NotNil(t, serviceRes)
+ assert.Equal(t, &services.GetServiceOK{
+ Payload: &services.GetServiceOKBody{
+ Mysql: &services.GetServiceOKBodyMysql{
+ ServiceID: serviceID,
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 3306,
+ ServiceName: serviceName,
+ },
+ },
+ }, serviceRes)
+
+ // Check duplicates.
+ params = &services.AddMySQLServiceParams{
+ Body: services.AddMySQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "127.0.0.1",
+ Port: 3336,
+ ServiceName: serviceName,
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err = client.Default.Services.AddMySQLService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 409, codes.AlreadyExists, "Service with name %q already exists.", serviceName)
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveServices(t, res.Payload.Mysql.ServiceID)
+ }
+ })
+
+ t.Run("AddNodeIDEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ params := &services.AddMySQLServiceParams{
+ Body: services.AddMySQLServiceBody{
+ NodeID: "",
+ Address: "localhost",
+ Port: 3306,
+ ServiceName: pmmapitests.TestString(t, "MySQL Service with empty node id"),
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddMySQLService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field NodeId: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveServices(t, res.Payload.Mysql.ServiceID)
+ }
+ })
+
+ t.Run("AddEmptyPort", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ params := &services.AddMySQLServiceParams{
+ Body: services.AddMySQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ ServiceName: pmmapitests.TestString(t, "MySQL Service with empty node id"),
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddMySQLService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Port are expected to be passed with address.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveServices(t, res.Payload.Mysql.ServiceID)
+ }
+ })
+
+ t.Run("AddAddressSocketConflict", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ params := &services.AddMySQLServiceParams{
+ Body: services.AddMySQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 3306,
+ Socket: "/var/run/mysqld/mysqld.sock",
+ ServiceName: pmmapitests.TestString(t, "MySQL Service with address and socket conflict"),
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddMySQLService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Socket and address cannot be specified together.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveServices(t, res.Payload.Mysql.ServiceID)
+ }
+ })
+
+ t.Run("AddPortWithNoAddress", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ params := &services.AddMySQLServiceParams{
+ Body: services.AddMySQLServiceBody{
+ NodeID: genericNodeID,
+ ServiceName: pmmapitests.TestString(t, "MySQL Service with port and socket"),
+ Port: 3306,
+ Socket: "/var/run/mysqld/mysqld.sock",
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddMySQLService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Socket and port cannot be specified together.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveServices(t, res.Payload.Mysql.ServiceID)
+ }
+ })
+
+ t.Run("AddEpmtyAddressAndSocket", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ params := &services.AddMySQLServiceParams{
+ Body: services.AddMySQLServiceBody{
+ NodeID: genericNodeID,
+ ServiceName: pmmapitests.TestString(t, "MySQL Service with empty address and socket"),
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddMySQLService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Neither socket nor address passed.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveServices(t, res.Payload.Mysql.ServiceID)
+ }
+ })
+
+ t.Run("AddServiceNameEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ params := &services.AddMySQLServiceParams{
+ Body: services.AddMySQLServiceBody{
+ NodeID: genericNodeID,
+ ServiceName: "",
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddMySQLService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field ServiceName: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveServices(t, res.Payload.Mysql.ServiceID)
+ }
+ })
+}
+
+func TestMongoDBService(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ serviceName := pmmapitests.TestString(t, "Basic Mongo Service")
+ params := &services.AddMongoDBServiceParams{
+ Body: services.AddMongoDBServiceBody{
+ NodeID: genericNodeID,
+ ServiceName: serviceName,
+ Address: "localhost",
+ Port: 27017,
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddMongoDBService(params)
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ serviceID := res.Payload.Mongodb.ServiceID
+ assert.Equal(t, &services.AddMongoDBServiceOK{
+ Payload: &services.AddMongoDBServiceOKBody{
+ Mongodb: &services.AddMongoDBServiceOKBodyMongodb{
+ ServiceID: serviceID,
+ NodeID: genericNodeID,
+ ServiceName: serviceName,
+ Address: "localhost",
+ Port: 27017,
+ },
+ },
+ }, res)
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ // Check if the service saved in pmm-managed.
+ serviceRes, err := client.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{ServiceID: serviceID},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ require.NotNil(t, serviceRes)
+ assert.Equal(t, &services.GetServiceOK{
+ Payload: &services.GetServiceOKBody{
+ Mongodb: &services.GetServiceOKBodyMongodb{
+ ServiceID: serviceID,
+ NodeID: genericNodeID,
+ ServiceName: serviceName,
+ Address: "localhost",
+ Port: 27017,
+ },
+ },
+ }, serviceRes)
+
+ // Check duplicates.
+ params = &services.AddMongoDBServiceParams{
+ Body: services.AddMongoDBServiceBody{
+ NodeID: genericNodeID,
+ ServiceName: serviceName,
+ Address: "localhost",
+ Port: 27017,
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err = client.Default.Services.AddMongoDBService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 409, codes.AlreadyExists, "Service with name %q already exists.", serviceName)
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveServices(t, res.Payload.Mongodb.ServiceID)
+ }
+ })
+
+ t.Run("AddNodeIDEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ params := &services.AddMongoDBServiceParams{
+ Body: services.AddMongoDBServiceBody{
+ NodeID: "",
+ ServiceName: pmmapitests.TestString(t, "MongoDB Service with empty node id"),
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddMongoDBService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field NodeId: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveServices(t, res.Payload.Mongodb.ServiceID)
+ }
+ })
+
+ t.Run("AddServiceNameEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ params := &services.AddMongoDBServiceParams{
+ Body: services.AddMongoDBServiceBody{
+ NodeID: genericNodeID,
+ ServiceName: "",
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddMongoDBService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field ServiceName: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveServices(t, res.Payload.Mongodb.ServiceID)
+ }
+ })
+
+ t.Run("AddAddressSocketConflict", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ params := &services.AddMongoDBServiceParams{
+ Body: services.AddMongoDBServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 27017,
+ Socket: "/tmp/mongodb-27017.sock",
+ ServiceName: pmmapitests.TestString(t, "MongoDB Service with address and socket conflict"),
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddMongoDBService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Socket and address cannot be specified together.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveServices(t, res.Payload.Mongodb.ServiceID)
+ }
+ })
+
+ t.Run("AddPortWithNoAddress", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ params := &services.AddMongoDBServiceParams{
+ Body: services.AddMongoDBServiceBody{
+ NodeID: genericNodeID,
+ ServiceName: pmmapitests.TestString(t, "MongoDB Service with port and socket"),
+ Port: 27017,
+ Socket: "/tmp/mongodb-27017.sock",
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddMongoDBService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Socket and port cannot be specified together.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveServices(t, res.Payload.Mongodb.ServiceID)
+ }
+ })
+
+ t.Run("AddEpmtyAddressAndSocket", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ params := &services.AddMongoDBServiceParams{
+ Body: services.AddMongoDBServiceBody{
+ NodeID: genericNodeID,
+ ServiceName: pmmapitests.TestString(t, "MongoDB Service with empty address and socket"),
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddMongoDBService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Neither socket nor address passed.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveServices(t, res.Payload.Mongodb.ServiceID)
+ }
+ })
+
+ t.Run("Socket", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+ require.NotEmpty(t, genericNodeID)
+
+ serviceName := pmmapitests.TestString(t, "Mongo with Socket Service")
+ params := &services.AddMongoDBServiceParams{
+ Body: services.AddMongoDBServiceBody{
+ NodeID: genericNodeID,
+ ServiceName: serviceName,
+ Socket: "/tmp/mongodb-27017.sock",
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddMongoDBService(params)
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ serviceID := res.Payload.Mongodb.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+ assert.Equal(t, &services.AddMongoDBServiceOK{
+ Payload: &services.AddMongoDBServiceOKBody{
+ Mongodb: &services.AddMongoDBServiceOKBodyMongodb{
+ ServiceID: serviceID,
+ NodeID: genericNodeID,
+ ServiceName: serviceName,
+ Socket: "/tmp/mongodb-27017.sock",
+ },
+ },
+ }, res)
+ })
+}
+
+func TestPostgreSQLService(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ serviceName := pmmapitests.TestString(t, "Basic PostgreSQL Service")
+ params := &services.AddPostgreSQLServiceParams{
+ Body: services.AddPostgreSQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 5432,
+ ServiceName: serviceName,
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddPostgreSQLService(params)
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ serviceID := res.Payload.Postgresql.ServiceID
+ assert.Equal(t, &services.AddPostgreSQLServiceOK{
+ Payload: &services.AddPostgreSQLServiceOKBody{
+ Postgresql: &services.AddPostgreSQLServiceOKBodyPostgresql{
+ ServiceID: serviceID,
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 5432,
+ ServiceName: serviceName,
+ },
+ },
+ }, res)
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ // Check if the service saved in pmm-managed.
+ serviceRes, err := client.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{ServiceID: serviceID},
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.NotNil(t, serviceRes)
+ assert.Equal(t, &services.GetServiceOK{
+ Payload: &services.GetServiceOKBody{
+ Postgresql: &services.GetServiceOKBodyPostgresql{
+ ServiceID: serviceID,
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 5432,
+ ServiceName: serviceName,
+ },
+ },
+ }, serviceRes)
+
+ // Check duplicates.
+ params = &services.AddPostgreSQLServiceParams{
+ Body: services.AddPostgreSQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "127.0.0.1",
+ Port: 3336,
+ ServiceName: serviceName,
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err = client.Default.Services.AddPostgreSQLService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 409, codes.AlreadyExists, "Service with name %q already exists.", serviceName)
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveServices(t, res.Payload.Postgresql.ServiceID)
+ }
+ })
+
+ t.Run("AddNodeIDEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ params := &services.AddPostgreSQLServiceParams{
+ Body: services.AddPostgreSQLServiceBody{
+ NodeID: "",
+ Address: "localhost",
+ Port: 5432,
+ ServiceName: pmmapitests.TestString(t, "PostgreSQL Service with empty node id"),
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddPostgreSQLService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field NodeId: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveServices(t, res.Payload.Postgresql.ServiceID)
+ }
+ })
+
+ t.Run("AddEmptyPort", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ params := &services.AddPostgreSQLServiceParams{
+ Body: services.AddPostgreSQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ ServiceName: pmmapitests.TestString(t, "PostgreSQL Service with empty node id"),
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddPostgreSQLService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Port are expected to be passed with address.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveServices(t, res.Payload.Postgresql.ServiceID)
+ }
+ })
+
+ t.Run("AddServiceNameEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ params := &services.AddPostgreSQLServiceParams{
+ Body: services.AddPostgreSQLServiceBody{
+ NodeID: genericNodeID,
+ ServiceName: "",
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddPostgreSQLService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field ServiceName: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveServices(t, res.Payload.Postgresql.ServiceID)
+ }
+ })
+
+ t.Run("AddAddressSocketConflict", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+ params := &services.AddPostgreSQLServiceParams{
+ Body: services.AddPostgreSQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 5432,
+ Socket: "/var/run/postgresql",
+ ServiceName: pmmapitests.TestString(t, "PostgreSQL Service with address and socket conflict"),
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddPostgreSQLService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Socket and address cannot be specified together.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveServices(t, res.Payload.Postgresql.ServiceID)
+ }
+ })
+
+ t.Run("AddPortWithNoAddress", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ params := &services.AddPostgreSQLServiceParams{
+ Body: services.AddPostgreSQLServiceBody{
+ NodeID: genericNodeID,
+ ServiceName: pmmapitests.TestString(t, "PostgreSQL Service with port and socket"),
+ Port: 5432,
+ Socket: "/var/run/postgresql",
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddPostgreSQLService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Socket and port cannot be specified together.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveServices(t, res.Payload.Postgresql.ServiceID)
+ }
+ })
+
+ t.Run("AddEmptyAddressAndSocket", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ params := &services.AddPostgreSQLServiceParams{
+ Body: services.AddPostgreSQLServiceBody{
+ NodeID: genericNodeID,
+ ServiceName: pmmapitests.TestString(t, "PostgreSQL Service with empty address and socket"),
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddPostgreSQLService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Neither socket nor address passed.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveServices(t, res.Payload.Postgresql.ServiceID)
+ }
+ })
+}
+
+func TestProxySQLService(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ serviceName := pmmapitests.TestString(t, "Basic ProxySQL Service")
+ params := &services.AddProxySQLServiceParams{
+ Body: services.AddProxySQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 5432,
+ ServiceName: serviceName,
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddProxySQLService(params)
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ serviceID := res.Payload.Proxysql.ServiceID
+ assert.Equal(t, &services.AddProxySQLServiceOK{
+ Payload: &services.AddProxySQLServiceOKBody{
+ Proxysql: &services.AddProxySQLServiceOKBodyProxysql{
+ ServiceID: serviceID,
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 5432,
+ ServiceName: serviceName,
+ },
+ },
+ }, res)
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ // Check if the service saved in pmm-managed.
+ serviceRes, err := client.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{ServiceID: serviceID},
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.NotNil(t, serviceRes)
+ assert.Equal(t, &services.GetServiceOK{
+ Payload: &services.GetServiceOKBody{
+ Proxysql: &services.GetServiceOKBodyProxysql{
+ ServiceID: serviceID,
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 5432,
+ ServiceName: serviceName,
+ },
+ },
+ }, serviceRes)
+
+ // Check duplicates.
+ params = &services.AddProxySQLServiceParams{
+ Body: services.AddProxySQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "127.0.0.1",
+ Port: 3336,
+ ServiceName: serviceName,
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err = client.Default.Services.AddProxySQLService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 409, codes.AlreadyExists, "Service with name %q already exists.", serviceName)
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveServices(t, res.Payload.Proxysql.ServiceID)
+ }
+ })
+
+ t.Run("AddNodeIDEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ params := &services.AddProxySQLServiceParams{
+ Body: services.AddProxySQLServiceBody{
+ NodeID: "",
+ Address: "localhost",
+ Port: 5432,
+ ServiceName: pmmapitests.TestString(t, "ProxySQL Service with empty node id"),
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddProxySQLService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field NodeId: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveServices(t, res.Payload.Proxysql.ServiceID)
+ }
+ })
+
+ t.Run("AddEmptyPort", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ params := &services.AddProxySQLServiceParams{
+ Body: services.AddProxySQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ ServiceName: pmmapitests.TestString(t, "ProxySQL Service with empty node id"),
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddProxySQLService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Port are expected to be passed with address.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveServices(t, res.Payload.Proxysql.ServiceID)
+ }
+ })
+
+ t.Run("AddAddressSocketConflict", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ params := &services.AddProxySQLServiceParams{
+ Body: services.AddProxySQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 6032,
+ Socket: "/tmp/proxysql_admin.sock",
+ ServiceName: pmmapitests.TestString(t, "ProxySQL Service with address and socket conflict"),
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddProxySQLService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Socket and address cannot be specified together.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveServices(t, res.Payload.Proxysql.ServiceID)
+ }
+ })
+
+ t.Run("AddPortWithNoAddress", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ params := &services.AddProxySQLServiceParams{
+ Body: services.AddProxySQLServiceBody{
+ NodeID: genericNodeID,
+ ServiceName: pmmapitests.TestString(t, "ProxySQL Service with port and socket"),
+ Port: 6032,
+ Socket: "/tmp/proxysql_admin.sock",
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddProxySQLService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Socket and port cannot be specified together.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveServices(t, res.Payload.Proxysql.ServiceID)
+ }
+ })
+
+ t.Run("AddEpmtyAddressAndSocket", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ params := &services.AddProxySQLServiceParams{
+ Body: services.AddProxySQLServiceBody{
+ NodeID: genericNodeID,
+ ServiceName: pmmapitests.TestString(t, "ProxySQL Service with empty address and socket"),
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddProxySQLService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Neither socket nor address passed.")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveServices(t, res.Payload.Proxysql.ServiceID)
+ }
+ })
+
+ t.Run("AddServiceNameEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ params := &services.AddProxySQLServiceParams{
+ Body: services.AddProxySQLServiceBody{
+ NodeID: genericNodeID,
+ ServiceName: "",
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddProxySQLService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field ServiceName: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveServices(t, res.Payload.Proxysql.ServiceID)
+ }
+ })
+}
+
+func TestExternalService(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ t.Parallel()
+
+ containsExternalWithGroup := func(items []*services.ExternalItems0, expectedGroup string) func() bool {
+ return func() bool {
+ for _, ext := range items {
+ if ext.Group == expectedGroup {
+ return true
+ }
+ }
+ return false
+ }
+ }
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ serviceName := pmmapitests.TestString(t, "Basic External Service")
+ params := &services.AddExternalServiceParams{
+ Body: services.AddExternalServiceBody{
+ NodeID: genericNodeID,
+ ServiceName: serviceName,
+ Group: "redis",
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddExternalService(params)
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ serviceID := res.Payload.External.ServiceID
+ assert.Equal(t, &services.AddExternalServiceOK{
+ Payload: &services.AddExternalServiceOKBody{
+ External: &services.AddExternalServiceOKBodyExternal{
+ ServiceID: serviceID,
+ NodeID: genericNodeID,
+ ServiceName: serviceName,
+ Group: "redis",
+ },
+ },
+ }, res)
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ // Check if the service saved in pmm-managed.
+ serviceRes, err := client.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{ServiceID: serviceID},
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.NotNil(t, serviceRes)
+ assert.Equal(t, &services.GetServiceOK{
+ Payload: &services.GetServiceOKBody{
+ External: &services.GetServiceOKBodyExternal{
+ ServiceID: serviceID,
+ NodeID: genericNodeID,
+ ServiceName: serviceName,
+ Group: "redis",
+ },
+ },
+ }, serviceRes)
+
+ // Filter services by external group.
+ servicesList, err := client.Default.Services.ListServices(&services.ListServicesParams{
+ Body: services.ListServicesBody{
+ ExternalGroup: "redis",
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.NotNil(t, servicesList)
+ assert.Len(t, servicesList.Payload.Mysql, 0)
+ assert.Len(t, servicesList.Payload.Mongodb, 0)
+ assert.Len(t, servicesList.Payload.Postgresql, 0)
+ assert.Len(t, servicesList.Payload.Proxysql, 0)
+ assert.Len(t, servicesList.Payload.External, 1)
+ assert.Conditionf(t, containsExternalWithGroup(servicesList.Payload.External, "redis"), "list does not contain external group %s", "redis")
+
+ // Filter services by a non-existing external group.
+ emptyServicesList, err := client.Default.Services.ListServices(&services.ListServicesParams{
+ Body: services.ListServicesBody{
+ ExternalGroup: "non-existing-external-group",
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.NotNil(t, emptyServicesList)
+ assert.Len(t, emptyServicesList.Payload.Mysql, 0)
+ assert.Len(t, emptyServicesList.Payload.Mongodb, 0)
+ assert.Len(t, emptyServicesList.Payload.Postgresql, 0)
+ assert.Len(t, emptyServicesList.Payload.Proxysql, 0)
+ assert.Len(t, emptyServicesList.Payload.External, 0)
+
+ // List services with out filter by external group.
+ noFilterServicesList, err := client.Default.Services.ListServices(&services.ListServicesParams{
+ Body: services.ListServicesBody{
+ ExternalGroup: "",
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.NotNil(t, noFilterServicesList)
+ assert.GreaterOrEqual(t, len(noFilterServicesList.Payload.Mysql), 0)
+ assert.GreaterOrEqual(t, len(noFilterServicesList.Payload.Mongodb), 0)
+ assert.GreaterOrEqual(t, len(noFilterServicesList.Payload.Postgresql), 1)
+ assert.GreaterOrEqual(t, len(noFilterServicesList.Payload.Proxysql), 0)
+ assert.GreaterOrEqual(t, len(noFilterServicesList.Payload.External), 1)
+ assert.Conditionf(t, containsExternalWithGroup(noFilterServicesList.Payload.External, "redis"), "list does not contain external group %s", "redis")
+
+ // Check duplicates.
+ params = &services.AddExternalServiceParams{
+ Body: services.AddExternalServiceBody{
+ NodeID: genericNodeID,
+ ServiceName: serviceName,
+ Group: "redis",
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err = client.Default.Services.AddExternalService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 409, codes.AlreadyExists, "Service with name %q already exists.", serviceName)
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveServices(t, res.Payload.External.ServiceID)
+ }
+ })
+
+ t.Run("AddNodeIDEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ params := &services.AddExternalServiceParams{
+ Body: services.AddExternalServiceBody{
+ NodeID: "",
+ ServiceName: pmmapitests.TestString(t, "External Service with empty node id"),
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddExternalService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field NodeId: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveServices(t, res.Payload.External.ServiceID)
+ }
+ })
+
+ t.Run("AddServiceNameEmpty", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ params := &services.AddExternalServiceParams{
+ Body: services.AddExternalServiceBody{
+ NodeID: genericNodeID,
+ ServiceName: "",
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddExternalService(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field ServiceName: value '' must not be an empty string")
+ if !assert.Nil(t, res) {
+ pmmapitests.RemoveServices(t, res.Payload.External.ServiceID)
+ }
+ })
+
+ t.Run("AddServiceWithOutGroup", func(t *testing.T) {
+ t.Parallel()
+
+ genericNodeID := pmmapitests.AddGenericNode(t, pmmapitests.TestString(t, "")).NodeID
+ require.NotEmpty(t, genericNodeID)
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ serviceName := pmmapitests.TestString(t, "Basic External Service")
+ params := &services.AddExternalServiceParams{
+ Body: services.AddExternalServiceBody{
+ NodeID: genericNodeID,
+ ServiceName: serviceName,
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := client.Default.Services.AddExternalService(params)
+ assert.NoError(t, err)
+ require.NotNil(t, res)
+ serviceID := res.Payload.External.ServiceID
+ assert.Equal(t, &services.AddExternalServiceOK{
+ Payload: &services.AddExternalServiceOKBody{
+ External: &services.AddExternalServiceOKBodyExternal{
+ ServiceID: serviceID,
+ NodeID: genericNodeID,
+ ServiceName: serviceName,
+ Group: "external",
+ },
+ },
+ }, res)
+ defer pmmapitests.RemoveServices(t, serviceID)
+ })
+}
diff --git a/api-tests/management/action/explain_test.go b/api-tests/management/action/explain_test.go
new file mode 100644
index 0000000000..ba81b1e5a6
--- /dev/null
+++ b/api-tests/management/action/explain_test.go
@@ -0,0 +1,117 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package action
+
+import (
+ "encoding/json"
+ "testing"
+ "time"
+
+ "github.com/percona/pmm/api/managementpb/json/client"
+ "github.com/percona/pmm/api/managementpb/json/client/actions"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestRunExplain(t *testing.T) {
+ t.Skip("not implemented yet")
+
+ explainActionOK, err := client.Default.Actions.StartMySQLExplainAction(&actions.StartMySQLExplainActionParams{
+ Context: pmmapitests.Context,
+ Body: actions.StartMySQLExplainActionBody{
+ // PMMAgentID: "/agent_id/f235005b-9cca-4b73-bbbd-1251067c3138",
+ ServiceID: "/service_id/5a9a7aa6-7af4-47be-817c-6d88e955bff2",
+ Query: "SELECT `t` . * FROM `test` . `key_value` `t`",
+ },
+ })
+ require.NoError(t, err)
+ require.NotEmpty(t, explainActionOK.Payload.ActionID)
+
+ time.Sleep(2 * time.Second)
+
+ actionOK, err := client.Default.Actions.GetAction(&actions.GetActionParams{
+ Context: pmmapitests.Context,
+ Body: actions.GetActionBody{
+ ActionID: explainActionOK.Payload.ActionID,
+ },
+ })
+ require.NoError(t, err)
+ require.Empty(t, actionOK.Payload.Error)
+ t.Log(actionOK.Payload.Output)
+}
+
+func TestRunMongoDBExplain(t *testing.T) {
+ // When we have an pmm-agent in dev-container and we can remove this skip, please remove the t.Logf at the end
+ // of this test and replace it with a proper test that checks the results.
+ t.Skip("pmm-agent in dev-container is not fully implemented yet")
+
+ explainActionOK, err := client.Default.Actions.StartMongoDBExplainAction(&actions.StartMongoDBExplainActionParams{
+ Context: pmmapitests.Context,
+ Body: actions.StartMongoDBExplainActionBody{
+ ServiceID: "/service_id/2402bf45-19c2-4bee-931a-307b26ed5300",
+ Query: `{"ns":"test.coll","op":"query","query":{"k":{"$lte":{"$numberInt":"1"}}}}`,
+ },
+ })
+ require.NoError(t, err)
+ require.NotEmpty(t, explainActionOK.Payload.ActionID)
+
+ var actionOK *actions.GetActionOK
+
+ for i := 0; i < 6; i++ {
+ var err error
+ actionOK, err = client.Default.Actions.GetAction(&actions.GetActionParams{
+ Context: pmmapitests.Context,
+ Body: actions.GetActionBody{
+ ActionID: explainActionOK.Payload.ActionID,
+ },
+ })
+ require.NoError(t, err)
+ require.Empty(t, actionOK.Payload.Error)
+
+ if actionOK.Payload.Done {
+ break
+ }
+
+ time.Sleep(500 * time.Millisecond)
+ }
+ assert.True(t, actionOK.Payload.Done)
+
+ want := map[string]interface{}{
+ "winningPlan": map[string]interface{}{
+ "stage": "EOF",
+ },
+ "rejectedPlans": []interface{}{},
+ "plannerVersion": map[string]interface{}{
+ "$numberInt": "1",
+ },
+ "namespace": "test.coll",
+ "indexFilterSet": bool(false),
+ "parsedQuery": map[string]interface{}{
+ "k": map[string]interface{}{
+ "$lte": map[string]interface{}{
+ "$numberInt": "1",
+ },
+ },
+ },
+ }
+ m := make(map[string]interface{})
+ err = json.Unmarshal([]byte(actionOK.Payload.Output), &m)
+ assert.NoError(t, err)
+ assert.Equal(t, m["queryPlanner"], want)
+}
diff --git a/api-tests/management/action/ptsummary_test.go b/api-tests/management/action/ptsummary_test.go
new file mode 100644
index 0000000000..099ac0ef6d
--- /dev/null
+++ b/api-tests/management/action/ptsummary_test.go
@@ -0,0 +1,66 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package action
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/percona/pmm/api/managementpb/json/client"
+ "github.com/percona/pmm/api/managementpb/json/client/actions"
+ "github.com/stretchr/testify/require"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestPTSummary(t *testing.T) {
+ ctx, cancel := context.WithTimeout(pmmapitests.Context, 30*time.Second)
+ defer cancel()
+
+ explainActionOK, err := client.Default.Actions.StartPTSummaryAction(&actions.StartPTSummaryActionParams{
+ Context: ctx,
+ Body: actions.StartPTSummaryActionBody{
+ NodeID: "pmm-server",
+ },
+ })
+ require.NoError(t, err)
+ require.NotEmpty(t, explainActionOK.Payload.ActionID)
+
+ for {
+ actionOK, err := client.Default.Actions.GetAction(&actions.GetActionParams{
+ Context: ctx,
+ Body: actions.GetActionBody{
+ ActionID: explainActionOK.Payload.ActionID,
+ },
+ })
+ require.NoError(t, err)
+
+ if !actionOK.Payload.Done {
+ time.Sleep(1 * time.Second)
+
+ continue
+ }
+
+ require.True(t, actionOK.Payload.Done)
+ require.Empty(t, actionOK.Payload.Error)
+ require.NotEmpty(t, actionOK.Payload.Output)
+ t.Log(actionOK.Payload.Output)
+
+ break
+ }
+}
diff --git a/api-tests/management/action/testdata/mongo_explain.json b/api-tests/management/action/testdata/mongo_explain.json
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/api-tests/management/annotation_test.go b/api-tests/management/annotation_test.go
new file mode 100644
index 0000000000..a3ee153016
--- /dev/null
+++ b/api-tests/management/annotation_test.go
@@ -0,0 +1,147 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package management
+
+import (
+ "testing"
+
+ inventoryClient "github.com/percona/pmm/api/inventorypb/json/client"
+ "github.com/percona/pmm/api/inventorypb/json/client/nodes"
+ "github.com/percona/pmm/api/inventorypb/json/client/services"
+ "github.com/percona/pmm/api/managementpb/json/client"
+ "github.com/percona/pmm/api/managementpb/json/client/annotation"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestAddAnnotation(t *testing.T) {
+ t.Run("Add Basic Annotation", func(t *testing.T) {
+ params := &annotation.AddAnnotationParams{
+ Body: annotation.AddAnnotationBody{
+ Text: "Annotation Text",
+ Tags: []string{"tag1", "tag2"},
+ },
+ Context: pmmapitests.Context,
+ }
+ _, err := client.Default.Annotation.AddAnnotation(params)
+ require.NoError(t, err)
+ })
+
+ t.Run("Add Empty Annotation", func(t *testing.T) {
+ params := &annotation.AddAnnotationParams{
+ Body: annotation.AddAnnotationBody{
+ Text: "",
+ Tags: []string{},
+ },
+ Context: pmmapitests.Context,
+ }
+ _, err := client.Default.Annotation.AddAnnotation(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field Text: value '' must not be an empty string")
+ })
+
+ t.Run("Non-existing service", func(t *testing.T) {
+ params := &annotation.AddAnnotationParams{
+ Body: annotation.AddAnnotationBody{
+ Text: "Some text",
+ ServiceNames: []string{"no-service"},
+ },
+ Context: pmmapitests.Context,
+ }
+ _, err := client.Default.Annotation.AddAnnotation(params)
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, `Service with name "no-service" not found.`)
+ })
+
+ t.Run("Non-existing node", func(t *testing.T) {
+ params := &annotation.AddAnnotationParams{
+ Body: annotation.AddAnnotationBody{
+ Text: "Some text",
+ NodeName: "no-node",
+ },
+ Context: pmmapitests.Context,
+ }
+ _, err := client.Default.Annotation.AddAnnotation(params)
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, `Node with name "no-node" not found.`)
+ })
+
+ t.Run("Existing service", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "annotation-node")
+ paramsNode := &nodes.AddGenericNodeParams{
+ Body: nodes.AddGenericNodeBody{
+ NodeName: nodeName,
+ Address: "10.0.0.1",
+ },
+ Context: pmmapitests.Context,
+ }
+ resNode, err := inventoryClient.Default.Nodes.AddGenericNode(paramsNode)
+ assert.NoError(t, err)
+ genericNodeID := resNode.Payload.Generic.NodeID
+ defer pmmapitests.RemoveNodes(t, genericNodeID)
+
+ serviceName := pmmapitests.TestString(t, "annotation-service")
+ paramsService := &services.AddMySQLServiceParams{
+ Body: services.AddMySQLServiceBody{
+ NodeID: genericNodeID,
+ Address: "localhost",
+ Port: 3306,
+ ServiceName: serviceName,
+ },
+ Context: pmmapitests.Context,
+ }
+ resService, err := inventoryClient.Default.Services.AddMySQLService(paramsService)
+ assert.NoError(t, err)
+ require.NotNil(t, resService)
+ serviceID := resService.Payload.Mysql.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ paramsAdd := &annotation.AddAnnotationParams{
+ Body: annotation.AddAnnotationBody{
+ Text: "Some text",
+ ServiceNames: []string{serviceName},
+ },
+ Context: pmmapitests.Context,
+ }
+ _, err = client.Default.Annotation.AddAnnotation(paramsAdd)
+ require.NoError(t, err)
+ })
+
+ t.Run("Existing node", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "annotation-node")
+ params := &nodes.AddGenericNodeParams{
+ Body: nodes.AddGenericNodeBody{
+ NodeName: nodeName,
+ Address: "10.0.0.1",
+ },
+ Context: pmmapitests.Context,
+ }
+ res, err := inventoryClient.Default.Nodes.AddGenericNode(params)
+ assert.NoError(t, err)
+ defer pmmapitests.RemoveNodes(t, res.Payload.Generic.NodeID)
+
+ paramsAdd := &annotation.AddAnnotationParams{
+ Body: annotation.AddAnnotationBody{
+ Text: "Some text",
+ NodeName: nodeName,
+ },
+ Context: pmmapitests.Context,
+ }
+ _, err = client.Default.Annotation.AddAnnotation(paramsAdd)
+ require.NoError(t, err)
+ })
+}
diff --git a/api-tests/management/backup/backups_test.go b/api-tests/management/backup/backups_test.go
new file mode 100644
index 0000000000..bcd198aa22
--- /dev/null
+++ b/api-tests/management/backup/backups_test.go
@@ -0,0 +1,157 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package backup
+
+import (
+ "testing"
+
+ "github.com/AlekSi/pointer"
+ "github.com/brianvoe/gofakeit/v6"
+ backupClient "github.com/percona/pmm/api/managementpb/backup/json/client"
+ "github.com/percona/pmm/api/managementpb/backup/json/client/backups"
+ "github.com/percona/pmm/api/managementpb/backup/json/client/locations"
+ managementClient "github.com/percona/pmm/api/managementpb/json/client"
+ mysql "github.com/percona/pmm/api/managementpb/json/client/my_sql"
+ "github.com/percona/pmm/api/managementpb/json/client/node"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+ "github.com/percona/pmm-managed/api-tests/management"
+)
+
+func TestScheduleBackup(t *testing.T) {
+ t.Parallel()
+
+ nodeName := pmmapitests.TestString(t, "node-for-basic-name")
+ nodeID, pmmAgentID := management.RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer management.RemovePMMAgentWithSubAgents(t, pmmAgentID)
+ serviceName := pmmapitests.TestString(t, "service-for-basic-name")
+
+ params := &mysql.AddMySQLParams{
+ Context: pmmapitests.Context,
+ Body: mysql.AddMySQLBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ Username: "username",
+
+ SkipConnectionCheck: true,
+ DisableCollectors: []string{"global_status", "perf_schema.tablelocks"},
+ },
+ }
+ addMySQLOK, err := managementClient.Default.MySQL.AddMySQL(params)
+ require.NoError(t, err)
+ serviceID := addMySQLOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ resp, err := backupClient.Default.Locations.AddLocation(&locations.AddLocationParams{
+ Body: locations.AddLocationBody{
+ Name: gofakeit.Name(),
+ Description: gofakeit.Question(),
+ PMMClientConfig: &locations.AddLocationParamsBodyPMMClientConfig{
+ Path: "/tmp",
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ defer deleteLocation(t, backupClient.Default.Locations, resp.Payload.LocationID)
+
+ client := backupClient.Default.Backups
+ backupRes, err := client.ScheduleBackup(&backups.ScheduleBackupParams{
+ Body: backups.ScheduleBackupBody{
+ ServiceID: serviceID,
+ LocationID: resp.Payload.LocationID,
+ CronExpression: "0 1 1 1 1",
+ Name: "testing",
+ Description: "testing",
+ Enabled: false,
+ },
+ Context: pmmapitests.Context,
+ })
+
+ assert.NoError(t, err)
+ assert.NotEmpty(t, backupRes.Payload.ScheduledBackupID)
+
+ body := backups.ChangeScheduledBackupBody{
+ ScheduledBackupID: backupRes.Payload.ScheduledBackupID,
+ Enabled: true,
+ CronExpression: "0 2 2 2 2",
+ Name: "test2",
+ Description: "test2",
+ }
+ changeRes, err := client.ChangeScheduledBackup(&backups.ChangeScheduledBackupParams{
+ Body: body,
+ Context: pmmapitests.Context,
+ })
+
+ assert.NoError(t, err)
+ assert.NotNil(t, changeRes)
+
+ listRes, err := client.ListScheduledBackups(&backups.ListScheduledBackupsParams{
+ Context: pmmapitests.Context,
+ })
+
+ assert.NoError(t, err)
+ var backup *backups.ScheduledBackupsItems0
+ for _, b := range listRes.Payload.ScheduledBackups {
+ if b.ScheduledBackupID == backupRes.Payload.ScheduledBackupID {
+ backup = b
+ break
+ }
+ }
+
+ require.NotNil(t, backup)
+
+ // Assert change
+ assert.Equal(t, body.Enabled, backup.Enabled)
+ assert.Equal(t, body.Name, backup.Name)
+ assert.Equal(t, body.Description, backup.Description)
+ assert.Equal(t, body.CronExpression, backup.CronExpression)
+
+ _, err = client.RemoveScheduledBackup(&backups.RemoveScheduledBackupParams{
+ Body: backups.RemoveScheduledBackupBody{
+ ScheduledBackupID: backupRes.Payload.ScheduledBackupID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+
+ find := func(id string, backups []*backups.ScheduledBackupsItems0) *backups.ScheduledBackupsItems0 {
+ for _, b := range backups {
+ if b.ScheduledBackupID == id {
+ return b
+ }
+ }
+ return nil
+ }
+ listRes, err = client.ListScheduledBackups(&backups.ListScheduledBackupsParams{
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, listRes)
+
+ deleted := find(backupRes.Payload.ScheduledBackupID, listRes.Payload.ScheduledBackups)
+ assert.Nil(t, deleted, "scheduled backup %s is not deleted", backupRes.Payload.ScheduledBackupID)
+}
diff --git a/api-tests/management/backup/locations_test.go b/api-tests/management/backup/locations_test.go
new file mode 100644
index 0000000000..112375eadf
--- /dev/null
+++ b/api-tests/management/backup/locations_test.go
@@ -0,0 +1,600 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package backup
+
+import (
+ "os"
+ "testing"
+
+ "github.com/brianvoe/gofakeit/v6"
+ backupClient "github.com/percona/pmm/api/managementpb/backup/json/client"
+ "github.com/percona/pmm/api/managementpb/backup/json/client/locations"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestAddLocation(t *testing.T) {
+ t.Parallel()
+ client := backupClient.Default.Locations
+
+ t.Run("normal pmm client config", func(t *testing.T) {
+ t.Parallel()
+
+ resp, err := client.AddLocation(&locations.AddLocationParams{
+ Body: locations.AddLocationBody{
+ Name: gofakeit.Name(),
+ Description: gofakeit.Question(),
+ PMMClientConfig: &locations.AddLocationParamsBodyPMMClientConfig{
+ Path: "/tmp",
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ defer deleteLocation(t, client, resp.Payload.LocationID)
+
+ assert.NotEmpty(t, resp.Payload.LocationID)
+ })
+
+ t.Run("normal pmm server config", func(t *testing.T) {
+ t.Parallel()
+
+ resp, err := client.AddLocation(&locations.AddLocationParams{
+ Body: locations.AddLocationBody{
+ Name: gofakeit.Name(),
+ Description: gofakeit.Question(),
+ PMMServerConfig: &locations.AddLocationParamsBodyPMMServerConfig{
+ Path: "/tmp",
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ defer deleteLocation(t, client, resp.Payload.LocationID)
+
+ assert.NotEmpty(t, resp.Payload.LocationID)
+ })
+
+ t.Run("normal s3 config", func(t *testing.T) {
+ t.Parallel()
+ accessKey, secretKey, bucketName := os.Getenv("AWS_ACCESS_KEY"), os.Getenv("AWS_SECRET_KEY"), os.Getenv("AWS_BUCKET_NAME")
+ if accessKey == "" || secretKey == "" || bucketName == "" {
+ t.Skip("Skipping add S3 backup location - missing credentials")
+ }
+ resp, err := client.AddLocation(&locations.AddLocationParams{
+ Body: locations.AddLocationBody{
+ Name: gofakeit.Name(),
+ Description: gofakeit.Question(),
+ S3Config: &locations.AddLocationParamsBodyS3Config{
+ Endpoint: "https://s3.us-west-2.amazonaws.com",
+ AccessKey: accessKey,
+ SecretKey: secretKey,
+ BucketName: bucketName,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ defer deleteLocation(t, client, resp.Payload.LocationID)
+
+ assert.NotEmpty(t, resp.Payload.LocationID)
+ })
+}
+
+func TestAddWrongLocation(t *testing.T) {
+ t.Parallel()
+ client := backupClient.Default.Locations
+
+ t.Run("missing config", func(t *testing.T) {
+ t.Parallel()
+
+ resp, err := client.AddLocation(&locations.AddLocationParams{
+ Body: locations.AddLocationBody{
+ Name: gofakeit.Name(),
+ Description: gofakeit.Question(),
+ },
+ Context: pmmapitests.Context,
+ })
+
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Missing location config.")
+ assert.Nil(t, resp)
+ })
+
+ t.Run("missing client config path", func(t *testing.T) {
+ t.Parallel()
+
+ resp, err := client.AddLocation(&locations.AddLocationParams{
+ Body: locations.AddLocationBody{
+ Name: gofakeit.Name(),
+ Description: gofakeit.Question(),
+ PMMClientConfig: &locations.AddLocationParamsBodyPMMClientConfig{},
+ },
+ Context: pmmapitests.Context,
+ })
+
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field PmmClientConfig.Path: value '' must not be an empty string")
+ assert.Nil(t, resp)
+ })
+
+ t.Run("missing name", func(t *testing.T) {
+ t.Parallel()
+
+ resp, err := client.AddLocation(&locations.AddLocationParams{
+ Body: locations.AddLocationBody{
+ Description: gofakeit.Question(),
+ },
+ Context: pmmapitests.Context,
+ })
+
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field Name: value '' must not be an empty string")
+ assert.Nil(t, resp)
+ })
+
+ t.Run("missing s3 endpoint", func(t *testing.T) {
+ t.Parallel()
+
+ resp, err := client.AddLocation(&locations.AddLocationParams{
+ Body: locations.AddLocationBody{
+ Name: gofakeit.Name(),
+ Description: gofakeit.Question(),
+ S3Config: &locations.AddLocationParamsBodyS3Config{
+ AccessKey: "access_key",
+ SecretKey: "secret_key",
+ BucketName: "example_bucket",
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field S3Config.Endpoint: value '' must not be an empty string")
+ assert.Nil(t, resp)
+ })
+
+ t.Run("missing s3 bucket", func(t *testing.T) {
+ t.Parallel()
+
+ resp, err := client.AddLocation(&locations.AddLocationParams{
+ Body: locations.AddLocationBody{
+ Name: gofakeit.Name(),
+ Description: gofakeit.Question(),
+ S3Config: &locations.AddLocationParamsBodyS3Config{
+ Endpoint: "http://example.com",
+ AccessKey: "access_key",
+ SecretKey: "secret_key",
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field S3Config.BucketName: value '' must not be an empty string")
+ assert.Nil(t, resp)
+ })
+
+ t.Run("double config", func(t *testing.T) {
+ t.Parallel()
+
+ resp, err := client.AddLocation(&locations.AddLocationParams{
+ Body: locations.AddLocationBody{
+ Name: gofakeit.Name(),
+ Description: gofakeit.Question(),
+ PMMClientConfig: &locations.AddLocationParamsBodyPMMClientConfig{
+ Path: "/tmp",
+ },
+ S3Config: &locations.AddLocationParamsBodyS3Config{
+ Endpoint: "http://example.com",
+ AccessKey: "access_key",
+ SecretKey: "secret_key",
+ BucketName: "example_bucket",
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Only one config is allowed.")
+
+ assert.Nil(t, resp)
+ })
+}
+
+func TestListLocations(t *testing.T) {
+ t.Parallel()
+ client := backupClient.Default.Locations
+
+ body := locations.AddLocationBody{
+ Name: gofakeit.Name(),
+ Description: gofakeit.Question(),
+ PMMClientConfig: &locations.AddLocationParamsBodyPMMClientConfig{
+ Path: "/tmp",
+ },
+ }
+ addResp, err := client.AddLocation(&locations.AddLocationParams{
+ Body: body,
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ defer deleteLocation(t, client, addResp.Payload.LocationID)
+
+ resp, err := client.ListLocations(&locations.ListLocationsParams{Context: pmmapitests.Context})
+ require.NoError(t, err)
+
+ assert.NotEmpty(t, resp.Payload.Locations)
+ var found bool
+ for _, loc := range resp.Payload.Locations {
+ if loc.LocationID == addResp.Payload.LocationID {
+ assert.Equal(t, body.Name, loc.Name)
+ assert.Equal(t, body.Description, loc.Description)
+ assert.Equal(t, body.PMMClientConfig.Path, loc.PMMClientConfig.Path)
+ found = true
+ }
+ }
+ assert.True(t, found, "Expected location not found")
+}
+
+func TestChangeLocation(t *testing.T) {
+ t.Parallel()
+ client := backupClient.Default.Locations
+
+ checkChange := func(t *testing.T, req locations.ChangeLocationBody, locations []*locations.LocationsItems0) {
+ t.Helper()
+ var found bool
+ for _, loc := range locations {
+ if loc.LocationID == req.LocationID {
+ assert.Equal(t, req.Name, loc.Name)
+ if req.Description != "" {
+ assert.Equal(t, req.Description, loc.Description)
+ }
+
+ if req.PMMServerConfig != nil {
+ require.NotNil(t, loc.PMMServerConfig)
+ assert.Equal(t, req.PMMServerConfig.Path, loc.PMMServerConfig.Path)
+ } else {
+ assert.Nil(t, loc.PMMServerConfig)
+ }
+
+ if req.PMMClientConfig != nil {
+ require.NotNil(t, loc.PMMClientConfig)
+ assert.Equal(t, req.PMMClientConfig.Path, loc.PMMClientConfig.Path)
+ } else {
+ assert.Nil(t, loc.PMMClientConfig)
+ }
+
+ if req.S3Config != nil {
+ require.NotNil(t, loc.S3Config)
+ assert.Equal(t, req.S3Config.Endpoint, loc.S3Config.Endpoint)
+ assert.Equal(t, req.S3Config.AccessKey, loc.S3Config.AccessKey)
+ assert.Equal(t, req.S3Config.SecretKey, loc.S3Config.SecretKey)
+ assert.Equal(t, req.S3Config.BucketName, loc.S3Config.BucketName)
+ } else {
+ assert.Nil(t, loc.S3Config)
+ }
+
+ found = true
+
+ break
+ }
+ }
+ assert.True(t, found)
+ }
+ t.Run("update name and config path", func(t *testing.T) {
+ t.Parallel()
+
+ addReqBody := locations.AddLocationBody{
+ Name: gofakeit.Name(),
+ Description: gofakeit.Question(),
+ PMMServerConfig: &locations.AddLocationParamsBodyPMMServerConfig{
+ Path: "/tmp",
+ },
+ }
+ resp, err := client.AddLocation(&locations.AddLocationParams{
+ Body: addReqBody,
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ defer deleteLocation(t, client, resp.Payload.LocationID)
+
+ updateBody := locations.ChangeLocationBody{
+ LocationID: resp.Payload.LocationID,
+ Name: gofakeit.Name(),
+ PMMServerConfig: &locations.ChangeLocationParamsBodyPMMServerConfig{
+ Path: "/tmp/nested",
+ },
+ }
+ _, err = client.ChangeLocation(&locations.ChangeLocationParams{
+ Body: updateBody,
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ listResp, err := client.ListLocations(&locations.ListLocationsParams{Context: pmmapitests.Context})
+ require.NoError(t, err)
+
+ checkChange(t, updateBody, listResp.Payload.Locations)
+ })
+
+ t.Run("update only name", func(t *testing.T) {
+ t.Parallel()
+
+ addReqBody := locations.AddLocationBody{
+ Name: gofakeit.Name(),
+ Description: gofakeit.Question(),
+ PMMServerConfig: &locations.AddLocationParamsBodyPMMServerConfig{
+ Path: "/tmp",
+ },
+ }
+ resp, err := client.AddLocation(&locations.AddLocationParams{
+ Body: addReqBody,
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ defer deleteLocation(t, client, resp.Payload.LocationID)
+
+ updateBody := locations.ChangeLocationBody{
+ LocationID: resp.Payload.LocationID,
+ Name: gofakeit.Name(),
+ }
+ _, err = client.ChangeLocation(&locations.ChangeLocationParams{
+ Body: updateBody,
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ listResp, err := client.ListLocations(&locations.ListLocationsParams{Context: pmmapitests.Context})
+ require.NoError(t, err)
+
+ var location *locations.LocationsItems0
+ for _, loc := range listResp.Payload.Locations {
+ if loc.LocationID == resp.Payload.LocationID {
+ location = loc
+ break
+ }
+ }
+ require.NotNil(t, location)
+
+ assert.Equal(t, location.Name, updateBody.Name)
+ require.NotNil(t, location.PMMServerConfig)
+ assert.Equal(t, addReqBody.PMMServerConfig.Path, location.PMMServerConfig.Path)
+ })
+
+ t.Run("change config type", func(t *testing.T) {
+ t.Parallel()
+
+ addReqBody := locations.AddLocationBody{
+ Name: gofakeit.Name(),
+ Description: gofakeit.Question(),
+ PMMServerConfig: &locations.AddLocationParamsBodyPMMServerConfig{
+ Path: "/tmp",
+ },
+ }
+ resp, err := client.AddLocation(&locations.AddLocationParams{
+ Body: addReqBody,
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ defer deleteLocation(t, client, resp.Payload.LocationID)
+
+ updateBody := locations.ChangeLocationBody{
+ LocationID: resp.Payload.LocationID,
+ Name: gofakeit.Name(),
+ PMMClientConfig: &locations.ChangeLocationParamsBodyPMMClientConfig{
+ Path: "/root",
+ },
+ }
+ _, err = client.ChangeLocation(&locations.ChangeLocationParams{
+ Body: updateBody,
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ listResp, err := client.ListLocations(&locations.ListLocationsParams{Context: pmmapitests.Context})
+ require.NoError(t, err)
+
+ checkChange(t, updateBody, listResp.Payload.Locations)
+ })
+
+ t.Run("change to existing name - error", func(t *testing.T) {
+ t.Parallel()
+
+ addReqBody1 := locations.AddLocationBody{
+ Name: gofakeit.Name(),
+ Description: gofakeit.Question(),
+ PMMServerConfig: &locations.AddLocationParamsBodyPMMServerConfig{
+ Path: "/tmp",
+ },
+ }
+ resp1, err := client.AddLocation(&locations.AddLocationParams{
+ Body: addReqBody1,
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ defer deleteLocation(t, client, resp1.Payload.LocationID)
+
+ addReqBody2 := locations.AddLocationBody{
+ Name: gofakeit.Name(),
+ Description: gofakeit.Question(),
+ PMMServerConfig: &locations.AddLocationParamsBodyPMMServerConfig{
+ Path: "/tmp",
+ },
+ }
+ resp2, err := client.AddLocation(&locations.AddLocationParams{
+ Body: addReqBody2,
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ defer deleteLocation(t, client, resp2.Payload.LocationID)
+
+ updateBody := locations.ChangeLocationBody{
+ LocationID: resp2.Payload.LocationID,
+ Name: addReqBody1.Name,
+ PMMServerConfig: &locations.ChangeLocationParamsBodyPMMServerConfig{
+ Path: "/tmp",
+ },
+ }
+ _, err = client.ChangeLocation(&locations.ChangeLocationParams{
+ Body: updateBody,
+ Context: pmmapitests.Context,
+ })
+
+ pmmapitests.AssertAPIErrorf(t, err, 409, codes.AlreadyExists, `Location with name "%s" already exists.`, updateBody.Name)
+
+ })
+}
+
+func TestRemoveLocation(t *testing.T) {
+ t.Parallel()
+ client := backupClient.Default.Locations
+ resp, err := client.AddLocation(&locations.AddLocationParams{
+ Body: locations.AddLocationBody{
+ Name: gofakeit.Name(),
+ Description: gofakeit.Question(),
+ PMMClientConfig: &locations.AddLocationParamsBodyPMMClientConfig{
+ Path: "/tmp",
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ _, err = client.RemoveLocation(&locations.RemoveLocationParams{
+ Body: locations.RemoveLocationBody{
+ LocationID: resp.Payload.LocationID,
+ Force: false,
+ },
+ Context: pmmapitests.Context,
+ })
+
+ require.NoError(t, err)
+
+ assertNotFound := func(id string, locations []*locations.LocationsItems0) func() bool {
+ return func() bool {
+ for _, loc := range locations {
+ if loc.LocationID == id {
+ return false
+ }
+ }
+ return true
+ }
+ }
+
+ listResp, err := client.ListLocations(&locations.ListLocationsParams{Context: pmmapitests.Context})
+ require.NoError(t, err)
+
+ assert.Condition(t, assertNotFound(resp.Payload.LocationID, listResp.Payload.Locations))
+}
+
+func TestLocationConfigValidation(t *testing.T) {
+ t.Parallel()
+ client := backupClient.Default.Locations
+
+ t.Run("missing config", func(t *testing.T) {
+ t.Parallel()
+
+ resp, err := client.TestLocationConfig(&locations.TestLocationConfigParams{
+ Body: locations.TestLocationConfigBody{},
+ Context: pmmapitests.Context,
+ })
+
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Missing location config.")
+ assert.Nil(t, resp)
+ })
+
+ t.Run("missing client config path", func(t *testing.T) {
+ t.Parallel()
+
+ resp, err := client.TestLocationConfig(&locations.TestLocationConfigParams{
+ Body: locations.TestLocationConfigBody{
+ PMMClientConfig: &locations.TestLocationConfigParamsBodyPMMClientConfig{},
+ },
+ Context: pmmapitests.Context,
+ })
+
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field PmmClientConfig.Path: value '' must not be an empty string")
+ assert.Nil(t, resp)
+ })
+
+ t.Run("missing s3 endpoint", func(t *testing.T) {
+ t.Parallel()
+
+ resp, err := client.TestLocationConfig(&locations.TestLocationConfigParams{
+ Body: locations.TestLocationConfigBody{
+ S3Config: &locations.TestLocationConfigParamsBodyS3Config{
+ AccessKey: "access_key",
+ SecretKey: "secret_key",
+ BucketName: "example_bucket",
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field S3Config.Endpoint: value '' must not be an empty string")
+ assert.Nil(t, resp)
+ })
+
+ t.Run("missing s3 bucket", func(t *testing.T) {
+ t.Parallel()
+
+ resp, err := client.TestLocationConfig(&locations.TestLocationConfigParams{
+ Body: locations.TestLocationConfigBody{
+ S3Config: &locations.TestLocationConfigParamsBodyS3Config{
+ Endpoint: "http://example.com",
+ AccessKey: "access_key",
+ SecretKey: "secret_key",
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field S3Config.BucketName: value '' must not be an empty string")
+ assert.Nil(t, resp)
+ })
+
+ t.Run("double config", func(t *testing.T) {
+ t.Parallel()
+
+ resp, err := client.TestLocationConfig(&locations.TestLocationConfigParams{
+ Body: locations.TestLocationConfigBody{
+ PMMClientConfig: &locations.TestLocationConfigParamsBodyPMMClientConfig{
+ Path: "/tmp",
+ },
+ S3Config: &locations.TestLocationConfigParamsBodyS3Config{
+ Endpoint: "http://example.com",
+ AccessKey: "access_key",
+ SecretKey: "secret_key",
+ BucketName: "example_bucket",
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Only one config is allowed.")
+
+ assert.Nil(t, resp)
+ })
+}
+
+func deleteLocation(t *testing.T, client locations.ClientService, id string) {
+ t.Helper()
+ _, err := client.RemoveLocation(&locations.RemoveLocationParams{
+ Body: locations.RemoveLocationBody{
+ LocationID: id,
+ Force: false,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+}
diff --git a/api-tests/management/dbaas/helpers.go b/api-tests/management/dbaas/helpers.go
new file mode 100644
index 0000000000..cf9d4b03cd
--- /dev/null
+++ b/api-tests/management/dbaas/helpers.go
@@ -0,0 +1,54 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package dbaas
+
+import (
+ "testing"
+
+ dbaasClient "github.com/percona/pmm/api/managementpb/dbaas/json/client"
+ "github.com/percona/pmm/api/managementpb/dbaas/json/client/kubernetes"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func registerKubernetesCluster(t *testing.T, kubernetesClusterName string, kubeconfig string) {
+ registerKubernetesClusterResponse, err := dbaasClient.Default.Kubernetes.RegisterKubernetesCluster(
+ &kubernetes.RegisterKubernetesClusterParams{
+ Body: kubernetes.RegisterKubernetesClusterBody{
+ KubernetesClusterName: kubernetesClusterName,
+ KubeAuth: &kubernetes.RegisterKubernetesClusterParamsBodyKubeAuth{Kubeconfig: kubeconfig},
+ },
+ Context: pmmapitests.Context,
+ },
+ )
+ require.NoError(t, err)
+ assert.NotNil(t, registerKubernetesClusterResponse)
+ t.Cleanup(func() {
+ _, _ = unregisterKubernetesCluster(kubernetesClusterName)
+ })
+}
+
+func unregisterKubernetesCluster(kubernetesClusterName string) (*kubernetes.UnregisterKubernetesClusterOK, error) {
+ return dbaasClient.Default.Kubernetes.UnregisterKubernetesCluster(
+ &kubernetes.UnregisterKubernetesClusterParams{
+ Body: kubernetes.UnregisterKubernetesClusterBody{KubernetesClusterName: kubernetesClusterName},
+ Context: pmmapitests.Context,
+ },
+ )
+}
diff --git a/api-tests/management/dbaas/kubernetes_server_test.go b/api-tests/management/dbaas/kubernetes_server_test.go
new file mode 100644
index 0000000000..5e05e75ff7
--- /dev/null
+++ b/api-tests/management/dbaas/kubernetes_server_test.go
@@ -0,0 +1,268 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package dbaas
+
+import (
+ "fmt"
+ "os"
+ "testing"
+ "time"
+
+ dbaasClient "github.com/percona/pmm/api/managementpb/dbaas/json/client"
+ "github.com/percona/pmm/api/managementpb/dbaas/json/client/kubernetes"
+ psmdbcluster "github.com/percona/pmm/api/managementpb/dbaas/json/client/psmdb_cluster"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestKubernetesServer(t *testing.T) {
+ if os.Getenv("PERCONA_TEST_DBAAS") != "1" {
+ t.Skip("PERCONA_TEST_DBAAS env variable is not passed, skipping")
+ }
+ kubeConfig := os.Getenv("PERCONA_TEST_DBAAS_KUBECONFIG")
+ if kubeConfig == "" {
+ t.Skip("PERCONA_TEST_DBAAS_KUBECONFIG env variable is not provided")
+ }
+ t.Run("Basic", func(t *testing.T) {
+ kubernetesClusterName := pmmapitests.TestString(t, "api-test-cluster")
+ clusters, err := dbaasClient.Default.Kubernetes.ListKubernetesClusters(nil)
+ require.NoError(t, err)
+ require.NotContains(t, clusters.Payload.KubernetesClusters, &kubernetes.KubernetesClustersItems0{KubernetesClusterName: kubernetesClusterName})
+
+ registerKubernetesCluster(t, kubernetesClusterName, kubeConfig)
+ clusters, err = dbaasClient.Default.Kubernetes.ListKubernetesClusters(nil)
+ assert.NoError(t, err)
+ assert.GreaterOrEqual(t, len(clusters.Payload.KubernetesClusters), 1)
+ assert.Contains(t, clusters.Payload.KubernetesClusters, &kubernetes.KubernetesClustersItems0{KubernetesClusterName: kubernetesClusterName})
+
+ unregisterKubernetesClusterResponse, err := dbaasClient.Default.Kubernetes.UnregisterKubernetesCluster(
+ &kubernetes.UnregisterKubernetesClusterParams{
+ Body: kubernetes.UnregisterKubernetesClusterBody{KubernetesClusterName: kubernetesClusterName},
+ Context: pmmapitests.Context,
+ },
+ )
+ require.NoError(t, err)
+ assert.NotNil(t, unregisterKubernetesClusterResponse)
+
+ clusters, err = dbaasClient.Default.Kubernetes.ListKubernetesClusters(nil)
+ assert.NoError(t, err)
+ require.NotContains(t, clusters.Payload.KubernetesClusters, &kubernetes.KubernetesClustersItems0{KubernetesClusterName: kubernetesClusterName})
+ })
+
+ t.Run("DuplicateClusterName", func(t *testing.T) {
+ kubernetesClusterName := pmmapitests.TestString(t, "api-test-cluster-duplicate")
+
+ registerKubernetesCluster(t, kubernetesClusterName, kubeConfig)
+ registerKubernetesClusterResponse, err := dbaasClient.Default.Kubernetes.RegisterKubernetesCluster(
+ &kubernetes.RegisterKubernetesClusterParams{
+ Body: kubernetes.RegisterKubernetesClusterBody{
+ KubernetesClusterName: kubernetesClusterName,
+ KubeAuth: &kubernetes.RegisterKubernetesClusterParamsBodyKubeAuth{Kubeconfig: kubeConfig},
+ },
+ Context: pmmapitests.Context,
+ },
+ )
+ pmmapitests.AssertAPIErrorf(t, err, 409, codes.AlreadyExists, fmt.Sprintf("Kubernetes Cluster with Name %q already exists.", kubernetesClusterName))
+ require.Nil(t, registerKubernetesClusterResponse)
+ })
+
+ t.Run("EmptyKubernetesClusterName", func(t *testing.T) {
+ registerKubernetesClusterResponse, err := dbaasClient.Default.Kubernetes.RegisterKubernetesCluster(
+ &kubernetes.RegisterKubernetesClusterParams{
+ Body: kubernetes.RegisterKubernetesClusterBody{
+ KubernetesClusterName: "",
+ KubeAuth: &kubernetes.RegisterKubernetesClusterParamsBodyKubeAuth{Kubeconfig: kubeConfig},
+ },
+ Context: pmmapitests.Context,
+ },
+ )
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field KubernetesClusterName: value '' must not be an empty string")
+ require.Nil(t, registerKubernetesClusterResponse)
+ })
+
+ t.Run("EmptyKubeConfig", func(t *testing.T) {
+ registerKubernetesClusterResponse, err := dbaasClient.Default.Kubernetes.RegisterKubernetesCluster(
+ &kubernetes.RegisterKubernetesClusterParams{
+ Body: kubernetes.RegisterKubernetesClusterBody{
+ KubernetesClusterName: "empty-kube-config",
+ KubeAuth: &kubernetes.RegisterKubernetesClusterParamsBodyKubeAuth{},
+ },
+ Context: pmmapitests.Context,
+ },
+ )
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field KubeAuth.Kubeconfig: value '' must not be an empty string")
+ require.Nil(t, registerKubernetesClusterResponse)
+ })
+
+ t.Run("GetKubernetesCluster", func(t *testing.T) {
+ kubernetesClusterName := pmmapitests.TestString(t, "api-test-cluster")
+ registerKubernetesCluster(t, kubernetesClusterName, kubeConfig)
+
+ cluster, err := dbaasClient.Default.Kubernetes.GetKubernetesCluster(&kubernetes.GetKubernetesClusterParams{
+ Body: kubernetes.GetKubernetesClusterBody{
+ KubernetesClusterName: kubernetesClusterName,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.NotNil(t, cluster)
+ assert.NotNil(t, cluster.Payload.KubeAuth)
+ assert.Equal(t, kubeConfig, cluster.Payload.KubeAuth.Kubeconfig)
+ })
+
+ t.Run("GetResources", func(t *testing.T) {
+ kubernetesClusterName := pmmapitests.TestString(t, "api-test-cluster")
+
+ resources, err := dbaasClient.Default.Kubernetes.GetResources(&kubernetes.GetResourcesParams{
+ Body: kubernetes.GetResourcesBody{
+ KubernetesClusterName: kubernetesClusterName,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ require.NotNil(t, resources)
+ require.NotNil(t, resources.Payload.All)
+ require.NotNil(t, resources.Payload.Available)
+ assert.Greater(t, resources.Payload.All.CPUm, resources.Payload.Available.CPUm)
+ assert.Greater(t, resources.Payload.All.MemoryBytes, resources.Payload.Available.MemoryBytes)
+ assert.Greater(t, resources.Payload.All.DiskSize, resources.Payload.Available.DiskSize)
+ assert.Greater(t, resources.Payload.Available.CPUm, uint64(0))
+ assert.Greater(t, resources.Payload.Available.MemoryBytes, uint64(0))
+ assert.Greater(t, resources.Payload.Available.DiskSize, uint64(0))
+ })
+
+ t.Run("UnregisterNotExistCluster", func(t *testing.T) {
+ unregisterKubernetesClusterOK, err := unregisterKubernetesCluster("not-exist-cluster")
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Kubernetes Cluster with name \"not-exist-cluster\" not found.")
+ require.Nil(t, unregisterKubernetesClusterOK)
+ })
+
+ t.Run("UnregisterEmptyClusterName", func(t *testing.T) {
+ unregisterKubernetesClusterOK, err := unregisterKubernetesCluster("")
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field KubernetesClusterName: value '' must not be an empty string")
+ require.Nil(t, unregisterKubernetesClusterOK)
+ })
+
+ t.Run("UnregisterWithoutAndWithForce", func(t *testing.T) {
+ kubernetesClusterName := pmmapitests.TestString(t, "api-test-cluster")
+ dbClusterName := "first-psmdb-test"
+ clusters, err := dbaasClient.Default.Kubernetes.ListKubernetesClusters(nil)
+ require.NoError(t, err)
+ require.NotContains(t, clusters.Payload.KubernetesClusters, &kubernetes.KubernetesClustersItems0{KubernetesClusterName: kubernetesClusterName})
+ registerKubernetesCluster(t, kubernetesClusterName, kubeConfig)
+
+ paramsFirstPSMDB := psmdbcluster.CreatePSMDBClusterParams{
+ Context: pmmapitests.Context,
+ Body: psmdbcluster.CreatePSMDBClusterBody{
+ KubernetesClusterName: kubernetesClusterName,
+ Name: dbClusterName,
+ Params: &psmdbcluster.CreatePSMDBClusterParamsBodyParams{
+ ClusterSize: 3,
+ Replicaset: &psmdbcluster.CreatePSMDBClusterParamsBodyParamsReplicaset{
+ ComputeResources: &psmdbcluster.CreatePSMDBClusterParamsBodyParamsReplicasetComputeResources{
+ CPUm: 500,
+ MemoryBytes: "1000000000",
+ },
+ DiskSize: "1000000000",
+ },
+ },
+ },
+ }
+ _, err = dbaasClient.Default.PSMDBCluster.CreatePSMDBCluster(¶msFirstPSMDB)
+ assert.NoError(t, err)
+
+ clusters, err = dbaasClient.Default.Kubernetes.ListKubernetesClusters(nil)
+ assert.NoError(t, err)
+ assert.GreaterOrEqual(t, len(clusters.Payload.KubernetesClusters), 1)
+ assert.Contains(t, clusters.Payload.KubernetesClusters, &kubernetes.KubernetesClustersItems0{KubernetesClusterName: kubernetesClusterName})
+
+ _, err = dbaasClient.Default.Kubernetes.UnregisterKubernetesCluster(
+ &kubernetes.UnregisterKubernetesClusterParams{
+ Body: kubernetes.UnregisterKubernetesClusterBody{
+ KubernetesClusterName: kubernetesClusterName,
+ },
+ Context: pmmapitests.Context,
+ },
+ )
+ require.Error(t, err)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.FailedPrecondition, fmt.Sprintf(`Kubernetes cluster %s has PSMDB clusters`, kubernetesClusterName))
+
+ unregisterKubernetesClusterResponse, err := dbaasClient.Default.Kubernetes.UnregisterKubernetesCluster(
+ &kubernetes.UnregisterKubernetesClusterParams{
+ Body: kubernetes.UnregisterKubernetesClusterBody{
+ KubernetesClusterName: kubernetesClusterName,
+ Force: true,
+ },
+ Context: pmmapitests.Context,
+ },
+ )
+ require.NoError(t, err)
+ assert.NotNil(t, unregisterKubernetesClusterResponse)
+
+ _, err = dbaasClient.Default.Kubernetes.UnregisterKubernetesCluster(
+ &kubernetes.UnregisterKubernetesClusterParams{
+ Body: kubernetes.UnregisterKubernetesClusterBody{
+ KubernetesClusterName: kubernetesClusterName,
+ },
+ Context: pmmapitests.Context,
+ },
+ )
+ require.Error(t, err)
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, fmt.Sprintf(`Kubernetes Cluster with name "%s" not found.`, kubernetesClusterName))
+
+ registerKubernetesCluster(t, kubernetesClusterName, kubeConfig)
+ deletePSMDBClusterParamsParam := psmdbcluster.DeletePSMDBClusterParams{
+ Context: pmmapitests.Context,
+ Body: psmdbcluster.DeletePSMDBClusterBody{
+ KubernetesClusterName: kubernetesClusterName,
+ Name: dbClusterName,
+ },
+ }
+ _, err = dbaasClient.Default.PSMDBCluster.DeletePSMDBCluster(&deletePSMDBClusterParamsParam)
+ assert.NoError(t, err)
+
+ listPSMDBClustersParamsParam := psmdbcluster.ListPSMDBClustersParams{
+ Context: pmmapitests.Context,
+ Body: psmdbcluster.ListPSMDBClustersBody{
+ KubernetesClusterName: kubernetesClusterName,
+ },
+ }
+
+ for {
+ psmDBClusters, err := dbaasClient.Default.PSMDBCluster.ListPSMDBClusters(&listPSMDBClustersParamsParam)
+ assert.NoError(t, err)
+ if len(psmDBClusters.Payload.Clusters) == 0 {
+ break
+ }
+ time.Sleep(1 * time.Second)
+ }
+
+ unregisterKubernetesClusterResponse, err = dbaasClient.Default.Kubernetes.UnregisterKubernetesCluster(
+ &kubernetes.UnregisterKubernetesClusterParams{
+ Body: kubernetes.UnregisterKubernetesClusterBody{
+ KubernetesClusterName: kubernetesClusterName,
+ },
+ Context: pmmapitests.Context,
+ },
+ )
+ assert.NoError(t, err)
+ assert.NotNil(t, unregisterKubernetesClusterResponse)
+ })
+}
diff --git a/api-tests/management/dbaas/psmdb_cluster_test.go b/api-tests/management/dbaas/psmdb_cluster_test.go
new file mode 100644
index 0000000000..a2ea7b41c9
--- /dev/null
+++ b/api-tests/management/dbaas/psmdb_cluster_test.go
@@ -0,0 +1,282 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package dbaas
+
+import (
+ "testing"
+
+ dbaasClient "github.com/percona/pmm/api/managementpb/dbaas/json/client"
+ psmdbcluster "github.com/percona/pmm/api/managementpb/dbaas/json/client/psmdb_cluster"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+const (
+ psmdbKubernetesClusterName = "api-test-k8s-mongodb-cluster"
+)
+
+//nolint:funlen
+func TestPSMDBClusterServer(t *testing.T) {
+ if pmmapitests.Kubeconfig == "" {
+ t.Skip("Skip tests of PSMDBClusterServer without kubeconfig")
+ }
+ registerKubernetesCluster(t, psmdbKubernetesClusterName, pmmapitests.Kubeconfig)
+
+ t.Run("BasicPSMDBCluster", func(t *testing.T) {
+ paramsFirstPSMDB := psmdbcluster.CreatePSMDBClusterParams{
+ Context: pmmapitests.Context,
+ Body: psmdbcluster.CreatePSMDBClusterBody{
+ KubernetesClusterName: psmdbKubernetesClusterName,
+ Name: "first-psmdb-test",
+ Params: &psmdbcluster.CreatePSMDBClusterParamsBodyParams{
+ ClusterSize: 3,
+ Replicaset: &psmdbcluster.CreatePSMDBClusterParamsBodyParamsReplicaset{
+ ComputeResources: &psmdbcluster.CreatePSMDBClusterParamsBodyParamsReplicasetComputeResources{
+ CPUm: 500,
+ MemoryBytes: "1000000000",
+ },
+ DiskSize: "1000000000",
+ },
+ },
+ },
+ }
+
+ _, err := dbaasClient.Default.PSMDBCluster.CreatePSMDBCluster(¶msFirstPSMDB)
+ assert.NoError(t, err)
+ // Create one more PSMDB Cluster.
+ paramsSecondPSMDB := psmdbcluster.CreatePSMDBClusterParams{
+ Context: pmmapitests.Context,
+ Body: psmdbcluster.CreatePSMDBClusterBody{
+ KubernetesClusterName: psmdbKubernetesClusterName,
+ Name: "second-psmdb-test",
+ Params: &psmdbcluster.CreatePSMDBClusterParamsBodyParams{
+ ClusterSize: 1,
+ Replicaset: &psmdbcluster.CreatePSMDBClusterParamsBodyParamsReplicaset{
+ ComputeResources: &psmdbcluster.CreatePSMDBClusterParamsBodyParamsReplicasetComputeResources{
+ CPUm: 500,
+ MemoryBytes: "1000000000",
+ },
+ DiskSize: "1000000000",
+ },
+ },
+ },
+ }
+ _, err = dbaasClient.Default.PSMDBCluster.CreatePSMDBCluster(¶msSecondPSMDB)
+ assert.NoError(t, err)
+
+ listPSMDBClustersParamsParam := psmdbcluster.ListPSMDBClustersParams{
+ Context: pmmapitests.Context,
+ Body: psmdbcluster.ListPSMDBClustersBody{
+ KubernetesClusterName: psmdbKubernetesClusterName,
+ },
+ }
+ xtraDBClusters, err := dbaasClient.Default.PSMDBCluster.ListPSMDBClusters(&listPSMDBClustersParamsParam)
+ assert.NoError(t, err)
+
+ for _, name := range []string{"first-psmdb-test", "second-psmdb-test"} {
+ foundPSMDB := false
+ for _, psmdb := range xtraDBClusters.Payload.Clusters {
+ if name == psmdb.Name {
+ foundPSMDB = true
+
+ break
+ }
+ }
+ assert.True(t, foundPSMDB, "Cannot find PSMDB with name %s in cluster list", name)
+ }
+
+ paramsUpdatePSMDB := psmdbcluster.UpdatePSMDBClusterParams{
+ Context: pmmapitests.Context,
+ Body: psmdbcluster.UpdatePSMDBClusterBody{
+ KubernetesClusterName: psmdbKubernetesClusterName,
+ Name: "second-psmdb-test",
+ Params: &psmdbcluster.UpdatePSMDBClusterParamsBodyParams{
+ ClusterSize: 2,
+ Replicaset: &psmdbcluster.UpdatePSMDBClusterParamsBodyParamsReplicaset{
+ ComputeResources: &psmdbcluster.UpdatePSMDBClusterParamsBodyParamsReplicasetComputeResources{
+ CPUm: 2,
+ MemoryBytes: "128",
+ },
+ },
+ },
+ },
+ }
+
+ _, err = dbaasClient.Default.PSMDBCluster.UpdatePSMDBCluster(¶msUpdatePSMDB)
+ pmmapitests.AssertAPIErrorf(t, err, 500, codes.Internal, `state is initializing: PSMDB cluster is not ready`)
+
+ for _, psmdb := range xtraDBClusters.Payload.Clusters {
+ if psmdb.Name == "" {
+ continue
+ }
+ deletePSMDBClusterParamsParam := psmdbcluster.DeletePSMDBClusterParams{
+ Context: pmmapitests.Context,
+ Body: psmdbcluster.DeletePSMDBClusterBody{
+ KubernetesClusterName: psmdbKubernetesClusterName,
+ Name: psmdb.Name,
+ },
+ }
+ _, err := dbaasClient.Default.PSMDBCluster.DeletePSMDBCluster(&deletePSMDBClusterParamsParam)
+ assert.NoError(t, err)
+ }
+
+ cluster, err := dbaasClient.Default.PSMDBCluster.GetPSMDBClusterCredentials(&psmdbcluster.GetPSMDBClusterCredentialsParams{
+ Body: psmdbcluster.GetPSMDBClusterCredentialsBody{
+ KubernetesClusterName: psmdbKubernetesClusterName,
+ Name: "second-psmdb-test",
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, cluster.Payload.ConnectionCredentials.Username, "userAdmin")
+ assert.Equal(t, cluster.Payload.ConnectionCredentials.Host, "second-psmdb-test-rs0.default.svc.cluster.local")
+ assert.Equal(t, cluster.Payload.ConnectionCredentials.Port, int32(27017))
+ assert.Equal(t, cluster.Payload.ConnectionCredentials.Replicaset, "rs0")
+ assert.NotEmpty(t, cluster.Payload.ConnectionCredentials.Password)
+
+ t.Skip("Skip restart till better implementation. https://jira.percona.com/browse/PMM-6980")
+ _, err = dbaasClient.Default.PSMDBCluster.RestartPSMDBCluster(&psmdbcluster.RestartPSMDBClusterParams{
+ Body: psmdbcluster.RestartPSMDBClusterBody{
+ KubernetesClusterName: psmdbKubernetesClusterName,
+ Name: "first-psmdb-test",
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ })
+
+ t.Run("CreatePSMDBClusterEmptyName", func(t *testing.T) {
+ paramsPSMDBEmptyName := psmdbcluster.CreatePSMDBClusterParams{
+ Context: pmmapitests.Context,
+ Body: psmdbcluster.CreatePSMDBClusterBody{
+ KubernetesClusterName: psmdbKubernetesClusterName,
+ Name: "",
+ Params: &psmdbcluster.CreatePSMDBClusterParamsBodyParams{
+ ClusterSize: 3,
+ Replicaset: &psmdbcluster.CreatePSMDBClusterParamsBodyParamsReplicaset{
+ ComputeResources: &psmdbcluster.CreatePSMDBClusterParamsBodyParamsReplicasetComputeResources{
+ CPUm: 1,
+ MemoryBytes: "64",
+ },
+ },
+ },
+ },
+ }
+ _, err := dbaasClient.Default.PSMDBCluster.CreatePSMDBCluster(¶msPSMDBEmptyName)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, `invalid field Name: value '' must be a string conforming to regex "^[a-z]([-a-z0-9]*[a-z0-9])?$"`)
+ })
+
+ t.Run("CreatePSMDBClusterInvalidName", func(t *testing.T) {
+ paramsPSMDBInvalidName := psmdbcluster.CreatePSMDBClusterParams{
+ Context: pmmapitests.Context,
+ Body: psmdbcluster.CreatePSMDBClusterBody{
+ KubernetesClusterName: psmdbKubernetesClusterName,
+ Name: "123_asd",
+ Params: &psmdbcluster.CreatePSMDBClusterParamsBodyParams{
+ ClusterSize: 3,
+ Replicaset: &psmdbcluster.CreatePSMDBClusterParamsBodyParamsReplicaset{
+ ComputeResources: &psmdbcluster.CreatePSMDBClusterParamsBodyParamsReplicasetComputeResources{
+ CPUm: 1,
+ MemoryBytes: "64",
+ },
+ },
+ },
+ },
+ }
+ _, err := dbaasClient.Default.PSMDBCluster.CreatePSMDBCluster(¶msPSMDBInvalidName)
+ assert.Error(t, err)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, `invalid field Name: value '123_asd' must be a string conforming to regex "^[a-z]([-a-z0-9]*[a-z0-9])?$"`)
+ })
+
+ t.Run("ListUnknownCluster", func(t *testing.T) {
+ listPSMDBClustersParamsParam := psmdbcluster.ListPSMDBClustersParams{
+ Context: pmmapitests.Context,
+ Body: psmdbcluster.ListPSMDBClustersBody{
+ KubernetesClusterName: "Unknown-kubernetes-cluster-name",
+ },
+ }
+ _, err := dbaasClient.Default.PSMDBCluster.ListPSMDBClusters(&listPSMDBClustersParamsParam)
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, `Kubernetes Cluster with name "Unknown-kubernetes-cluster-name" not found.`)
+ })
+
+ t.Run("RestartUnknownPSMDBCluster", func(t *testing.T) {
+ restartPSMDBClusterParamsParam := psmdbcluster.RestartPSMDBClusterParams{
+ Context: pmmapitests.Context,
+ Body: psmdbcluster.RestartPSMDBClusterBody{
+ KubernetesClusterName: psmdbKubernetesClusterName,
+ Name: "Unknown-psmdb-name",
+ },
+ }
+ _, err := dbaasClient.Default.PSMDBCluster.RestartPSMDBCluster(&restartPSMDBClusterParamsParam)
+ require.Error(t, err)
+ assert.Equal(t, 500, err.(pmmapitests.ErrorResponse).Code())
+ })
+
+ t.Run("DeleteUnknownPSMDBCluster", func(t *testing.T) {
+ deletePSMDBClusterParamsParam := psmdbcluster.DeletePSMDBClusterParams{
+ Context: pmmapitests.Context,
+ Body: psmdbcluster.DeletePSMDBClusterBody{
+ KubernetesClusterName: psmdbKubernetesClusterName,
+ Name: "Unknown-psmdb-name",
+ },
+ }
+ _, err := dbaasClient.Default.PSMDBCluster.DeletePSMDBCluster(&deletePSMDBClusterParamsParam)
+ require.Error(t, err)
+ assert.Equal(t, 500, err.(pmmapitests.ErrorResponse).Code())
+ })
+
+ t.Run("SuspendResumeCluster", func(t *testing.T) {
+ paramsUpdatePSMDB := psmdbcluster.UpdatePSMDBClusterParams{
+ Context: pmmapitests.Context,
+ Body: psmdbcluster.UpdatePSMDBClusterBody{
+ KubernetesClusterName: psmdbKubernetesClusterName,
+ Name: "second-psmdb-test",
+ Params: &psmdbcluster.UpdatePSMDBClusterParamsBodyParams{
+ Suspend: true,
+ Resume: true,
+ },
+ },
+ }
+ _, err := dbaasClient.Default.PSMDBCluster.UpdatePSMDBCluster(¶msUpdatePSMDB)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, `resume and suspend cannot be set together`)
+ })
+
+ t.Run("GetPSMDBClusterResources", func(t *testing.T) {
+ paramsPSMDBClusterResources := psmdbcluster.GetPSMDBClusterResourcesParams{
+ Context: pmmapitests.Context,
+ Body: psmdbcluster.GetPSMDBClusterResourcesBody{
+ Params: &psmdbcluster.GetPSMDBClusterResourcesParamsBodyParams{
+ ClusterSize: 4,
+ Replicaset: &psmdbcluster.GetPSMDBClusterResourcesParamsBodyParamsReplicaset{
+ ComputeResources: &psmdbcluster.GetPSMDBClusterResourcesParamsBodyParamsReplicasetComputeResources{
+ CPUm: 2000,
+ MemoryBytes: "2000000000",
+ },
+ },
+ },
+ },
+ }
+ resources, err := dbaasClient.Default.PSMDBCluster.GetPSMDBClusterResources(¶msPSMDBClusterResources)
+ assert.NoError(t, err)
+ assert.Equal(t, resources.Payload.Expected.MemoryBytes, 16000000000)
+ assert.Equal(t, resources.Payload.Expected.CPUm, 16000)
+ assert.Equal(t, resources.Payload.Expected.DiskSize, 14000000000)
+ })
+}
diff --git a/api-tests/management/dbaas/xtra_db_cluster_test.go b/api-tests/management/dbaas/xtra_db_cluster_test.go
new file mode 100644
index 0000000000..e912e639b0
--- /dev/null
+++ b/api-tests/management/dbaas/xtra_db_cluster_test.go
@@ -0,0 +1,320 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package dbaas
+
+import (
+ "testing"
+
+ dbaasClient "github.com/percona/pmm/api/managementpb/dbaas/json/client"
+ "github.com/percona/pmm/api/managementpb/dbaas/json/client/xtra_db_cluster"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+const (
+ kubernetesClusterName = "api-test-k8s-cluster"
+)
+
+//nolint:funlen
+func TestXtraDBClusterServer(t *testing.T) {
+ if pmmapitests.Kubeconfig == "" {
+ t.Skip("Skip tests of XtraDBClusterServer without kubeconfig")
+ }
+ registerKubernetesCluster(t, kubernetesClusterName, pmmapitests.Kubeconfig)
+
+ t.Run("BasicXtraDBCluster", func(t *testing.T) {
+ paramsFirstPXC := xtra_db_cluster.CreateXtraDBClusterParams{
+ Context: pmmapitests.Context,
+ Body: xtra_db_cluster.CreateXtraDBClusterBody{
+ KubernetesClusterName: kubernetesClusterName,
+ Name: "first-pxc-test",
+ Params: &xtra_db_cluster.CreateXtraDBClusterParamsBodyParams{
+ ClusterSize: 3,
+ Haproxy: &xtra_db_cluster.CreateXtraDBClusterParamsBodyParamsHaproxy{
+ ComputeResources: &xtra_db_cluster.CreateXtraDBClusterParamsBodyParamsHaproxyComputeResources{
+ CPUm: 500,
+ MemoryBytes: "1000000000",
+ },
+ },
+ Pxc: &xtra_db_cluster.CreateXtraDBClusterParamsBodyParamsPxc{
+ ComputeResources: &xtra_db_cluster.CreateXtraDBClusterParamsBodyParamsPxcComputeResources{
+ CPUm: 1,
+ MemoryBytes: "64",
+ },
+ DiskSize: "1000000000",
+ },
+ },
+ },
+ }
+
+ _, err := dbaasClient.Default.XtraDBCluster.CreateXtraDBCluster(¶msFirstPXC)
+ assert.NoError(t, err)
+
+ // Create one more XtraDB Cluster.
+ paramsSecondPXC := xtra_db_cluster.CreateXtraDBClusterParams{
+ Context: pmmapitests.Context,
+ Body: xtra_db_cluster.CreateXtraDBClusterBody{
+ KubernetesClusterName: kubernetesClusterName,
+ Name: "second-pxc-test",
+ Params: &xtra_db_cluster.CreateXtraDBClusterParamsBodyParams{
+ ClusterSize: 1,
+ Proxysql: &xtra_db_cluster.CreateXtraDBClusterParamsBodyParamsProxysql{
+ ComputeResources: &xtra_db_cluster.CreateXtraDBClusterParamsBodyParamsProxysqlComputeResources{
+ CPUm: 500,
+ MemoryBytes: "1000000000",
+ },
+ DiskSize: "1000000000",
+ },
+ Pxc: &xtra_db_cluster.CreateXtraDBClusterParamsBodyParamsPxc{
+ ComputeResources: &xtra_db_cluster.CreateXtraDBClusterParamsBodyParamsPxcComputeResources{
+ CPUm: 1,
+ MemoryBytes: "64",
+ },
+ DiskSize: "1000000000",
+ },
+ },
+ },
+ }
+ _, err = dbaasClient.Default.XtraDBCluster.CreateXtraDBCluster(¶msSecondPXC)
+ assert.NoError(t, err)
+
+ listXtraDBClustersParamsParam := xtra_db_cluster.ListXtraDBClustersParams{
+ Context: pmmapitests.Context,
+ Body: xtra_db_cluster.ListXtraDBClustersBody{
+ KubernetesClusterName: kubernetesClusterName,
+ },
+ }
+ xtraDBClusters, err := dbaasClient.Default.XtraDBCluster.ListXtraDBClusters(&listXtraDBClustersParamsParam)
+ assert.NoError(t, err)
+
+ for _, name := range []string{"first-pxc-test", "second-pxc-test"} {
+ foundPXC := false
+ for _, pxc := range xtraDBClusters.Payload.Clusters {
+ if name == pxc.Name {
+ foundPXC = true
+
+ break
+ }
+ }
+ assert.True(t, foundPXC, "Cannot find PXC with name %s in cluster list", name)
+ }
+
+ getXtraDBClusterParamsParam := xtra_db_cluster.GetXtraDBClusterCredentialsParams{
+ Context: pmmapitests.Context,
+ Body: xtra_db_cluster.GetXtraDBClusterCredentialsBody{
+ KubernetesClusterName: kubernetesClusterName,
+ Name: "first-pxc-test",
+ },
+ }
+ xtraDBCluster, err := dbaasClient.Default.XtraDBCluster.GetXtraDBClusterCredentials(&getXtraDBClusterParamsParam)
+ assert.NoError(t, err)
+ assert.Equal(t, xtraDBCluster.Payload.ConnectionCredentials.Username, "root")
+ assert.Equal(t, xtraDBCluster.Payload.ConnectionCredentials.Host, "first-pxc-test-haproxy")
+ assert.Equal(t, xtraDBCluster.Payload.ConnectionCredentials.Port, int32(3306))
+ assert.NotEmpty(t, xtraDBCluster.Payload.ConnectionCredentials.Password)
+
+ paramsUpdatePXC := xtra_db_cluster.UpdateXtraDBClusterParams{
+ Context: pmmapitests.Context,
+ Body: xtra_db_cluster.UpdateXtraDBClusterBody{
+ KubernetesClusterName: kubernetesClusterName,
+ Name: "second-pxc-test",
+ Params: &xtra_db_cluster.UpdateXtraDBClusterParamsBodyParams{
+ ClusterSize: 2,
+ Proxysql: &xtra_db_cluster.UpdateXtraDBClusterParamsBodyParamsProxysql{
+ ComputeResources: &xtra_db_cluster.UpdateXtraDBClusterParamsBodyParamsProxysqlComputeResources{
+ CPUm: 2,
+ MemoryBytes: "128",
+ },
+ },
+ Pxc: &xtra_db_cluster.UpdateXtraDBClusterParamsBodyParamsPxc{
+ ComputeResources: &xtra_db_cluster.UpdateXtraDBClusterParamsBodyParamsPxcComputeResources{
+ CPUm: 2,
+ MemoryBytes: "128",
+ },
+ },
+ },
+ },
+ }
+
+ _, err = dbaasClient.Default.XtraDBCluster.UpdateXtraDBCluster(¶msUpdatePXC)
+ pmmapitests.AssertAPIErrorf(t, err, 500, codes.Internal, `state is Error: XtraDB cluster is not ready`)
+
+ for _, pxc := range xtraDBClusters.Payload.Clusters {
+ if pxc.Name == "" {
+ continue
+ }
+ deleteXtraDBClusterParamsParam := xtra_db_cluster.DeleteXtraDBClusterParams{
+ Context: pmmapitests.Context,
+ Body: xtra_db_cluster.DeleteXtraDBClusterBody{
+ KubernetesClusterName: kubernetesClusterName,
+ Name: pxc.Name,
+ },
+ }
+ _, err := dbaasClient.Default.XtraDBCluster.DeleteXtraDBCluster(&deleteXtraDBClusterParamsParam)
+ assert.NoError(t, err)
+ }
+
+ t.Skip("Skip restart till better implementation. https://jira.percona.com/browse/PMM-6980")
+ restartXtraDBClusterParamsParam := xtra_db_cluster.RestartXtraDBClusterParams{
+ Context: pmmapitests.Context,
+ Body: xtra_db_cluster.RestartXtraDBClusterBody{
+ KubernetesClusterName: kubernetesClusterName,
+ Name: "first-pxc-test",
+ },
+ }
+ _, err = dbaasClient.Default.XtraDBCluster.RestartXtraDBCluster(&restartXtraDBClusterParamsParam)
+ assert.NoError(t, err)
+ })
+
+ t.Run("CreateXtraDBClusterEmptyName", func(t *testing.T) {
+ paramsPXCEmptyName := xtra_db_cluster.CreateXtraDBClusterParams{
+ Context: pmmapitests.Context,
+ Body: xtra_db_cluster.CreateXtraDBClusterBody{
+ KubernetesClusterName: kubernetesClusterName,
+ Name: "",
+ Params: &xtra_db_cluster.CreateXtraDBClusterParamsBodyParams{
+ ClusterSize: 1,
+ Proxysql: &xtra_db_cluster.CreateXtraDBClusterParamsBodyParamsProxysql{
+ ComputeResources: &xtra_db_cluster.CreateXtraDBClusterParamsBodyParamsProxysqlComputeResources{
+ CPUm: 1,
+ MemoryBytes: "64",
+ },
+ },
+ Pxc: &xtra_db_cluster.CreateXtraDBClusterParamsBodyParamsPxc{
+ ComputeResources: &xtra_db_cluster.CreateXtraDBClusterParamsBodyParamsPxcComputeResources{
+ CPUm: 1,
+ MemoryBytes: "64",
+ },
+ },
+ },
+ },
+ }
+ _, err := dbaasClient.Default.XtraDBCluster.CreateXtraDBCluster(¶msPXCEmptyName)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, `invalid field Name: value '' must be a string conforming to regex "^[a-z]([-a-z0-9]*[a-z0-9])?$"`)
+ })
+
+ t.Run("CreateXtraDBClusterInvalidName", func(t *testing.T) {
+ paramsPXCInvalidName := xtra_db_cluster.CreateXtraDBClusterParams{
+ Context: pmmapitests.Context,
+ Body: xtra_db_cluster.CreateXtraDBClusterBody{
+ KubernetesClusterName: kubernetesClusterName,
+ Name: "123_asd",
+ Params: &xtra_db_cluster.CreateXtraDBClusterParamsBodyParams{
+ ClusterSize: 1,
+ Proxysql: &xtra_db_cluster.CreateXtraDBClusterParamsBodyParamsProxysql{
+ ComputeResources: &xtra_db_cluster.CreateXtraDBClusterParamsBodyParamsProxysqlComputeResources{
+ CPUm: 1,
+ MemoryBytes: "64",
+ },
+ },
+ Pxc: &xtra_db_cluster.CreateXtraDBClusterParamsBodyParamsPxc{
+ ComputeResources: &xtra_db_cluster.CreateXtraDBClusterParamsBodyParamsPxcComputeResources{
+ CPUm: 1,
+ MemoryBytes: "64",
+ },
+ },
+ },
+ },
+ }
+ _, err := dbaasClient.Default.XtraDBCluster.CreateXtraDBCluster(¶msPXCInvalidName)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, `invalid field Name: value '123_asd' must be a string conforming to regex "^[a-z]([-a-z0-9]*[a-z0-9])?$"`)
+ })
+
+ t.Run("ListUnknownCluster", func(t *testing.T) {
+ listXtraDBClustersParamsParam := xtra_db_cluster.ListXtraDBClustersParams{
+ Context: pmmapitests.Context,
+ Body: xtra_db_cluster.ListXtraDBClustersBody{
+ KubernetesClusterName: "Unknown-kubernetes-cluster-name",
+ },
+ }
+ _, err := dbaasClient.Default.XtraDBCluster.ListXtraDBClusters(&listXtraDBClustersParamsParam)
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, `Kubernetes Cluster with name "Unknown-kubernetes-cluster-name" not found.`)
+ })
+
+ t.Run("RestartUnknownXtraDBCluster", func(t *testing.T) {
+ restartXtraDBClusterParamsParam := xtra_db_cluster.RestartXtraDBClusterParams{
+ Context: pmmapitests.Context,
+ Body: xtra_db_cluster.RestartXtraDBClusterBody{
+ KubernetesClusterName: kubernetesClusterName,
+ Name: "Unknown-pxc-name",
+ },
+ }
+ _, err := dbaasClient.Default.XtraDBCluster.RestartXtraDBCluster(&restartXtraDBClusterParamsParam)
+ require.Error(t, err)
+ assert.Equal(t, 500, err.(pmmapitests.ErrorResponse).Code())
+ })
+
+ t.Run("DeleteUnknownXtraDBCluster", func(t *testing.T) {
+ deleteXtraDBClusterParamsParam := xtra_db_cluster.DeleteXtraDBClusterParams{
+ Context: pmmapitests.Context,
+ Body: xtra_db_cluster.DeleteXtraDBClusterBody{
+ KubernetesClusterName: kubernetesClusterName,
+ Name: "Unknown-pxc-name",
+ },
+ }
+ _, err := dbaasClient.Default.XtraDBCluster.DeleteXtraDBCluster(&deleteXtraDBClusterParamsParam)
+ require.Error(t, err)
+ assert.Equal(t, 500, err.(pmmapitests.ErrorResponse).Code())
+ })
+
+ t.Run("SuspendResumeCluster", func(t *testing.T) {
+ paramsUpdatePXC := xtra_db_cluster.UpdateXtraDBClusterParams{
+ Context: pmmapitests.Context,
+ Body: xtra_db_cluster.UpdateXtraDBClusterBody{
+ KubernetesClusterName: kubernetesClusterName,
+ Name: "second-pxc-test",
+ Params: &xtra_db_cluster.UpdateXtraDBClusterParamsBodyParams{
+ Suspend: true,
+ Resume: true,
+ },
+ },
+ }
+ _, err := dbaasClient.Default.XtraDBCluster.UpdateXtraDBCluster(¶msUpdatePXC)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, `resume and suspend cannot be set together`)
+ })
+
+ t.Run("GetXtraDBClusterResources", func(t *testing.T) {
+ paramsXtraDBClusterResources := xtra_db_cluster.GetXtraDBClusterResourcesParams{
+ Context: pmmapitests.Context,
+ Body: xtra_db_cluster.GetXtraDBClusterResourcesBody{
+ Params: &xtra_db_cluster.GetXtraDBClusterResourcesParamsBodyParams{
+ ClusterSize: 1,
+ Proxysql: &xtra_db_cluster.GetXtraDBClusterResourcesParamsBodyParamsProxysql{
+ ComputeResources: &xtra_db_cluster.GetXtraDBClusterResourcesParamsBodyParamsProxysqlComputeResources{
+ CPUm: 1000,
+ MemoryBytes: "1000000000",
+ },
+ },
+ Pxc: &xtra_db_cluster.GetXtraDBClusterResourcesParamsBodyParamsPxc{
+ ComputeResources: &xtra_db_cluster.GetXtraDBClusterResourcesParamsBodyParamsPxcComputeResources{
+ CPUm: 1000,
+ MemoryBytes: "1000000000",
+ },
+ },
+ },
+ },
+ }
+ resources, err := dbaasClient.Default.XtraDBCluster.GetXtraDBClusterResources(¶msXtraDBClusterResources)
+ assert.NoError(t, err)
+ assert.Equal(t, resources.Payload.Expected.MemoryBytes, 2000000000)
+ assert.Equal(t, resources.Payload.Expected.CPUm, 2000)
+ assert.Equal(t, resources.Payload.Expected.DiskSize, 2000000000)
+ })
+}
diff --git a/api-tests/management/external_test.go b/api-tests/management/external_test.go
new file mode 100644
index 0000000000..0061a86cb3
--- /dev/null
+++ b/api-tests/management/external_test.go
@@ -0,0 +1,543 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package management
+
+import (
+ "testing"
+
+ "github.com/AlekSi/pointer"
+ inventoryClient "github.com/percona/pmm/api/inventorypb/json/client"
+ "github.com/percona/pmm/api/inventorypb/json/client/agents"
+ "github.com/percona/pmm/api/inventorypb/json/client/nodes"
+ "github.com/percona/pmm/api/inventorypb/json/client/services"
+ "github.com/percona/pmm/api/managementpb/json/client"
+ "github.com/percona/pmm/api/managementpb/json/client/external"
+ "github.com/percona/pmm/api/managementpb/json/client/service"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestAddExternal(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "genericNode-for-basic-name")
+ genericNode := pmmapitests.AddGenericNode(t, nodeName)
+ nodeID := genericNode.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ serviceName := pmmapitests.TestString(t, "service-for-basic-name")
+
+ params := &external.AddExternalParams{
+ Context: pmmapitests.Context,
+ Body: external.AddExternalBody{
+ RunsOnNodeID: nodeID,
+ ServiceName: serviceName,
+ ListenPort: 9104,
+ NodeID: nodeID,
+ Group: "", // empty group - pmm-admin does not support group.
+ SkipConnectionCheck: true,
+ },
+ }
+ addExternalOK, err := client.Default.External.AddExternal(params)
+ require.NoError(t, err)
+ require.NotNil(t, addExternalOK)
+ require.NotNil(t, addExternalOK.Payload.Service)
+ serviceID := addExternalOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ External: &services.GetServiceOKBodyExternal{
+ ServiceID: serviceID,
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Group: "external",
+ },
+ }, *serviceOK.Payload)
+
+ // Check that external exporter is added by default.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, agents.ListAgentsOKBody{
+ ExternalExporter: []*agents.ExternalExporterItems0{
+ {
+ AgentID: listAgents.Payload.ExternalExporter[0].AgentID,
+ ServiceID: serviceID,
+ ListenPort: 9104,
+ RunsOnNodeID: nodeID,
+ Scheme: "http",
+ MetricsPath: "/metrics",
+ },
+ },
+ }, *listAgents.Payload)
+ defer removeAllAgentsInList(t, listAgents)
+ })
+
+ t.Run("With labels", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-for-all-fields-name")
+ genericNode := pmmapitests.AddGenericNode(t, nodeName)
+ nodeID := genericNode.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ serviceName := pmmapitests.TestString(t, "service-for-all-fields-name")
+
+ params := &external.AddExternalParams{
+ Context: pmmapitests.Context,
+ Body: external.AddExternalBody{
+ RunsOnNodeID: nodeID,
+ ServiceName: serviceName,
+ Username: "username",
+ Password: "password",
+ Scheme: "https",
+ MetricsPath: "/metrics-path",
+ ListenPort: 9250,
+ NodeID: nodeID,
+ Environment: "some-environment",
+ Cluster: "cluster-name",
+ ReplicationSet: "replication-set",
+ CustomLabels: map[string]string{"bar": "foo"},
+ Group: "redis",
+ SkipConnectionCheck: true,
+ },
+ }
+ addExternalOK, err := client.Default.External.AddExternal(params)
+ require.NoError(t, err)
+ require.NotNil(t, addExternalOK)
+ require.NotNil(t, addExternalOK.Payload.Service)
+ serviceID := addExternalOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+ defer removeServiceAgents(t, serviceID)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ External: &services.GetServiceOKBodyExternal{
+ ServiceID: serviceID,
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Environment: "some-environment",
+ Cluster: "cluster-name",
+ ReplicationSet: "replication-set",
+ CustomLabels: map[string]string{"bar": "foo"},
+
+ Group: "redis",
+ },
+ }, *serviceOK.Payload)
+ })
+
+ t.Run("OnRemoteNode", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "genericNode-for-basic-name")
+
+ serviceName := pmmapitests.TestString(t, "service-for-basic-name")
+
+ params := &external.AddExternalParams{
+ Context: pmmapitests.Context,
+ Body: external.AddExternalBody{
+ AddNode: &external.AddExternalParamsBodyAddNode{
+ NodeType: pointer.ToString(external.AddExternalParamsBodyAddNodeNodeTypeREMOTENODE),
+ NodeName: nodeName,
+ MachineID: "/machine-id/",
+ Distro: "linux",
+ Region: "us-west2",
+ CustomLabels: map[string]string{"foo": "bar-for-node"},
+ },
+ Address: "localhost",
+ ServiceName: serviceName,
+ ListenPort: 9104,
+ Group: "", // empty group - pmm-admin does not support group.
+ SkipConnectionCheck: true,
+ },
+ }
+ addExternalOK, err := client.Default.External.AddExternal(params)
+ require.NoError(t, err)
+ require.NotNil(t, addExternalOK)
+ require.NotNil(t, addExternalOK.Payload.Service)
+ nodeID := addExternalOK.Payload.Service.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ serviceID := addExternalOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ // Check that node is created and its fields.
+ node, err := inventoryClient.Default.Nodes.GetNode(&nodes.GetNodeParams{
+ Body: nodes.GetNodeBody{
+ NodeID: nodeID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, node)
+ assert.Equal(t, nodes.GetNodeOKBody{
+ Remote: &nodes.GetNodeOKBodyRemote{
+ NodeID: nodeID,
+ NodeName: nodeName,
+ Address: "localhost",
+ Region: "us-west2",
+ CustomLabels: map[string]string{"foo": "bar-for-node"},
+ },
+ }, *node.Payload)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ External: &services.GetServiceOKBodyExternal{
+ ServiceID: serviceID,
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Group: "external",
+ },
+ }, *serviceOK.Payload)
+
+ // Check that external exporter is added.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, agents.ListAgentsOKBody{
+ ExternalExporter: []*agents.ExternalExporterItems0{
+ {
+ AgentID: listAgents.Payload.ExternalExporter[0].AgentID,
+ ServiceID: serviceID,
+ ListenPort: 9104,
+ RunsOnNodeID: nodeID,
+ Scheme: "http",
+ MetricsPath: "/metrics",
+ },
+ },
+ }, *listAgents.Payload)
+ defer removeAllAgentsInList(t, listAgents)
+ })
+
+ t.Run("With the same name", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-for-the-same-name")
+ genericNode := pmmapitests.AddGenericNode(t, nodeName)
+ nodeID := genericNode.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ serviceName := pmmapitests.TestString(t, "service-for-the-same-name")
+
+ params := &external.AddExternalParams{
+ Context: pmmapitests.Context,
+ Body: external.AddExternalBody{
+ NodeID: nodeID,
+ RunsOnNodeID: nodeID,
+ ServiceName: serviceName,
+ ListenPort: 9250,
+ Group: "external",
+ SkipConnectionCheck: true,
+ },
+ }
+ addExternalOK, err := client.Default.External.AddExternal(params)
+ require.NoError(t, err)
+ require.NotNil(t, addExternalOK)
+ require.NotNil(t, addExternalOK.Payload.Service)
+ serviceID := addExternalOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+ defer removeServiceAgents(t, serviceID)
+
+ params = &external.AddExternalParams{
+ Context: pmmapitests.Context,
+ Body: external.AddExternalBody{
+ NodeID: nodeID,
+ RunsOnNodeID: nodeID,
+ ServiceName: serviceName,
+ ListenPort: 9260,
+ Group: "external",
+ SkipConnectionCheck: true,
+ },
+ }
+ addExternalOK, err = client.Default.External.AddExternal(params)
+ require.Nil(t, addExternalOK)
+ pmmapitests.AssertAPIErrorf(t, err, 409, codes.AlreadyExists, `Service with name %q already exists.`, serviceName)
+ })
+
+ t.Run("Empty Service Name", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ genericNode := pmmapitests.AddGenericNode(t, nodeName)
+ nodeID := genericNode.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ params := &external.AddExternalParams{
+ Context: pmmapitests.Context,
+ Body: external.AddExternalBody{
+ NodeID: nodeID,
+ RunsOnNodeID: nodeID,
+ Group: "external",
+ SkipConnectionCheck: true,
+ },
+ }
+ addExternalOK, err := client.Default.External.AddExternal(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field ServiceName: value '' must not be an empty string")
+ assert.Nil(t, addExternalOK)
+ })
+
+ t.Run("Empty ListenPort", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ genericNode := pmmapitests.AddGenericNode(t, nodeName)
+ nodeID := genericNode.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ serviceName := pmmapitests.TestString(t, "service-name")
+ params := &external.AddExternalParams{
+ Context: pmmapitests.Context,
+ Body: external.AddExternalBody{
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ RunsOnNodeID: nodeID,
+ Group: "external",
+ SkipConnectionCheck: true,
+ },
+ }
+ addExternalOK, err := client.Default.External.AddExternal(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field ListenPort: value '0' must be greater than '0'")
+ assert.Nil(t, addExternalOK)
+ })
+
+ t.Run("Empty Node ID", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ genericNode := pmmapitests.AddGenericNode(t, nodeName)
+ nodeID := genericNode.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ serviceName := pmmapitests.TestString(t, "service-name")
+ params := &external.AddExternalParams{
+ Context: pmmapitests.Context,
+ Body: external.AddExternalBody{
+ RunsOnNodeID: nodeID,
+ ServiceName: serviceName,
+ ListenPort: 12345,
+ Group: "external",
+ SkipConnectionCheck: true,
+ },
+ }
+ addExternalOK, err := client.Default.External.AddExternal(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "runs_on_node_id and node_id should be specified together.")
+ assert.Nil(t, addExternalOK)
+ })
+
+ t.Run("Empty Runs On Node ID", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ genericNode := pmmapitests.AddGenericNode(t, nodeName)
+ nodeID := genericNode.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ serviceName := pmmapitests.TestString(t, "service-name")
+ params := &external.AddExternalParams{
+ Context: pmmapitests.Context,
+ Body: external.AddExternalBody{
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ ListenPort: 12345,
+ Group: "external",
+ SkipConnectionCheck: true,
+ },
+ }
+ addExternalOK, err := client.Default.External.AddExternal(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "runs_on_node_id and node_id should be specified together.")
+ assert.Nil(t, addExternalOK)
+ })
+
+ t.Run("Empty Address for Add Node", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ genericNode := pmmapitests.AddGenericNode(t, nodeName)
+ nodeID := genericNode.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ serviceName := pmmapitests.TestString(t, "service-name")
+ params := &external.AddExternalParams{
+ Context: pmmapitests.Context,
+ Body: external.AddExternalBody{
+ AddNode: &external.AddExternalParamsBodyAddNode{
+ NodeType: pointer.ToString(external.AddExternalParamsBodyAddNodeNodeTypeREMOTENODE),
+ NodeName: "external-serverless",
+ },
+ ServiceName: serviceName,
+ ListenPort: 12345,
+ Group: "external",
+ SkipConnectionCheck: true,
+ },
+ }
+ addExternalOK, err := client.Default.External.AddExternal(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "address can't be empty for add node request.")
+ assert.Nil(t, addExternalOK)
+ })
+}
+
+func TestRemoveExternal(t *testing.T) {
+ addExternal := func(t *testing.T, serviceName, nodeName string) (nodeID string, serviceID string) {
+ t.Helper()
+ genericNode := pmmapitests.AddGenericNode(t, nodeName)
+ nodeID = genericNode.NodeID
+
+ params := &external.AddExternalParams{
+ Context: pmmapitests.Context,
+ Body: external.AddExternalBody{
+ NodeID: nodeID,
+ RunsOnNodeID: nodeID,
+ ServiceName: serviceName,
+ Username: "username",
+ Password: "password",
+ ListenPort: 12345,
+ Group: "external",
+ SkipConnectionCheck: true,
+ },
+ }
+ addExternalOK, err := client.Default.External.AddExternal(params)
+ require.NoError(t, err)
+ require.NotNil(t, addExternalOK)
+ require.NotNil(t, addExternalOK.Payload.Service)
+ serviceID = addExternalOK.Payload.Service.ServiceID
+ return
+ }
+
+ t.Run("By name", func(t *testing.T) {
+ serviceName := pmmapitests.TestString(t, "service-remove-by-name")
+ nodeName := pmmapitests.TestString(t, "node-remove-by-name")
+ nodeID, serviceID := addExternal(t, serviceName, nodeName)
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{
+ Body: service.RemoveServiceBody{
+ ServiceName: serviceName,
+ ServiceType: pointer.ToString(service.RemoveServiceBodyServiceTypeEXTERNALSERVICE),
+ },
+ Context: pmmapitests.Context,
+ })
+ noError := assert.NoError(t, err)
+ notNil := assert.NotNil(t, removeServiceOK)
+ if !noError || !notNil {
+ defer pmmapitests.RemoveServices(t, serviceID)
+ }
+
+ // Check that the service removed with agents.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Service with ID %q not found.", serviceID)
+ assert.Nil(t, listAgents)
+ })
+
+ t.Run("By ID", func(t *testing.T) {
+ serviceName := pmmapitests.TestString(t, "service-remove-by-id")
+ nodeName := pmmapitests.TestString(t, "node-remove-by-id")
+ nodeID, serviceID := addExternal(t, serviceName, nodeName)
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{
+ Body: service.RemoveServiceBody{
+ ServiceID: serviceID,
+ ServiceType: pointer.ToString(service.RemoveServiceBodyServiceTypeEXTERNALSERVICE),
+ },
+ Context: pmmapitests.Context,
+ })
+ noError := assert.NoError(t, err)
+ notNil := assert.NotNil(t, removeServiceOK)
+ if !noError || !notNil {
+ defer pmmapitests.RemoveServices(t, serviceID)
+ }
+
+ // Check that the service removed with agents.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Service with ID %q not found.", serviceID)
+ assert.Nil(t, listAgents)
+ })
+
+ t.Run("Both params", func(t *testing.T) {
+ serviceName := pmmapitests.TestString(t, "service-remove-both-params")
+ nodeName := pmmapitests.TestString(t, "node-remove-both-params")
+ nodeID, serviceID := addExternal(t, serviceName, nodeName)
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{
+ Body: service.RemoveServiceBody{
+ ServiceID: serviceID,
+ ServiceName: serviceName,
+ ServiceType: pointer.ToString(service.RemoveServiceBodyServiceTypeEXTERNALSERVICE),
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.Nil(t, removeServiceOK)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "service_id or service_name expected; not both")
+ })
+
+ t.Run("Wrong type", func(t *testing.T) {
+ serviceName := pmmapitests.TestString(t, "service-remove-wrong-type")
+ nodeName := pmmapitests.TestString(t, "node-remove-wrong-type")
+ nodeID, serviceID := addExternal(t, serviceName, nodeName)
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{
+ Body: service.RemoveServiceBody{
+ ServiceID: serviceID,
+ ServiceType: pointer.ToString(service.RemoveServiceBodyServiceTypePOSTGRESQLSERVICE),
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.Nil(t, removeServiceOK)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "wrong service type")
+ })
+
+ t.Run("No params", func(t *testing.T) {
+ removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{
+ Body: service.RemoveServiceBody{},
+ Context: pmmapitests.Context,
+ })
+ assert.Nil(t, removeServiceOK)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "service_id or service_name expected")
+ })
+}
diff --git a/api-tests/management/haproxy_test.go b/api-tests/management/haproxy_test.go
new file mode 100644
index 0000000000..1c9aca82c6
--- /dev/null
+++ b/api-tests/management/haproxy_test.go
@@ -0,0 +1,505 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package management
+
+import (
+ "testing"
+
+ "github.com/AlekSi/pointer"
+ inventoryClient "github.com/percona/pmm/api/inventorypb/json/client"
+ "github.com/percona/pmm/api/inventorypb/json/client/agents"
+ "github.com/percona/pmm/api/inventorypb/json/client/nodes"
+ "github.com/percona/pmm/api/inventorypb/json/client/services"
+ "github.com/percona/pmm/api/managementpb/json/client"
+ "github.com/percona/pmm/api/managementpb/json/client/ha_proxy"
+ "github.com/percona/pmm/api/managementpb/json/client/node"
+ "github.com/percona/pmm/api/managementpb/json/client/service"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestAddHAProxy(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "genericNode-for-basic-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-for-basic-name")
+
+ params := &ha_proxy.AddHAProxyParams{
+ Context: pmmapitests.Context,
+ Body: ha_proxy.AddHAProxyBody{
+ ServiceName: serviceName,
+ ListenPort: 8404,
+ NodeID: nodeID,
+ SkipConnectionCheck: true,
+ },
+ }
+ addHAProxyOK, err := client.Default.HAProxy.AddHAProxy(params)
+ require.NoError(t, err)
+ require.NotNil(t, addHAProxyOK)
+ require.NotNil(t, addHAProxyOK.Payload.Service)
+ serviceID := addHAProxyOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ Haproxy: &services.GetServiceOKBodyHaproxy{
+ ServiceID: serviceID,
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ },
+ }, *serviceOK.Payload)
+
+ // Check that external exporter is added by default.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, agents.ListAgentsOKBody{
+ ExternalExporter: []*agents.ExternalExporterItems0{
+ {
+ AgentID: listAgents.Payload.ExternalExporter[0].AgentID,
+ ServiceID: serviceID,
+ ListenPort: 8404,
+ RunsOnNodeID: nodeID,
+ Scheme: "http",
+ MetricsPath: "/metrics",
+ PushMetricsEnabled: true,
+ },
+ },
+ }, *listAgents.Payload)
+ defer removeAllAgentsInList(t, listAgents)
+ })
+
+ t.Run("With labels", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "genericNode-for-basic-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-for-all-fields-name")
+
+ params := &ha_proxy.AddHAProxyParams{
+ Context: pmmapitests.Context,
+ Body: ha_proxy.AddHAProxyBody{
+ ServiceName: serviceName,
+ Username: "username",
+ Password: "password",
+ Scheme: "https",
+ MetricsPath: "/metrics-path",
+ ListenPort: 9250,
+ NodeID: nodeID,
+ Environment: "some-environment",
+ Cluster: "cluster-name",
+ ReplicationSet: "replication-set",
+ CustomLabels: map[string]string{"bar": "foo"},
+ SkipConnectionCheck: true,
+ },
+ }
+ addHAProxyOK, err := client.Default.HAProxy.AddHAProxy(params)
+ require.NoError(t, err)
+ require.NotNil(t, addHAProxyOK)
+ require.NotNil(t, addHAProxyOK.Payload.Service)
+ serviceID := addHAProxyOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+ defer removeServiceAgents(t, serviceID)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ Haproxy: &services.GetServiceOKBodyHaproxy{
+ ServiceID: serviceID,
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Environment: "some-environment",
+ Cluster: "cluster-name",
+ ReplicationSet: "replication-set",
+ CustomLabels: map[string]string{"bar": "foo"},
+ },
+ }, *serviceOK.Payload)
+ })
+
+ t.Run("OnRemoteNode", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "genericNode-for-basic-name")
+
+ serviceName := pmmapitests.TestString(t, "service-for-basic-name")
+
+ params := &ha_proxy.AddHAProxyParams{
+ Context: pmmapitests.Context,
+ Body: ha_proxy.AddHAProxyBody{
+ AddNode: &ha_proxy.AddHAProxyParamsBodyAddNode{
+ NodeType: pointer.ToString(ha_proxy.AddHAProxyParamsBodyAddNodeNodeTypeREMOTENODE),
+ NodeName: nodeName,
+ MachineID: "/machine-id/",
+ Distro: "linux",
+ Region: "us-west2",
+ CustomLabels: map[string]string{"foo": "bar-for-node"},
+ },
+ Address: "localhost",
+ ServiceName: serviceName,
+ ListenPort: 8404,
+ SkipConnectionCheck: true,
+ },
+ }
+ addHAProxyOK, err := client.Default.HAProxy.AddHAProxy(params)
+ require.NoError(t, err)
+ require.NotNil(t, addHAProxyOK)
+ require.NotNil(t, addHAProxyOK.Payload.Service)
+ nodeID := addHAProxyOK.Payload.Service.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ serviceID := addHAProxyOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ // Check that node is created and its fields.
+ node, err := inventoryClient.Default.Nodes.GetNode(&nodes.GetNodeParams{
+ Body: nodes.GetNodeBody{
+ NodeID: nodeID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, node)
+ assert.Equal(t, nodes.GetNodeOKBody{
+ Remote: &nodes.GetNodeOKBodyRemote{
+ NodeID: nodeID,
+ NodeName: nodeName,
+ Address: "localhost",
+ Region: "us-west2",
+ CustomLabels: map[string]string{"foo": "bar-for-node"},
+ },
+ }, *node.Payload)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ Haproxy: &services.GetServiceOKBodyHaproxy{
+ ServiceID: serviceID,
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ },
+ }, *serviceOK.Payload)
+
+ // Check that external exporter is added.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, agents.ListAgentsOKBody{
+ ExternalExporter: []*agents.ExternalExporterItems0{
+ {
+ AgentID: listAgents.Payload.ExternalExporter[0].AgentID,
+ ServiceID: serviceID,
+ ListenPort: 8404,
+ RunsOnNodeID: nodeID,
+ Scheme: "http",
+ MetricsPath: "/metrics",
+ },
+ },
+ }, *listAgents.Payload)
+ defer removeAllAgentsInList(t, listAgents)
+ })
+
+ t.Run("With the same name", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "genericNode-for-basic-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-for-the-same-name")
+
+ params := &ha_proxy.AddHAProxyParams{
+ Context: pmmapitests.Context,
+ Body: ha_proxy.AddHAProxyBody{
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ ListenPort: 9250,
+ SkipConnectionCheck: true,
+ },
+ }
+ addHAProxyOK, err := client.Default.HAProxy.AddHAProxy(params)
+ require.NoError(t, err)
+ require.NotNil(t, addHAProxyOK)
+ require.NotNil(t, addHAProxyOK.Payload.Service)
+ serviceID := addHAProxyOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+ defer removeServiceAgents(t, serviceID)
+
+ params = &ha_proxy.AddHAProxyParams{
+ Context: pmmapitests.Context,
+ Body: ha_proxy.AddHAProxyBody{
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ ListenPort: 9260,
+ },
+ }
+ addHAProxyOK, err = client.Default.HAProxy.AddHAProxy(params)
+ require.Nil(t, addHAProxyOK)
+ pmmapitests.AssertAPIErrorf(t, err, 409, codes.AlreadyExists, `Service with name %q already exists.`, serviceName)
+ })
+
+ t.Run("Empty Service Name", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ genericNode := pmmapitests.AddGenericNode(t, nodeName)
+ nodeID := genericNode.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ params := &ha_proxy.AddHAProxyParams{
+ Context: pmmapitests.Context,
+ Body: ha_proxy.AddHAProxyBody{
+ NodeID: nodeID,
+ },
+ }
+ addHAProxyOK, err := client.Default.HAProxy.AddHAProxy(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field ServiceName: value '' must not be an empty string")
+ assert.Nil(t, addHAProxyOK)
+ })
+
+ t.Run("Empty ListenPort", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ genericNode := pmmapitests.AddGenericNode(t, nodeName)
+ nodeID := genericNode.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ serviceName := pmmapitests.TestString(t, "service-name")
+ params := &ha_proxy.AddHAProxyParams{
+ Context: pmmapitests.Context,
+ Body: ha_proxy.AddHAProxyBody{
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ },
+ }
+ addHAProxyOK, err := client.Default.HAProxy.AddHAProxy(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field ListenPort: value '0' must be greater than '0'")
+ assert.Nil(t, addHAProxyOK)
+ })
+
+ t.Run("Empty Node ID", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ genericNode := pmmapitests.AddGenericNode(t, nodeName)
+ nodeID := genericNode.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ serviceName := pmmapitests.TestString(t, "service-name")
+ params := &ha_proxy.AddHAProxyParams{
+ Context: pmmapitests.Context,
+ Body: ha_proxy.AddHAProxyBody{
+ ServiceName: serviceName,
+ ListenPort: 12345,
+ },
+ }
+ addHAProxyOK, err := client.Default.HAProxy.AddHAProxy(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "expected only one param; node id, node name or register node params")
+ assert.Nil(t, addHAProxyOK)
+ })
+
+ t.Run("Empty Address for Add Node", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ genericNode := pmmapitests.AddGenericNode(t, nodeName)
+ nodeID := genericNode.NodeID
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ serviceName := pmmapitests.TestString(t, "service-name")
+ params := &ha_proxy.AddHAProxyParams{
+ Context: pmmapitests.Context,
+ Body: ha_proxy.AddHAProxyBody{
+ AddNode: &ha_proxy.AddHAProxyParamsBodyAddNode{
+ NodeType: pointer.ToString(ha_proxy.AddHAProxyParamsBodyAddNodeNodeTypeREMOTENODE),
+ NodeName: "haproxy-serverless",
+ },
+ ServiceName: serviceName,
+ ListenPort: 12345,
+ },
+ }
+ addHAProxyOK, err := client.Default.HAProxy.AddHAProxy(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "address can't be empty for add node request.")
+ assert.Nil(t, addHAProxyOK)
+ })
+}
+
+func TestRemoveHAProxy(t *testing.T) {
+ addHAProxy := func(t *testing.T, serviceName, nodeName string) (nodeID string, serviceID string) {
+ t.Helper()
+ genericNode := pmmapitests.AddGenericNode(t, nodeName)
+ nodeID = genericNode.NodeID
+
+ params := &ha_proxy.AddHAProxyParams{
+ Context: pmmapitests.Context,
+ Body: ha_proxy.AddHAProxyBody{
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Username: "username",
+ Password: "password",
+ ListenPort: 12345,
+ SkipConnectionCheck: true,
+ },
+ }
+ addHAProxyOK, err := client.Default.HAProxy.AddHAProxy(params)
+ require.NoError(t, err)
+ require.NotNil(t, addHAProxyOK)
+ require.NotNil(t, addHAProxyOK.Payload.Service)
+ serviceID = addHAProxyOK.Payload.Service.ServiceID
+ return
+ }
+
+ t.Run("By name", func(t *testing.T) {
+ serviceName := pmmapitests.TestString(t, "service-remove-by-name")
+ nodeName := pmmapitests.TestString(t, "node-remove-by-name")
+ nodeID, serviceID := addHAProxy(t, serviceName, nodeName)
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{
+ Body: service.RemoveServiceBody{
+ ServiceName: serviceName,
+ ServiceType: pointer.ToString(service.RemoveServiceBodyServiceTypeHAPROXYSERVICE),
+ },
+ Context: pmmapitests.Context,
+ })
+ noError := assert.NoError(t, err)
+ notNil := assert.NotNil(t, removeServiceOK)
+ if !noError || !notNil {
+ defer pmmapitests.RemoveServices(t, serviceID)
+ }
+
+ // Check that the service removed with agents.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Service with ID %q not found.", serviceID)
+ assert.Nil(t, listAgents)
+ })
+
+ t.Run("By ID", func(t *testing.T) {
+ serviceName := pmmapitests.TestString(t, "service-remove-by-id")
+ nodeName := pmmapitests.TestString(t, "node-remove-by-id")
+ nodeID, serviceID := addHAProxy(t, serviceName, nodeName)
+ defer pmmapitests.RemoveNodes(t, nodeID)
+
+ removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{
+ Body: service.RemoveServiceBody{
+ ServiceID: serviceID,
+ ServiceType: pointer.ToString(service.RemoveServiceBodyServiceTypeHAPROXYSERVICE),
+ },
+ Context: pmmapitests.Context,
+ })
+ noError := assert.NoError(t, err)
+ notNil := assert.NotNil(t, removeServiceOK)
+ if !noError || !notNil {
+ defer pmmapitests.RemoveServices(t, serviceID)
+ }
+
+ // Check that the service removed with agents.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Service with ID %q not found.", serviceID)
+ assert.Nil(t, listAgents)
+ })
+
+ t.Run("Both params", func(t *testing.T) {
+ serviceName := pmmapitests.TestString(t, "service-remove-both-params")
+ nodeName := pmmapitests.TestString(t, "node-remove-both-params")
+ nodeID, serviceID := addHAProxy(t, serviceName, nodeName)
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{
+ Body: service.RemoveServiceBody{
+ ServiceID: serviceID,
+ ServiceName: serviceName,
+ ServiceType: pointer.ToString(service.RemoveServiceBodyServiceTypeHAPROXYSERVICE),
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.Nil(t, removeServiceOK)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "service_id or service_name expected; not both")
+ })
+
+ t.Run("Wrong type", func(t *testing.T) {
+ serviceName := pmmapitests.TestString(t, "service-remove-wrong-type")
+ nodeName := pmmapitests.TestString(t, "node-remove-wrong-type")
+ nodeID, serviceID := addHAProxy(t, serviceName, nodeName)
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{
+ Body: service.RemoveServiceBody{
+ ServiceID: serviceID,
+ ServiceType: pointer.ToString(service.RemoveServiceBodyServiceTypePOSTGRESQLSERVICE),
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.Nil(t, removeServiceOK)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "wrong service type")
+ })
+
+ t.Run("No params", func(t *testing.T) {
+ removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{
+ Body: service.RemoveServiceBody{},
+ Context: pmmapitests.Context,
+ })
+ assert.Nil(t, removeServiceOK)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "service_id or service_name expected")
+ })
+}
diff --git a/api-tests/management/helpers.go b/api-tests/management/helpers.go
new file mode 100644
index 0000000000..a2968b168b
--- /dev/null
+++ b/api-tests/management/helpers.go
@@ -0,0 +1,194 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package management
+
+import (
+ "context"
+
+ "github.com/percona/pmm/api/inventorypb"
+ inventoryClient "github.com/percona/pmm/api/inventorypb/json/client"
+ "github.com/percona/pmm/api/inventorypb/json/client/agents"
+ "github.com/percona/pmm/api/inventorypb/json/client/nodes"
+ "github.com/percona/pmm/api/managementpb/json/client"
+ "github.com/percona/pmm/api/managementpb/json/client/node"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+// AgentStatusUnknown means agent is not connected and we don't know anything about its status.
+var AgentStatusUnknown = inventorypb.AgentStatus_name[int32(inventorypb.AgentStatus_UNKNOWN)]
+
+func RegisterGenericNode(t pmmapitests.TestingT, body node.RegisterNodeBody) (string, string) {
+ t.Helper()
+ params := node.RegisterNodeParams{
+ Context: pmmapitests.Context,
+ Body: body,
+ }
+ registerOK, err := client.Default.Node.RegisterNode(¶ms)
+ require.NoError(t, err)
+ require.NotNil(t, registerOK)
+ require.NotNil(t, registerOK.Payload.PMMAgent)
+ require.NotNil(t, registerOK.Payload.PMMAgent.AgentID)
+ require.NotNil(t, registerOK.Payload.GenericNode)
+ require.NotNil(t, registerOK.Payload.GenericNode.NodeID)
+ return registerOK.Payload.GenericNode.NodeID, registerOK.Payload.PMMAgent.AgentID
+}
+
+func registerContainerNode(t pmmapitests.TestingT, body node.RegisterNodeBody) (string, string) {
+ t.Helper()
+
+ params := node.RegisterNodeParams{
+ Context: pmmapitests.Context,
+ Body: body,
+ }
+ registerOK, err := client.Default.Node.RegisterNode(¶ms)
+ require.NoError(t, err)
+ require.NotNil(t, registerOK)
+ require.NotNil(t, registerOK.Payload.PMMAgent)
+ require.NotNil(t, registerOK.Payload.PMMAgent.AgentID)
+ require.NotNil(t, registerOK.Payload.ContainerNode)
+ require.NotNil(t, registerOK.Payload.ContainerNode.NodeID)
+ return registerOK.Payload.ContainerNode.NodeID, registerOK.Payload.PMMAgent.AgentID
+}
+
+func assertNodeExporterCreated(t pmmapitests.TestingT, pmmAgentID string) (string, bool) {
+ t.Helper()
+
+ listAgentsOK, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Body: agents.ListAgentsBody{
+ PMMAgentID: pmmAgentID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.Len(t, listAgentsOK.Payload.NodeExporter, 1)
+ nodeExporterAgentID := listAgentsOK.Payload.NodeExporter[0].AgentID
+ asserted := assert.Equal(t, agents.NodeExporterItems0{
+ PMMAgentID: pmmAgentID,
+ AgentID: nodeExporterAgentID,
+ PushMetricsEnabled: true,
+ Status: &AgentStatusUnknown,
+ }, *listAgentsOK.Payload.NodeExporter[0])
+ return nodeExporterAgentID, asserted
+}
+
+func assertPMMAgentCreated(t pmmapitests.TestingT, nodeID string, pmmAgentID string) {
+ t.Helper()
+
+ agentOK, err := inventoryClient.Default.Agents.GetAgent(&agents.GetAgentParams{
+ Body: agents.GetAgentBody{
+ AgentID: pmmAgentID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, agents.GetAgentOKBody{
+ PMMAgent: &agents.GetAgentOKBodyPMMAgent{
+ AgentID: pmmAgentID,
+ RunsOnNodeID: nodeID,
+ },
+ }, *agentOK.Payload)
+}
+
+func assertNodeCreated(t pmmapitests.TestingT, nodeID string, expectedResult nodes.GetNodeOKBody) {
+ t.Helper()
+
+ nodeOK, err := inventoryClient.Default.Nodes.GetNode(&nodes.GetNodeParams{
+ Body: nodes.GetNodeBody{
+ NodeID: nodeID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, expectedResult, *nodeOK.Payload)
+}
+
+func RemovePMMAgentWithSubAgents(t pmmapitests.TestingT, pmmAgentID string) {
+ t.Helper()
+
+ listAgentsOK, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Body: agents.ListAgentsBody{
+ PMMAgentID: pmmAgentID,
+ },
+ Context: context.Background(),
+ })
+ assert.NoError(t, err)
+ removeAllAgentsInList(t, listAgentsOK)
+ pmmapitests.RemoveAgents(t, pmmAgentID)
+}
+
+func removeServiceAgents(t pmmapitests.TestingT, serviceID string) {
+ t.Helper()
+
+ listAgentsOK, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ Context: context.Background(),
+ })
+ assert.NoError(t, err)
+ removeAllAgentsInList(t, listAgentsOK)
+}
+
+func removeAllAgentsInList(t pmmapitests.TestingT, listAgentsOK *agents.ListAgentsOK) {
+ t.Helper()
+
+ require.NotNil(t, listAgentsOK)
+ require.NotNil(t, listAgentsOK.Payload)
+
+ var agentIDs []string
+ for _, agent := range listAgentsOK.Payload.NodeExporter {
+ agentIDs = append(agentIDs, agent.AgentID)
+ }
+ for _, agent := range listAgentsOK.Payload.PMMAgent {
+ agentIDs = append(agentIDs, agent.AgentID)
+ }
+ for _, agent := range listAgentsOK.Payload.PostgresExporter {
+ agentIDs = append(agentIDs, agent.AgentID)
+ }
+ for _, agent := range listAgentsOK.Payload.MysqldExporter {
+ agentIDs = append(agentIDs, agent.AgentID)
+ }
+ for _, agent := range listAgentsOK.Payload.ProxysqlExporter {
+ agentIDs = append(agentIDs, agent.AgentID)
+ }
+ for _, agent := range listAgentsOK.Payload.QANMysqlPerfschemaAgent {
+ agentIDs = append(agentIDs, agent.AgentID)
+ }
+ for _, agent := range listAgentsOK.Payload.MongodbExporter {
+ agentIDs = append(agentIDs, agent.AgentID)
+ }
+ for _, agent := range listAgentsOK.Payload.QANMongodbProfilerAgent {
+ agentIDs = append(agentIDs, agent.AgentID)
+ }
+ for _, agent := range listAgentsOK.Payload.QANMysqlSlowlogAgent {
+ agentIDs = append(agentIDs, agent.AgentID)
+ }
+ for _, agent := range listAgentsOK.Payload.QANPostgresqlPgstatementsAgent {
+ agentIDs = append(agentIDs, agent.AgentID)
+ }
+ for _, agent := range listAgentsOK.Payload.ExternalExporter {
+ agentIDs = append(agentIDs, agent.AgentID)
+ }
+ for _, agent := range listAgentsOK.Payload.VMAgent {
+ agentIDs = append(agentIDs, agent.AgentID)
+ }
+
+ pmmapitests.RemoveAgents(t, agentIDs...)
+}
diff --git a/api-tests/management/ia/channels_test.go b/api-tests/management/ia/channels_test.go
new file mode 100644
index 0000000000..0775408108
--- /dev/null
+++ b/api-tests/management/ia/channels_test.go
@@ -0,0 +1,363 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package ia
+
+import (
+ "testing"
+
+ "github.com/brianvoe/gofakeit/v6"
+ channelsClient "github.com/percona/pmm/api/managementpb/ia/json/client"
+ "github.com/percona/pmm/api/managementpb/ia/json/client/channels"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+// Note: Even though the IA services check for alerting enabled or disabled before returning results
+// we don't enable or disable IA explicit in our tests since it is enabled by default through
+// ENABLE_ALERTING env var.
+func TestAddChannel(t *testing.T) {
+ client := channelsClient.Default.Channels
+
+ t.Run("normal", func(t *testing.T) {
+ resp, err := client.AddChannel(&channels.AddChannelParams{
+ Body: channels.AddChannelBody{
+ Summary: gofakeit.Quote(),
+ Disabled: gofakeit.Bool(),
+ EmailConfig: &channels.AddChannelParamsBodyEmailConfig{
+ SendResolved: false,
+ To: []string{gofakeit.Email()},
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ defer deleteChannel(t, client, resp.Payload.ChannelID)
+
+ assert.NotEmpty(t, resp.Payload.ChannelID)
+ })
+
+ t.Run("invalid request", func(t *testing.T) {
+ resp, err := client.AddChannel(&channels.AddChannelParams{
+ Body: channels.AddChannelBody{
+ Summary: gofakeit.Quote(),
+ Disabled: gofakeit.Bool(),
+ EmailConfig: &channels.AddChannelParamsBodyEmailConfig{
+ SendResolved: false,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field EmailConfig.To: value '[]' must contain at least 1 elements")
+ assert.Nil(t, resp)
+ })
+
+ t.Run("missing config", func(t *testing.T) {
+ resp, err := client.AddChannel(&channels.AddChannelParams{
+ Body: channels.AddChannelBody{
+ Summary: gofakeit.Quote(),
+ Disabled: gofakeit.Bool(),
+ },
+ Context: pmmapitests.Context,
+ })
+
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Missing channel configuration.")
+ assert.Nil(t, resp)
+ })
+}
+
+func TestChangeChannel(t *testing.T) {
+ client := channelsClient.Default.Channels
+
+ t.Run("normal", func(t *testing.T) {
+ resp1, err := client.AddChannel(&channels.AddChannelParams{
+ Body: channels.AddChannelBody{
+ Summary: gofakeit.Quote(),
+ Disabled: false,
+ EmailConfig: &channels.AddChannelParamsBodyEmailConfig{
+ SendResolved: false,
+ To: []string{gofakeit.Email()},
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ defer deleteChannel(t, client, resp1.Payload.ChannelID)
+
+ slackChannel := gofakeit.UUID()
+ newSummary := gofakeit.UUID()
+ _, err = client.ChangeChannel(&channels.ChangeChannelParams{
+ Body: channels.ChangeChannelBody{
+ ChannelID: resp1.Payload.ChannelID,
+ Summary: newSummary,
+ Disabled: true,
+ SlackConfig: &channels.ChangeChannelParamsBodySlackConfig{
+ SendResolved: true,
+ Channel: slackChannel,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ resp2, err := client.ListChannels(&channels.ListChannelsParams{Context: pmmapitests.Context})
+ require.NoError(t, err)
+
+ assert.NotEmpty(t, resp2.Payload.Channels)
+ var found bool
+ for _, channel := range resp2.Payload.Channels {
+ if channel.ChannelID == resp1.Payload.ChannelID {
+ assert.Equal(t, newSummary, channel.Summary)
+ assert.True(t, channel.Disabled)
+ assert.Nil(t, channel.EmailConfig)
+ assert.Equal(t, slackChannel, channel.SlackConfig.Channel)
+ assert.True(t, channel.SlackConfig.SendResolved)
+ found = true
+ }
+ }
+
+ assert.True(t, found, "Expected channel not found")
+ })
+}
+
+func TestRemoveChannel(t *testing.T) {
+ client := channelsClient.Default.Channels
+
+ t.Run("normal", func(t *testing.T) {
+ summary := gofakeit.UUID()
+ resp1, err := client.AddChannel(&channels.AddChannelParams{
+ Body: channels.AddChannelBody{
+ Summary: summary,
+ Disabled: gofakeit.Bool(),
+ EmailConfig: &channels.AddChannelParamsBodyEmailConfig{
+ SendResolved: false,
+ To: []string{gofakeit.Email()},
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ _, err = client.RemoveChannel(&channels.RemoveChannelParams{
+ Body: channels.RemoveChannelBody{
+ ChannelID: resp1.Payload.ChannelID,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ resp2, err := client.ListChannels(&channels.ListChannelsParams{Context: pmmapitests.Context})
+ require.NoError(t, err)
+
+ for _, channel := range resp2.Payload.Channels {
+ assert.NotEqual(t, resp1, channel.ChannelID)
+ }
+ })
+ t.Run("unknown id", func(t *testing.T) {
+ resp, err := client.AddChannel(&channels.AddChannelParams{
+ Body: channels.AddChannelBody{
+ Summary: gofakeit.Quote(),
+ Disabled: gofakeit.Bool(),
+ EmailConfig: &channels.AddChannelParamsBodyEmailConfig{
+ SendResolved: false,
+ To: []string{gofakeit.Email()},
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ defer deleteChannel(t, client, resp.Payload.ChannelID)
+
+ _, err = client.RemoveChannel(&channels.RemoveChannelParams{
+ Body: channels.RemoveChannelBody{
+ ChannelID: gofakeit.UUID(),
+ },
+ Context: pmmapitests.Context,
+ })
+ require.Error(t, err)
+ })
+
+ t.Run("channel in use", func(t *testing.T) {
+ templateName := createTemplate(t)
+ defer deleteTemplate(t, channelsClient.Default.Templates, templateName)
+
+ channelID := createChannel(t)
+ defer deleteChannel(t, channelsClient.Default.Channels, channelID)
+
+ params := createAlertRuleParams(templateName, channelID, "param2", nil)
+ rule, err := channelsClient.Default.Rules.CreateAlertRule(params)
+ require.NoError(t, err)
+ defer deleteRule(t, channelsClient.Default.Rules, rule.Payload.RuleID)
+
+ _, err = client.RemoveChannel(&channels.RemoveChannelParams{
+ Body: channels.RemoveChannelBody{
+ ChannelID: channelID,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.FailedPrecondition, "Failed to delete notification channel %s, as it is being used by some rule.", channelID)
+
+ resp, err := client.ListChannels(&channels.ListChannelsParams{
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ var found bool
+ for _, channel := range resp.Payload.Channels {
+ if channelID == channel.ChannelID {
+ found = true
+ }
+ }
+ assert.Truef(t, found, "Channel with id %s not found", channelID)
+ })
+}
+
+func TestListChannels(t *testing.T) {
+ client := channelsClient.Default.Channels
+
+ summary := gofakeit.UUID()
+ email := gofakeit.Email()
+ disabled := gofakeit.Bool()
+ resp1, err := client.AddChannel(&channels.AddChannelParams{
+ Body: channels.AddChannelBody{
+ Summary: summary,
+ Disabled: disabled,
+ EmailConfig: &channels.AddChannelParamsBodyEmailConfig{
+ SendResolved: true,
+ To: []string{email},
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ defer deleteChannel(t, client, resp1.Payload.ChannelID)
+
+ t.Run("without pagination", func(t *testing.T) {
+ resp, err := client.ListChannels(&channels.ListChannelsParams{Context: pmmapitests.Context})
+ require.NoError(t, err)
+
+ assert.NotEmpty(t, resp.Payload.Channels)
+ var found bool
+ for _, channel := range resp.Payload.Channels {
+ if channel.ChannelID == resp1.Payload.ChannelID {
+ assert.Equal(t, summary, channel.Summary)
+ assert.Equal(t, disabled, channel.Disabled)
+ assert.Equal(t, []string{email}, channel.EmailConfig.To)
+ assert.True(t, channel.EmailConfig.SendResolved)
+ found = true
+ }
+ }
+ assert.True(t, found, "Expected channel not found")
+ })
+
+ t.Run("pagination", func(t *testing.T) {
+ const channelsCount = 5
+
+ channelIds := make(map[string]struct{})
+
+ for i := 0; i < channelsCount; i++ {
+ resp, err := client.AddChannel(&channels.AddChannelParams{
+ Body: channels.AddChannelBody{
+ Summary: gofakeit.Name(),
+ EmailConfig: &channels.AddChannelParamsBodyEmailConfig{
+ SendResolved: true,
+ To: []string{email},
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ channelIds[resp.Payload.ChannelID] = struct{}{}
+ }
+ defer func() {
+ for id := range channelIds {
+ deleteChannel(t, client, id)
+ }
+ }()
+
+ // list channels, so they are all on the first page
+ body := channels.ListChannelsBody{
+ PageParams: &channels.ListChannelsParamsBodyPageParams{
+ PageSize: 20,
+ Index: 0,
+ },
+ }
+ listAllChannels, err := client.ListChannels(&channels.ListChannelsParams{
+ Body: body,
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ assert.GreaterOrEqual(t, len(listAllChannels.Payload.Channels), channelsCount)
+ assert.Equal(t, int32(len(listAllChannels.Payload.Channels)), listAllChannels.Payload.Totals.TotalItems)
+ assert.Equal(t, int32(1), listAllChannels.Payload.Totals.TotalPages)
+
+ assertFindChannel := func(list []*channels.ChannelsItems0, id string) func() bool {
+ return func() bool {
+ for _, channel := range list {
+ if channel.ChannelID == id {
+ return true
+ }
+ }
+ return false
+ }
+ }
+
+ for name := range channelIds {
+ assert.Conditionf(t, assertFindChannel(listAllChannels.Payload.Channels, name), "channel %s not found", name)
+ }
+
+ // paginate page over page with page size 1 and check the order - it should be the same as in listAllTemplates.
+ // last iteration checks that there is no elements for not existing page.
+ for pageIndex := 0; pageIndex <= len(listAllChannels.Payload.Channels); pageIndex++ {
+ body := channels.ListChannelsBody{
+ PageParams: &channels.ListChannelsParamsBodyPageParams{
+ PageSize: 1,
+ Index: int32(pageIndex),
+ },
+ }
+ listOneTemplate, err := client.ListChannels(&channels.ListChannelsParams{
+ Body: body, Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ assert.Equal(t, listAllChannels.Payload.Totals.TotalItems, listOneTemplate.Payload.Totals.TotalItems)
+ assert.GreaterOrEqual(t, listOneTemplate.Payload.Totals.TotalPages, int32(channelsCount))
+
+ if pageIndex != len(listAllChannels.Payload.Channels) {
+ require.Len(t, listOneTemplate.Payload.Channels, 1)
+ assert.Equal(t, listAllChannels.Payload.Channels[pageIndex].ChannelID, listOneTemplate.Payload.Channels[0].ChannelID)
+ } else {
+ assert.Len(t, listOneTemplate.Payload.Channels, 0)
+ }
+ }
+ })
+}
+
+func deleteChannel(t *testing.T, client channels.ClientService, id string) {
+ _, err := client.RemoveChannel(&channels.RemoveChannelParams{
+ Body: channels.RemoveChannelBody{
+ ChannelID: id,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+}
diff --git a/api-tests/management/ia/rules_test.go b/api-tests/management/ia/rules_test.go
new file mode 100644
index 0000000000..ce0ee81714
--- /dev/null
+++ b/api-tests/management/ia/rules_test.go
@@ -0,0 +1,561 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package ia
+
+import (
+ "fmt"
+ "io/ioutil"
+ "testing"
+
+ "github.com/AlekSi/pointer"
+ "github.com/brianvoe/gofakeit/v6"
+ "github.com/percona/pmm/api/managementpb/ia/json/client"
+ "github.com/percona/pmm/api/managementpb/ia/json/client/channels"
+ "github.com/percona/pmm/api/managementpb/ia/json/client/rules"
+ "github.com/percona/pmm/api/managementpb/ia/json/client/templates"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+// Note: Even though the IA services check for alerting enabled or disabled before returning results
+// we don't enable or disable IA explicit in our tests since it is enabled by default through
+// ENABLE_ALERTING env var.
+func TestRulesAPI(t *testing.T) {
+ rulesClient := client.Default.Rules
+ channelsClient := client.Default.Channels
+
+ dummyFilter := &rules.FiltersItems0{
+ Type: pointer.ToString("EQUAL"),
+ Key: "threshold",
+ Value: "12",
+ }
+
+ templateName := createTemplate(t)
+ defer deleteTemplate(t, client.Default.Templates, templateName)
+
+ channelID := createChannel(t)
+ defer deleteChannel(t, channelsClient, channelID)
+
+ t.Run("add", func(t *testing.T) {
+ t.Run("normal", func(t *testing.T) {
+ params := createAlertRuleParams(templateName, channelID, "param2", dummyFilter)
+ rule, err := rulesClient.CreateAlertRule(params)
+ require.NoError(t, err)
+ defer deleteRule(t, rulesClient, rule.Payload.RuleID)
+
+ assert.NotEmpty(t, rule.Payload.RuleID)
+ })
+
+ t.Run("without channels and filters", func(t *testing.T) {
+ params := createAlertRuleParams(templateName, "", "param2", nil)
+ rule, err := rulesClient.CreateAlertRule(params)
+ require.NoError(t, err)
+ defer deleteRule(t, rulesClient, rule.Payload.RuleID)
+
+ assert.NotEmpty(t, rule.Payload.RuleID)
+ })
+
+ t.Run("builtin_template", func(t *testing.T) {
+ params := createAlertRuleParams("pmm_mongodb_restarted", channelID, "threshold", dummyFilter)
+ rule, err := rulesClient.CreateAlertRule(params)
+ require.NoError(t, err)
+ defer deleteRule(t, rulesClient, rule.Payload.RuleID)
+
+ assert.NotEmpty(t, rule.Payload.RuleID)
+ })
+
+ t.Run("use default value for parameter", func(t *testing.T) {
+ params := createAlertRuleParams(templateName, channelID, "param2", dummyFilter)
+ rule, err := rulesClient.CreateAlertRule(params)
+ require.NoError(t, err)
+ defer deleteRule(t, rulesClient, rule.Payload.RuleID)
+
+ assert.NotEmpty(t, rule.Payload.RuleID)
+ })
+
+ t.Run("unknown template", func(t *testing.T) {
+ templateName := gofakeit.UUID()
+ params := createAlertRuleParams(templateName, channelID, "param2", dummyFilter)
+ _, err := rulesClient.CreateAlertRule(params)
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Unknown template %s.", templateName)
+ })
+
+ t.Run("unknown channel", func(t *testing.T) {
+ channelID := gofakeit.UUID()
+ params := createAlertRuleParams(templateName, channelID, "param2", dummyFilter)
+ _, err := rulesClient.CreateAlertRule(params)
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Failed to find all required channels: [%s].", channelID)
+ })
+
+ t.Run("wrong parameter", func(t *testing.T) {
+ params := createAlertRuleParams(templateName, channelID, "param2", dummyFilter)
+ params.Body.Params = append(
+ params.Body.Params,
+ &rules.ParamsItems0{
+ Name: "unknown parameter",
+ Type: pointer.ToString("FLOAT"),
+ Float: 12,
+ })
+ _, err := rulesClient.CreateAlertRule(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Unknown parameters [unknown parameter].")
+ })
+
+ t.Run("wrong parameter type", func(t *testing.T) {
+ params := createAlertRuleParams(templateName, channelID, "param1", dummyFilter)
+ params.Body.Params = []*rules.ParamsItems0{{
+ Name: "param1",
+ Type: pointer.ToString("BOOL"),
+ Bool: true,
+ }}
+ _, err := rulesClient.CreateAlertRule(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Parameter param1 has type bool instead of float.")
+ })
+ })
+
+ t.Run("update", func(t *testing.T) {
+ newChannelID := createChannel(t)
+ defer deleteChannel(t, channelsClient, newChannelID)
+
+ t.Run("normal", func(t *testing.T) {
+ cParams := createAlertRuleParams(templateName, channelID, "param2", dummyFilter)
+ rule, err := rulesClient.CreateAlertRule(cParams)
+ require.NoError(t, err)
+ defer deleteRule(t, rulesClient, rule.Payload.RuleID)
+
+ params := &rules.UpdateAlertRuleParams{
+ Body: rules.UpdateAlertRuleBody{
+ RuleID: rule.Payload.RuleID,
+ Disabled: false,
+ Params: []*rules.ParamsItems0{{
+ Name: "param2",
+ Type: pointer.ToString("FLOAT"),
+ Float: 21,
+ }},
+ For: "10s",
+ Severity: pointer.ToString("SEVERITY_ERROR"),
+ CustomLabels: map[string]string{"foo": "bar", "baz": "faz"},
+ Filters: []*rules.FiltersItems0{{
+ Type: pointer.ToString("EQUAL"),
+ Key: "param1",
+ Value: "21",
+ }},
+ ChannelIds: []string{channelID, newChannelID},
+ },
+ Context: pmmapitests.Context,
+ }
+ _, err = rulesClient.UpdateAlertRule(params)
+ require.NoError(t, err)
+
+ list, err := rulesClient.ListAlertRules(&rules.ListAlertRulesParams{Context: pmmapitests.Context})
+ require.NoError(t, err)
+
+ var found bool
+ for _, r := range list.Payload.Rules {
+ if r.RuleID == rule.Payload.RuleID {
+ assert.False(t, r.Disabled)
+ assert.Equal(t, "10s", r.For)
+ assert.Len(t, r.Params, 2)
+ assert.Equal(t, params.Body.Params[0].Type, r.Params[1].Type)
+ assert.Equal(t, params.Body.Params[0].Name, r.Params[1].Name)
+ assert.Equal(t, params.Body.Params[0].Float, r.Params[1].Float)
+ assert.Equal(t, params.Body.Params[0].Bool, r.Params[1].Bool)
+ assert.Equal(t, params.Body.Params[0].String, r.Params[1].String)
+ assert.Equal(t, "FLOAT", *r.Params[0].Type)
+ assert.Equal(t, "param1", r.Params[0].Name)
+ assert.Equal(t, float32(80), r.Params[0].Float)
+ assert.Equal(t, false, r.Params[0].Bool)
+ assert.Equal(t, "", r.Params[0].String)
+ found = true
+ }
+ }
+ assert.Truef(t, found, "Rule with id %s not found", rule.Payload.RuleID)
+ })
+
+ t.Run("unknown channel", func(t *testing.T) {
+ cParams := createAlertRuleParams(templateName, channelID, "param2", dummyFilter)
+ rule, err := rulesClient.CreateAlertRule(cParams)
+ require.NoError(t, err)
+ defer deleteRule(t, rulesClient, rule.Payload.RuleID)
+
+ unknownChannelID := gofakeit.UUID()
+ params := &rules.UpdateAlertRuleParams{
+ Body: rules.UpdateAlertRuleBody{
+ RuleID: rule.Payload.RuleID,
+ Disabled: false,
+ Params: []*rules.ParamsItems0{{
+ Name: "param2",
+ Type: pointer.ToString("FLOAT"),
+ Float: 21,
+ }},
+ For: "10s",
+ Severity: pointer.ToString("SEVERITY_ERROR"),
+ CustomLabels: map[string]string{"foo": "bar", "baz": "faz"},
+ Filters: []*rules.FiltersItems0{{
+ Type: pointer.ToString("EQUAL"),
+ Key: "param1",
+ Value: "21",
+ }},
+ ChannelIds: []string{channelID, unknownChannelID},
+ },
+ Context: pmmapitests.Context,
+ }
+ _, err = rulesClient.UpdateAlertRule(params)
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Failed to find all required channels: [%s].", unknownChannelID)
+ })
+
+ t.Run("wrong parameter", func(t *testing.T) {
+ cParams := createAlertRuleParams(templateName, channelID, "param2", dummyFilter)
+ rule, err := rulesClient.CreateAlertRule(cParams)
+ require.NoError(t, err)
+ defer deleteRule(t, rulesClient, rule.Payload.RuleID)
+
+ params := &rules.UpdateAlertRuleParams{
+ Body: rules.UpdateAlertRuleBody{
+ RuleID: rule.Payload.RuleID,
+ Disabled: false,
+ Params: []*rules.ParamsItems0{{
+ Name: "param2",
+ Type: pointer.ToString("FLOAT"),
+ Float: 12,
+ }, {
+ Name: "unknown parameter",
+ Type: pointer.ToString("FLOAT"),
+ Float: 21,
+ }},
+ For: "10s",
+ Severity: pointer.ToString("SEVERITY_ERROR"),
+ CustomLabels: map[string]string{"foo": "bar", "baz": "faz"},
+ Filters: []*rules.FiltersItems0{{
+ Type: pointer.ToString("EQUAL"),
+ Key: "param1",
+ Value: "21",
+ }},
+ ChannelIds: []string{channelID, newChannelID},
+ },
+ Context: pmmapitests.Context,
+ }
+ _, err = rulesClient.UpdateAlertRule(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Unknown parameters [unknown parameter].")
+ })
+
+ t.Run("missing parameter", func(t *testing.T) {
+ cParams := createAlertRuleParams(templateName, channelID, "param2", dummyFilter)
+ rule, err := rulesClient.CreateAlertRule(cParams)
+ require.NoError(t, err)
+ defer deleteRule(t, rulesClient, rule.Payload.RuleID)
+
+ params := &rules.UpdateAlertRuleParams{
+ Body: rules.UpdateAlertRuleBody{
+ RuleID: rule.Payload.RuleID,
+ Disabled: false,
+ Params: nil,
+ For: "10s",
+ Severity: pointer.ToString("SEVERITY_ERROR"),
+ CustomLabels: map[string]string{"foo": "bar", "baz": "faz"},
+ Filters: []*rules.FiltersItems0{{
+ Type: pointer.ToString("EQUAL"),
+ Key: "param1",
+ Value: "21",
+ }},
+ ChannelIds: []string{channelID, newChannelID},
+ },
+ Context: pmmapitests.Context,
+ }
+ _, err = rulesClient.UpdateAlertRule(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Parameter param2 defined in template %s doesn't have default value, so it should be specified in rule", templateName)
+ })
+
+ t.Run("wrong parameter type", func(t *testing.T) {
+ cParams := createAlertRuleParams(templateName, channelID, "param2", dummyFilter)
+ rule, err := rulesClient.CreateAlertRule(cParams)
+ require.NoError(t, err)
+ defer deleteRule(t, rulesClient, rule.Payload.RuleID)
+
+ params := &rules.UpdateAlertRuleParams{
+ Body: rules.UpdateAlertRuleBody{
+ RuleID: rule.Payload.RuleID,
+ Disabled: false,
+ Params: []*rules.ParamsItems0{{
+ Name: "param1",
+ Type: pointer.ToString("BOOL"),
+ Bool: true,
+ }},
+ For: "10s",
+ Severity: pointer.ToString("SEVERITY_ERROR"),
+ CustomLabels: map[string]string{"foo": "bar", "baz": "faz"},
+ Filters: []*rules.FiltersItems0{{
+ Type: pointer.ToString("EQUAL"),
+ Key: "param1",
+ Value: "21",
+ }},
+ ChannelIds: []string{channelID, newChannelID},
+ },
+ Context: pmmapitests.Context,
+ }
+ _, err = rulesClient.UpdateAlertRule(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Parameter param1 has type bool instead of float.")
+ })
+ })
+
+ t.Run("toggle", func(t *testing.T) {
+ t.Run("normal", func(t *testing.T) {
+ cParams := createAlertRuleParams(templateName, channelID, "param2", dummyFilter)
+ rule, err := rulesClient.CreateAlertRule(cParams)
+ require.NoError(t, err)
+ defer deleteRule(t, rulesClient, rule.Payload.RuleID)
+
+ list, err := rulesClient.ListAlertRules(&rules.ListAlertRulesParams{Context: pmmapitests.Context})
+ require.NoError(t, err)
+
+ var found bool
+ for _, r := range list.Payload.Rules {
+ if r.RuleID == rule.Payload.RuleID {
+ assert.True(t, r.Disabled)
+ assert.Equal(t, "SEVERITY_WARNING", pointer.GetString(r.Severity))
+ found = true
+ }
+ }
+ assert.Truef(t, found, "Rule with id %s not found", rule.Payload.RuleID)
+
+ _, err = rulesClient.ToggleAlertRule(&rules.ToggleAlertRuleParams{
+ Body: rules.ToggleAlertRuleBody{
+ RuleID: rule.Payload.RuleID,
+ Disabled: pointer.ToString(rules.ToggleAlertRuleBodyDisabledFALSE),
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ list, err = rulesClient.ListAlertRules(&rules.ListAlertRulesParams{Context: pmmapitests.Context})
+ require.NoError(t, err)
+
+ found = false
+ for _, r := range list.Payload.Rules {
+ if r.RuleID == rule.Payload.RuleID {
+ assert.False(t, r.Disabled)
+ assert.Equal(t, "SEVERITY_WARNING", pointer.GetString(r.Severity))
+ found = true
+ }
+ }
+ assert.Truef(t, found, "Rule with id %s not found", rule.Payload.RuleID)
+ })
+ })
+
+ t.Run("delete", func(t *testing.T) {
+ params := createAlertRuleParams(templateName, channelID, "param2", dummyFilter)
+ rule, err := rulesClient.CreateAlertRule(params)
+ require.NoError(t, err)
+
+ _, err = rulesClient.DeleteAlertRule(&rules.DeleteAlertRuleParams{
+ Body: rules.DeleteAlertRuleBody{RuleID: rule.Payload.RuleID},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ list, err := rulesClient.ListAlertRules(&rules.ListAlertRulesParams{Context: pmmapitests.Context})
+ require.NoError(t, err)
+
+ for _, r := range list.Payload.Rules {
+ assert.NotEqual(t, rule.Payload.RuleID, r.RuleID)
+ }
+ })
+
+ t.Run("list without pagination", func(t *testing.T) {
+ params := createAlertRuleParams(templateName, channelID, "param2", dummyFilter)
+ rule, err := rulesClient.CreateAlertRule(params)
+ require.NoError(t, err)
+ defer deleteRule(t, rulesClient, rule.Payload.RuleID)
+
+ list, err := rulesClient.ListAlertRules(&rules.ListAlertRulesParams{Context: pmmapitests.Context})
+ require.NoError(t, err)
+
+ var found bool
+ for _, r := range list.Payload.Rules {
+ if r.RuleID == rule.Payload.RuleID {
+ assert.True(t, r.Disabled)
+ assert.Equal(t, params.Body.Summary, r.Summary)
+ assert.Len(t, r.Params, 2)
+ assert.Equal(t, params.Body.Params[0].Type, r.Params[1].Type)
+ assert.Equal(t, params.Body.Params[0].Name, r.Params[1].Name)
+ assert.Equal(t, params.Body.Params[0].Float, r.Params[1].Float)
+ assert.Equal(t, params.Body.Params[0].Bool, r.Params[1].Bool)
+ assert.Equal(t, params.Body.Params[0].String, r.Params[1].String)
+ assert.Equal(t, "FLOAT", *r.Params[0].Type)
+ assert.Equal(t, "param1", r.Params[0].Name)
+ assert.Equal(t, float32(80), r.Params[0].Float)
+ assert.Equal(t, false, r.Params[0].Bool)
+ assert.Equal(t, "", r.Params[0].String)
+ assert.Equal(t, params.Body.For, r.For)
+ assert.Equal(t, params.Body.Severity, r.Severity)
+ assert.Equal(t, params.Body.CustomLabels, r.CustomLabels)
+ assert.Len(t, params.Body.Filters, 1)
+ assert.Equal(t, params.Body.Filters[0].Type, r.Filters[0].Type)
+ assert.Equal(t, params.Body.Filters[0].Key, r.Filters[0].Key)
+ assert.Equal(t, params.Body.Filters[0].Value, r.Filters[0].Value)
+ assert.Len(t, r.Channels, 1)
+ assert.Equal(t, r.Channels[0].ChannelID, channelID)
+ assert.Equal(t, "5 > 2 and 2 < 12", r.Expr)
+ found = true
+ }
+ }
+ assert.Truef(t, found, "Rule with id %s not found", rule.Payload.RuleID)
+ })
+
+ t.Run("list pagination", func(t *testing.T) {
+ const rulesCount = 5
+
+ ruleIDs := make(map[string]struct{})
+
+ for i := 0; i < rulesCount; i++ {
+ params := createAlertRuleParams(templateName, channelID, "param2", dummyFilter)
+ rule, err := rulesClient.CreateAlertRule(params)
+ require.NoError(t, err)
+
+ ruleIDs[rule.Payload.RuleID] = struct{}{}
+ }
+ defer func() {
+ for id := range ruleIDs {
+ deleteRule(t, rulesClient, id)
+ }
+ }()
+
+ // list rules, so they are all on the first page
+ body := rules.ListAlertRulesBody{
+ PageParams: &rules.ListAlertRulesParamsBodyPageParams{
+ PageSize: 20,
+ Index: 0,
+ },
+ }
+ list1, err := rulesClient.ListAlertRules(&rules.ListAlertRulesParams{Body: body, Context: pmmapitests.Context})
+ require.NoError(t, err)
+
+ lp1 := list1.Payload
+ // some tests didn't remove rules, so expect more elements than created in current test
+ assert.GreaterOrEqual(t, len(lp1.Rules), rulesCount)
+ assert.Equal(t, int32(len(lp1.Rules)), lp1.Totals.TotalItems)
+ assert.Equal(t, int32(1), lp1.Totals.TotalPages)
+ for id := range ruleIDs {
+ var found bool
+ for _, r := range list1.Payload.Rules {
+ if r.RuleID == id {
+ found = true
+
+ break
+ }
+ }
+
+ assert.Truef(t, found, "rule (%s) not found", id)
+ }
+
+ // paginate page over page with page size 1 and check the order - it should be the same as in list1.
+ // last iteration checks that there is no elements for not existing page.
+ for pageIndex := 0; pageIndex <= len(lp1.Rules); pageIndex++ {
+ body := rules.ListAlertRulesBody{
+ PageParams: &rules.ListAlertRulesParamsBodyPageParams{
+ PageSize: 1,
+ Index: int32(pageIndex),
+ },
+ }
+ list2, err := rulesClient.ListAlertRules(&rules.ListAlertRulesParams{Body: body, Context: pmmapitests.Context})
+ require.NoError(t, err)
+
+ lp2 := list2.Payload
+ assert.Equal(t, lp1.Totals.TotalItems, lp2.Totals.TotalItems)
+ assert.GreaterOrEqual(t, lp2.Totals.TotalPages, int32(rulesCount))
+
+ if pageIndex != len(lp1.Rules) {
+ require.Len(t, lp2.Rules, 1)
+ assert.Equal(t, lp1.Rules[pageIndex].RuleID, lp2.Rules[0].RuleID)
+ } else {
+ assert.Len(t, lp2.Rules, 0)
+ }
+ }
+ })
+}
+
+func deleteRule(t *testing.T, client rules.ClientService, id string) {
+ _, err := client.DeleteAlertRule(&rules.DeleteAlertRuleParams{
+ Body: rules.DeleteAlertRuleBody{RuleID: id},
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+}
+
+func createAlertRuleParams(templateName, channelID, paramName string, filter *rules.FiltersItems0) *rules.CreateAlertRuleParams {
+ rule := &rules.CreateAlertRuleParams{
+ Body: rules.CreateAlertRuleBody{
+ TemplateName: templateName,
+ Disabled: true,
+ Summary: "example summary",
+ Params: []*rules.ParamsItems0{{
+ Name: paramName,
+ Type: pointer.ToString("FLOAT"),
+ Float: 12,
+ }},
+ For: "5s",
+ Severity: pointer.ToString("SEVERITY_WARNING"),
+ CustomLabels: map[string]string{"foo": "bar"},
+ },
+ Context: pmmapitests.Context,
+ }
+
+ if channelID != "" {
+ rule.Body.ChannelIds = []string{channelID}
+ }
+
+ if filter != nil {
+ rule.Body.Filters = []*rules.FiltersItems0{filter}
+ }
+
+ return rule
+}
+
+func createTemplate(t *testing.T) string {
+ b, err := ioutil.ReadFile("../../testdata/ia/template.yaml")
+ require.NoError(t, err)
+
+ templateName := gofakeit.UUID()
+ expression := "5 > 2 and 2 < [[ .param2 ]]"
+ _, err = client.Default.Templates.CreateTemplate(&templates.CreateTemplateParams{
+ Body: templates.CreateTemplateBody{
+ Yaml: fmt.Sprintf(string(b), templateName, expression, "%", "s"),
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ return templateName
+}
+
+func createChannel(t *testing.T) string {
+ resp, err := client.Default.Channels.AddChannel(&channels.AddChannelParams{
+ Body: channels.AddChannelBody{
+ Summary: gofakeit.Quote(),
+ Disabled: gofakeit.Bool(),
+ EmailConfig: &channels.AddChannelParamsBodyEmailConfig{
+ SendResolved: false,
+ To: []string{gofakeit.Email()},
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ return resp.Payload.ChannelID
+}
diff --git a/api-tests/management/ia/templates_test.go b/api-tests/management/ia/templates_test.go
new file mode 100644
index 0000000000..4ef90d5dbc
--- /dev/null
+++ b/api-tests/management/ia/templates_test.go
@@ -0,0 +1,531 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package ia
+
+import (
+ "fmt"
+ "io/ioutil"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/AlekSi/pointer"
+ "github.com/brianvoe/gofakeit/v6"
+ "github.com/percona-platform/saas/pkg/alert"
+ templatesClient "github.com/percona/pmm/api/managementpb/ia/json/client"
+ "github.com/percona/pmm/api/managementpb/ia/json/client/rules"
+ "github.com/percona/pmm/api/managementpb/ia/json/client/templates"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "gopkg.in/yaml.v3"
+
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+// Note: Even though the IA services check for alerting enabled or disabled before returning results
+// we don't enable or disable IA explicit in our tests since it is enabled by default through
+// ENABLE_ALERTING env var.
+func assertTemplate(t *testing.T, expectedTemplate alert.Template, listTemplates []*templates.TemplatesItems0) {
+ convertParamUnit := func(u string) alert.Unit {
+ switch u {
+ case templates.TemplatesItems0ParamsItems0UnitPERCENTAGE:
+ return alert.Percentage
+ case templates.TemplatesItems0ParamsItems0UnitSECONDS:
+ return alert.Seconds
+ }
+ return "INVALID"
+ }
+ convertParamType := func(u string) alert.Type {
+ switch u {
+ case templates.TemplatesItems0ParamsItems0TypeFLOAT:
+ return alert.Float
+ case templates.TemplatesItems0ParamsItems0TypeSTRING:
+ return alert.String
+ case templates.TemplatesItems0ParamsItems0TypeBOOL:
+ return alert.Bool
+ }
+ return "INVALID"
+ }
+ var tmpl *templates.TemplatesItems0
+ for _, listTmpl := range listTemplates {
+ if listTmpl.Name == expectedTemplate.Name {
+ tmpl = listTmpl
+ break
+ }
+ }
+ require.NotNilf(t, tmpl, "template %s not found", expectedTemplate.Name)
+ // IDE doesn't recognize that require stops execution
+ if tmpl == nil {
+ return
+ }
+ assert.Equal(t, expectedTemplate.Expr, tmpl.Expr)
+ assert.Equal(t, expectedTemplate.Summary, tmpl.Summary)
+ assert.Equal(t, "USER_API", *tmpl.Source)
+ assert.Equal(t, "SEVERITY_WARNING", *tmpl.Severity)
+
+ forDuration := fmt.Sprintf("%.0fs", time.Duration(expectedTemplate.For).Seconds())
+ assert.Equal(t, forDuration, tmpl.For)
+
+ require.Len(t, tmpl.Params, len(expectedTemplate.Params))
+ for i, expectedParam := range expectedTemplate.Params {
+ param := tmpl.Params[i]
+ assert.Equal(t, expectedParam.Name, param.Name)
+ assert.Equal(t, expectedParam.Summary, param.Summary)
+ assert.Equal(t, expectedParam.Type, convertParamType(*param.Type))
+ assert.Equal(t, expectedParam.Unit, convertParamUnit(*param.Unit))
+ switch expectedParam.Type {
+ case alert.Float:
+ if expectedParam.Value != nil {
+ require.NotNil(t, param.Float)
+ value, err := expectedParam.GetValueForFloat()
+ require.NoError(t, err)
+ assert.True(t, param.Float.HasDefault)
+ assert.Equal(t, float32(value), param.Float.Default)
+ }
+
+ if len(expectedParam.Range) != 0 {
+ min, max, err := expectedParam.GetRangeForFloat()
+ require.NoError(t, err)
+ assert.True(t, param.Float.HasMax)
+ assert.True(t, param.Float.HasMin)
+ assert.Equal(t, float32(min), param.Float.Min)
+ assert.Equal(t, float32(max), param.Float.Max)
+ }
+
+ assert.Nil(t, param.Bool)
+ assert.Nil(t, param.String)
+ default:
+ }
+
+ }
+
+ assert.Equal(t, expectedTemplate.Labels, tmpl.Labels)
+ assert.Equal(t, expectedTemplate.Annotations, tmpl.Annotations)
+
+ assert.NotEmpty(t, tmpl.CreatedAt)
+}
+func TestAddTemplate(t *testing.T) {
+ client := templatesClient.Default.Templates
+
+ b, err := ioutil.ReadFile("../../testdata/ia/template.yaml")
+ require.NoError(t, err)
+
+ t.Run("normal", func(t *testing.T) {
+ name := gofakeit.UUID()
+ expr := gofakeit.UUID()
+ alertTemplates, yml := formatTemplateYaml(t, fmt.Sprintf(string(b), name, expr, "%", "s"))
+ _, err := client.CreateTemplate(&templates.CreateTemplateParams{
+ Body: templates.CreateTemplateBody{
+ Yaml: yml,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ defer deleteTemplate(t, client, name)
+
+ resp, err := client.ListTemplates(&templates.ListTemplatesParams{
+ Body: templates.ListTemplatesBody{
+ Reload: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ assertTemplate(t, alertTemplates[0], resp.Payload.Templates)
+ })
+
+ t.Run("duplicate", func(t *testing.T) {
+ name := gofakeit.UUID()
+ yaml := fmt.Sprintf(string(b), name, gofakeit.UUID(), "s", "%")
+ _, err := client.CreateTemplate(&templates.CreateTemplateParams{
+ Body: templates.CreateTemplateBody{
+ Yaml: yaml,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ defer deleteTemplate(t, client, name)
+
+ _, err = client.CreateTemplate(&templates.CreateTemplateParams{
+ Body: templates.CreateTemplateBody{
+ Yaml: yaml,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 409, codes.AlreadyExists, fmt.Sprintf("Template with name \"%s\" already exists.", name))
+ })
+
+ t.Run("invalid yaml", func(t *testing.T) {
+ _, err := client.CreateTemplate(&templates.CreateTemplateParams{
+ Body: templates.CreateTemplateBody{
+ Yaml: "not a yaml",
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Failed to parse rule template.")
+ })
+
+ t.Run("invalid template", func(t *testing.T) {
+ b, err := ioutil.ReadFile("../../testdata/ia/invalid-template.yaml")
+ require.NoError(t, err)
+ name := gofakeit.UUID()
+ _, err = client.CreateTemplate(&templates.CreateTemplateParams{
+ Body: templates.CreateTemplateBody{
+ Yaml: fmt.Sprintf(string(b), name, gofakeit.UUID()),
+ },
+ Context: pmmapitests.Context,
+ })
+
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Failed to parse rule template.")
+ })
+}
+
+func TestChangeTemplate(t *testing.T) {
+ client := templatesClient.Default.Templates
+
+ b, err := ioutil.ReadFile("../../testdata/ia/template.yaml")
+ require.NoError(t, err)
+
+ t.Run("normal", func(t *testing.T) {
+ name := gofakeit.UUID()
+ expr := gofakeit.UUID()
+ _, err := client.CreateTemplate(&templates.CreateTemplateParams{
+ Body: templates.CreateTemplateBody{
+ Yaml: fmt.Sprintf(string(b), name, expr, "s", "%"),
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ defer deleteTemplate(t, client, name)
+
+ newExpr := gofakeit.UUID()
+ alertTemplates, yml := formatTemplateYaml(t, fmt.Sprintf(string(b), name, newExpr, "s", "%"))
+ _, err = client.UpdateTemplate(&templates.UpdateTemplateParams{
+ Body: templates.UpdateTemplateBody{
+ Name: name,
+ Yaml: yml,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ resp, err := client.ListTemplates(&templates.ListTemplatesParams{
+ Body: templates.ListTemplatesBody{
+ Reload: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ assertTemplate(t, alertTemplates[0], resp.Payload.Templates)
+ })
+
+ t.Run("unknown template", func(t *testing.T) {
+ name := gofakeit.UUID()
+ _, err = client.UpdateTemplate(&templates.UpdateTemplateParams{
+ Body: templates.UpdateTemplateBody{
+ Name: name,
+ Yaml: fmt.Sprintf(string(b), name, gofakeit.UUID(), "s", "%"),
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, fmt.Sprintf("Template with name \"%s\" not found.", name))
+ })
+
+ t.Run("invalid yaml", func(t *testing.T) {
+ name := gofakeit.UUID()
+ _, err := client.CreateTemplate(&templates.CreateTemplateParams{
+ Body: templates.CreateTemplateBody{
+ Yaml: fmt.Sprintf(string(b), name, gofakeit.UUID(), "s", "%"),
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ defer deleteTemplate(t, client, name)
+
+ _, err = client.UpdateTemplate(&templates.UpdateTemplateParams{
+ Body: templates.UpdateTemplateBody{
+ Name: name,
+ Yaml: "not a yaml",
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Failed to parse rule template.")
+ })
+
+ t.Run("invalid template", func(t *testing.T) {
+ name := gofakeit.UUID()
+ _, err = client.CreateTemplate(&templates.CreateTemplateParams{
+ Body: templates.CreateTemplateBody{
+ Yaml: fmt.Sprintf(string(b), name, gofakeit.UUID(), "s", "%"),
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ defer deleteTemplate(t, client, name)
+
+ b, err = ioutil.ReadFile("../../testdata/ia/invalid-template.yaml")
+ _, err = client.UpdateTemplate(&templates.UpdateTemplateParams{
+ Body: templates.UpdateTemplateBody{
+ Name: name,
+ Yaml: fmt.Sprintf(string(b), name, gofakeit.UUID()),
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Failed to parse rule template.")
+ })
+}
+
+func TestDeleteTemplate(t *testing.T) {
+ client := templatesClient.Default.Templates
+
+ b, err := ioutil.ReadFile("../../testdata/ia/template.yaml")
+ require.NoError(t, err)
+
+ t.Run("normal", func(t *testing.T) {
+ name := gofakeit.UUID()
+ _, err := client.CreateTemplate(&templates.CreateTemplateParams{
+ Body: templates.CreateTemplateBody{
+ Yaml: fmt.Sprintf(string(b), name, gofakeit.UUID(), "s", "%"),
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ _, err = client.DeleteTemplate(&templates.DeleteTemplateParams{
+ Body: templates.DeleteTemplateBody{
+ Name: name,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ resp, err := client.ListTemplates(&templates.ListTemplatesParams{
+ Body: templates.ListTemplatesBody{
+ Reload: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ for _, template := range resp.Payload.Templates {
+ assert.NotEqual(t, name, template.Name)
+ }
+ })
+
+ t.Run("template in use", func(t *testing.T) {
+ name := gofakeit.UUID()
+ _, err := client.CreateTemplate(&templates.CreateTemplateParams{
+ Body: templates.CreateTemplateBody{
+ Yaml: fmt.Sprintf(string(b), name, gofakeit.UUID(), "s", "%"),
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ defer deleteTemplate(t, templatesClient.Default.Templates, name)
+
+ channelID := createChannel(t)
+ defer deleteChannel(t, templatesClient.Default.Channels, channelID)
+
+ params := createAlertRuleParams(name, channelID, "param2", &rules.FiltersItems0{
+ Type: pointer.ToString("EQUAL"),
+ Key: "threshold",
+ Value: "12",
+ })
+
+ rule, err := templatesClient.Default.Rules.CreateAlertRule(params)
+ require.NoError(t, err)
+ defer deleteRule(t, templatesClient.Default.Rules, rule.Payload.RuleID)
+
+ _, err = client.DeleteTemplate(&templates.DeleteTemplateParams{
+ Body: templates.DeleteTemplateBody{
+ Name: name,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.FailedPrecondition, "Failed to delete rule template %s, as it is being used by some rule.", name)
+
+ resp, err := client.ListTemplates(&templates.ListTemplatesParams{
+ Body: templates.ListTemplatesBody{
+ Reload: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ var found bool
+ for _, template := range resp.Payload.Templates {
+ if name == template.Name {
+ found = true
+ }
+ }
+ assert.Truef(t, found, "Template with id %s not found", name)
+ })
+
+ t.Run("unknown template", func(t *testing.T) {
+ name := gofakeit.UUID()
+ _, err = client.DeleteTemplate(&templates.DeleteTemplateParams{
+ Body: templates.DeleteTemplateBody{
+ Name: name,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, fmt.Sprintf("Template with name \"%s\" not found.", name))
+ })
+}
+
+func TestListTemplate(t *testing.T) {
+ client := templatesClient.Default.Templates
+
+ b, err := ioutil.ReadFile("../../testdata/ia/template.yaml")
+ require.NoError(t, err)
+
+ name := gofakeit.UUID()
+ expr := gofakeit.UUID()
+ alertTemplates, yml := formatTemplateYaml(t, fmt.Sprintf(string(b), name, expr, "%", "s"))
+ _, err = client.CreateTemplate(&templates.CreateTemplateParams{
+ Body: templates.CreateTemplateBody{
+ Yaml: yml,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ defer deleteTemplate(t, client, name)
+
+ t.Run("without pagination", func(t *testing.T) {
+ resp, err := client.ListTemplates(&templates.ListTemplatesParams{
+ Body: templates.ListTemplatesBody{
+ Reload: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ assertTemplate(t, alertTemplates[0], resp.Payload.Templates)
+ })
+
+ t.Run("with pagination", func(t *testing.T) {
+ const templatesCount = 5
+
+ templateNames := make(map[string]struct{})
+
+ for i := 0; i < templatesCount; i++ {
+ name := gofakeit.UUID()
+ expr := gofakeit.UUID()
+ _, yml := formatTemplateYaml(t, fmt.Sprintf(string(b), name, expr, "%", "s"))
+ _, err = client.CreateTemplate(&templates.CreateTemplateParams{
+ Body: templates.CreateTemplateBody{
+ Yaml: yml,
+ },
+ Context: pmmapitests.Context,
+ })
+
+ templateNames[name] = struct{}{}
+ }
+ defer func() {
+ for name := range templateNames {
+ deleteTemplate(t, client, name)
+ }
+ }()
+
+ // list rules, so they are all on the first page
+ body := templates.ListTemplatesBody{
+ PageParams: &templates.ListTemplatesParamsBodyPageParams{
+ PageSize: 20,
+ Index: 0,
+ },
+ }
+ listAllTemplates, err := client.ListTemplates(&templates.ListTemplatesParams{
+ Body: body,
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ assert.GreaterOrEqual(t, len(listAllTemplates.Payload.Templates), templatesCount)
+ assert.Equal(t, int32(len(listAllTemplates.Payload.Templates)), listAllTemplates.Payload.Totals.TotalItems)
+ assert.Equal(t, int32(1), listAllTemplates.Payload.Totals.TotalPages)
+
+ assertFindTemplate := func(list []*templates.TemplatesItems0, name string) func() bool {
+ return func() bool {
+ for _, tmpl := range list {
+ if tmpl.Name == name {
+ return true
+ }
+ }
+ return false
+ }
+ }
+
+ for name := range templateNames {
+ assert.Conditionf(t, assertFindTemplate(listAllTemplates.Payload.Templates, name), "template %s not found", name)
+ }
+
+ // paginate page over page with page size 1 and check the order - it should be the same as in listAllTemplates.
+ // last iteration checks that there is no elements for not existing page.
+ for pageIndex := 0; pageIndex <= len(listAllTemplates.Payload.Templates); pageIndex++ {
+ body := templates.ListTemplatesBody{
+ PageParams: &templates.ListTemplatesParamsBodyPageParams{
+ PageSize: 1,
+ Index: int32(pageIndex),
+ },
+ }
+ listOneTemplate, err := client.ListTemplates(&templates.ListTemplatesParams{
+ Body: body, Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ assert.Equal(t, listAllTemplates.Payload.Totals.TotalItems, listOneTemplate.Payload.Totals.TotalItems)
+ assert.GreaterOrEqual(t, listOneTemplate.Payload.Totals.TotalPages, int32(templatesCount))
+
+ if pageIndex != len(listAllTemplates.Payload.Templates) {
+ require.Len(t, listOneTemplate.Payload.Templates, 1)
+ assert.Equal(t, listAllTemplates.Payload.Templates[pageIndex].Name, listOneTemplate.Payload.Templates[0].Name)
+ } else {
+ assert.Len(t, listOneTemplate.Payload.Templates, 0)
+ }
+ }
+
+ })
+}
+
+func deleteTemplate(t *testing.T, client templates.ClientService, name string) {
+ _, err := client.DeleteTemplate(&templates.DeleteTemplateParams{
+ Body: templates.DeleteTemplateBody{
+ Name: name,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+}
+
+func formatTemplateYaml(t *testing.T, yml string) ([]alert.Template, string) {
+ params := &alert.ParseParams{
+ DisallowUnknownFields: true,
+ DisallowInvalidTemplates: true,
+ }
+ r, err := alert.Parse(strings.NewReader(yml), params)
+ require.NoError(t, err)
+ type yamlTemplates struct {
+ Templates []alert.Template `yaml:"templates"`
+ }
+ s, err := yaml.Marshal(&yamlTemplates{Templates: r})
+ require.NoError(t, err)
+
+ return r, string(s)
+}
diff --git a/api-tests/management/mongodb_test.go b/api-tests/management/mongodb_test.go
new file mode 100644
index 0000000000..cc5185acc7
--- /dev/null
+++ b/api-tests/management/mongodb_test.go
@@ -0,0 +1,984 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package management
+
+import (
+ "testing"
+
+ "github.com/AlekSi/pointer"
+ inventoryClient "github.com/percona/pmm/api/inventorypb/json/client"
+ "github.com/percona/pmm/api/inventorypb/json/client/agents"
+ "github.com/percona/pmm/api/inventorypb/json/client/services"
+ "github.com/percona/pmm/api/managementpb/json/client"
+ mongodb "github.com/percona/pmm/api/managementpb/json/client/mongo_db"
+ "github.com/percona/pmm/api/managementpb/json/client/node"
+ "github.com/percona/pmm/api/managementpb/json/client/service"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestAddMongoDB(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-for-basic-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-name-for-basic-name")
+
+ params := &mongodb.AddMongoDBParams{
+ Context: pmmapitests.Context,
+ Body: mongodb.AddMongoDBBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 27017,
+
+ SkipConnectionCheck: true,
+ DisableCollectors: []string{"database"},
+ },
+ }
+ addMongoDBOK, err := client.Default.MongoDB.AddMongoDB(params)
+ require.NoError(t, err)
+ require.NotNil(t, addMongoDBOK)
+ require.NotNil(t, addMongoDBOK.Payload.Service)
+ serviceID := addMongoDBOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ Mongodb: &services.GetServiceOKBodyMongodb{
+ ServiceID: serviceID,
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 27017,
+ },
+ }, *serviceOK.Payload)
+
+ // Check that mongodb exporter is added by default.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, agents.ListAgentsOKBody{
+ MongodbExporter: []*agents.MongodbExporterItems0{
+ {
+ AgentID: listAgents.Payload.MongodbExporter[0].AgentID,
+ ServiceID: serviceID,
+ PMMAgentID: pmmAgentID,
+ DisabledCollectors: []string{"database"},
+ PushMetricsEnabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, *listAgents.Payload)
+ defer removeAllAgentsInList(t, listAgents)
+ })
+
+ t.Run("With agents", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name-for-all-fields")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-name-for-all-fields")
+
+ params := &mongodb.AddMongoDBParams{
+ Context: pmmapitests.Context,
+ Body: mongodb.AddMongoDBBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 27017,
+ Username: "username",
+ Password: "password",
+ QANMongodbProfiler: true,
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addMongoDBOK, err := client.Default.MongoDB.AddMongoDB(params)
+ require.NoError(t, err)
+ require.NotNil(t, addMongoDBOK)
+ require.NotNil(t, addMongoDBOK.Payload.Service)
+ serviceID := addMongoDBOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ Mongodb: &services.GetServiceOKBodyMongodb{
+ ServiceID: serviceID,
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 27017,
+ },
+ }, *serviceOK.Payload)
+
+ // Check that exporters are added.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, listAgents)
+ defer removeAllAgentsInList(t, listAgents)
+
+ require.Len(t, listAgents.Payload.MongodbExporter, 1)
+ require.Len(t, listAgents.Payload.QANMongodbProfilerAgent, 1)
+ assert.Equal(t, agents.ListAgentsOKBody{
+ MongodbExporter: []*agents.MongodbExporterItems0{
+ {
+ AgentID: listAgents.Payload.MongodbExporter[0].AgentID,
+ ServiceID: serviceID,
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ PushMetricsEnabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ QANMongodbProfilerAgent: []*agents.QANMongodbProfilerAgentItems0{
+ {
+ AgentID: listAgents.Payload.QANMongodbProfilerAgent[0].AgentID,
+ ServiceID: serviceID,
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, *listAgents.Payload)
+ })
+
+ t.Run("With labels", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name-for-all-fields")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-name-for-all-fields")
+
+ params := &mongodb.AddMongoDBParams{
+ Context: pmmapitests.Context,
+ Body: mongodb.AddMongoDBBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 27017,
+ Environment: "some-environment",
+ Cluster: "cluster-name",
+ ReplicationSet: "replication-set",
+ CustomLabels: map[string]string{"bar": "foo"},
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addMongoDBOK, err := client.Default.MongoDB.AddMongoDB(params)
+ require.NoError(t, err)
+ require.NotNil(t, addMongoDBOK)
+ require.NotNil(t, addMongoDBOK.Payload.Service)
+ serviceID := addMongoDBOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+ defer removeServiceAgents(t, serviceID)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ Mongodb: &services.GetServiceOKBodyMongodb{
+ ServiceID: serviceID,
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 27017,
+ Environment: "some-environment",
+ Cluster: "cluster-name",
+ ReplicationSet: "replication-set",
+ CustomLabels: map[string]string{"bar": "foo"},
+ },
+ }, *serviceOK.Payload)
+ })
+
+ t.Run("With the same name", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-for-the-same-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-for-the-same-name")
+
+ params := &mongodb.AddMongoDBParams{
+ Context: pmmapitests.Context,
+ Body: mongodb.AddMongoDBBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 27017,
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addMongoDBOK, err := client.Default.MongoDB.AddMongoDB(params)
+ require.NoError(t, err)
+ require.NotNil(t, addMongoDBOK)
+ require.NotNil(t, addMongoDBOK.Payload.Service)
+ serviceID := addMongoDBOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+ defer removeServiceAgents(t, serviceID)
+
+ params = &mongodb.AddMongoDBParams{
+ Context: pmmapitests.Context,
+ Body: mongodb.AddMongoDBBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "11.11.11.11",
+ Port: 27017,
+ },
+ }
+ addMongoDBOK, err = client.Default.MongoDB.AddMongoDB(params)
+ require.Nil(t, addMongoDBOK)
+ pmmapitests.AssertAPIErrorf(t, err, 409, codes.AlreadyExists, `Service with name %q already exists.`, serviceName)
+ })
+
+ t.Run("With add_node block", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-for-basic-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ nodeNameAddNode := pmmapitests.TestString(t, "node-for-add-node-name")
+ serviceName := pmmapitests.TestString(t, "service-name-for-basic-name")
+
+ params := &mongodb.AddMongoDBParams{
+ Context: pmmapitests.Context,
+ Body: mongodb.AddMongoDBBody{
+ AddNode: &mongodb.AddMongoDBParamsBodyAddNode{
+ NodeType: pointer.ToString(mongodb.AddMongoDBParamsBodyAddNodeNodeTypeGENERICNODE),
+ NodeName: nodeNameAddNode,
+ },
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 27017,
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addMongoDBOK, err := client.Default.MongoDB.AddMongoDB(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "add_node structure can be used only for remote nodes")
+
+ params = &mongodb.AddMongoDBParams{
+ Context: pmmapitests.Context,
+ Body: mongodb.AddMongoDBBody{
+ AddNode: &mongodb.AddMongoDBParamsBodyAddNode{
+ NodeType: pointer.ToString(mongodb.AddMongoDBParamsBodyAddNodeNodeTypeREMOTERDSNODE),
+ NodeName: nodeNameAddNode,
+ },
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 27017,
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addMongoDBOK, err = client.Default.MongoDB.AddMongoDB(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "add_node structure can be used only for remote nodes")
+
+ params = &mongodb.AddMongoDBParams{
+ Context: pmmapitests.Context,
+ Body: mongodb.AddMongoDBBody{
+ AddNode: &mongodb.AddMongoDBParamsBodyAddNode{
+ NodeType: pointer.ToString(mongodb.AddMongoDBParamsBodyAddNodeNodeTypeREMOTENODE),
+ NodeName: nodeNameAddNode,
+ },
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 27017,
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addMongoDBOK, err = client.Default.MongoDB.AddMongoDB(params)
+ require.NoError(t, err)
+ require.NotNil(t, addMongoDBOK)
+ require.NotNil(t, addMongoDBOK.Payload.Service)
+ serviceID := addMongoDBOK.Payload.Service.ServiceID
+
+ newNodeID := addMongoDBOK.Payload.Service.NodeID
+ require.NotEqual(t, nodeID, newNodeID)
+ defer pmmapitests.RemoveNodes(t, newNodeID)
+ defer pmmapitests.RemoveServices(t, serviceID)
+ defer removeServiceAgents(t, serviceID)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ Mongodb: &services.GetServiceOKBodyMongodb{
+ ServiceID: serviceID,
+ NodeID: newNodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 27017,
+ },
+ }, *serviceOK.Payload)
+
+ // Check that mongodb exporter is added by default.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, agents.ListAgentsOKBody{
+ MongodbExporter: []*agents.MongodbExporterItems0{
+ {
+ AgentID: listAgents.Payload.MongodbExporter[0].AgentID,
+ ServiceID: serviceID,
+ PMMAgentID: pmmAgentID,
+ PushMetricsEnabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, *listAgents.Payload)
+ defer removeAllAgentsInList(t, listAgents)
+ })
+
+ t.Run("With Wrong Node Type", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "generic-node-for-wrong-node-type")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ remoteNodeOKBody := pmmapitests.AddRemoteNode(t, pmmapitests.TestString(t, "Remote Node for wrong type test"))
+ remoteNodeID := remoteNodeOKBody.Remote.NodeID
+ defer pmmapitests.RemoveNodes(t, remoteNodeID)
+
+ serviceName := pmmapitests.TestString(t, "service-name")
+ params := &mongodb.AddMongoDBParams{
+ Context: pmmapitests.Context,
+ Body: mongodb.AddMongoDBBody{
+ NodeID: remoteNodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ PMMAgentID: pmmAgentID,
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addMongoDBOK, err := client.Default.MongoDB.AddMongoDB(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "node_id or node_name can be used only for generic nodes or container nodes")
+ assert.Nil(t, addMongoDBOK)
+ })
+
+ t.Run("Empty Service Name", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ params := &mongodb.AddMongoDBParams{
+ Context: pmmapitests.Context,
+ Body: mongodb.AddMongoDBBody{NodeID: nodeID},
+ }
+ addMongoDBOK, err := client.Default.MongoDB.AddMongoDB(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field ServiceName: value '' must not be an empty string")
+ assert.Nil(t, addMongoDBOK)
+ })
+
+ t.Run("Empty Address", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-name")
+ params := &mongodb.AddMongoDBParams{
+ Context: pmmapitests.Context,
+ Body: mongodb.AddMongoDBBody{
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ PMMAgentID: pmmAgentID,
+ },
+ }
+ addMongoDBOK, err := client.Default.MongoDB.AddMongoDB(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Neither socket nor address passed.")
+ assert.Nil(t, addMongoDBOK)
+ })
+
+ t.Run("Empty Port", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-name")
+ params := &mongodb.AddMongoDBParams{
+ Context: pmmapitests.Context,
+ Body: mongodb.AddMongoDBBody{
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ PMMAgentID: pmmAgentID,
+ },
+ }
+ addMongoDBOK, err := client.Default.MongoDB.AddMongoDB(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Port are expected to be passed with address.")
+ assert.Nil(t, addMongoDBOK)
+ })
+
+ t.Run("Empty Pmm Agent ID", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-name")
+ params := &mongodb.AddMongoDBParams{
+ Context: pmmapitests.Context,
+ Body: mongodb.AddMongoDBBody{
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ },
+ }
+ addMongoDBOK, err := client.Default.MongoDB.AddMongoDB(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field PmmAgentId: value '' must not be an empty string")
+ assert.Nil(t, addMongoDBOK)
+ })
+
+ t.Run("Address And Socket Conflict.", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-name")
+ params := &mongodb.AddMongoDBParams{
+ Context: pmmapitests.Context,
+ Body: mongodb.AddMongoDBBody{
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ Password: "password",
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 27017,
+ Socket: "/tmp/mongodb-27017.sock",
+ },
+ }
+ addProxySQLOK, err := client.Default.MongoDB.AddMongoDB(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Socket and address cannot be specified together.")
+ assert.Nil(t, addProxySQLOK)
+ })
+
+ t.Run("Socket", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-for-mongo-socket-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-name-for-mongo-socket-name")
+
+ params := &mongodb.AddMongoDBParams{
+ Context: pmmapitests.Context,
+ Body: mongodb.AddMongoDBBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Socket: "/tmp/mongodb-27017.sock",
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addMongoDBOK, err := client.Default.MongoDB.AddMongoDB(params)
+ require.NoError(t, err)
+ require.NotNil(t, addMongoDBOK)
+ require.NotNil(t, addMongoDBOK.Payload.Service)
+ serviceID := addMongoDBOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ Mongodb: &services.GetServiceOKBodyMongodb{
+ ServiceID: serviceID,
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Socket: "/tmp/mongodb-27017.sock",
+ },
+ }, *serviceOK.Payload)
+
+ // Check that mongodb exporter is added by default.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, agents.ListAgentsOKBody{
+ MongodbExporter: []*agents.MongodbExporterItems0{
+ {
+ AgentID: listAgents.Payload.MongodbExporter[0].AgentID,
+ ServiceID: serviceID,
+ PMMAgentID: pmmAgentID,
+ PushMetricsEnabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, *listAgents.Payload)
+ defer removeAllAgentsInList(t, listAgents)
+ })
+
+ t.Run("With MetricsModePush", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-for-basic-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-name-for-basic-name")
+
+ params := &mongodb.AddMongoDBParams{
+ Context: pmmapitests.Context,
+ Body: mongodb.AddMongoDBBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 27017,
+
+ SkipConnectionCheck: true,
+ MetricsMode: pointer.ToString("PUSH"),
+ },
+ }
+ addMongoDBOK, err := client.Default.MongoDB.AddMongoDB(params)
+ require.NoError(t, err)
+ require.NotNil(t, addMongoDBOK)
+ require.NotNil(t, addMongoDBOK.Payload.Service)
+ serviceID := addMongoDBOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ Mongodb: &services.GetServiceOKBodyMongodb{
+ ServiceID: serviceID,
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 27017,
+ },
+ }, *serviceOK.Payload)
+
+ // Check that mongodb exporter is added by default.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, agents.ListAgentsOKBody{
+ MongodbExporter: []*agents.MongodbExporterItems0{
+ {
+ AgentID: listAgents.Payload.MongodbExporter[0].AgentID,
+ ServiceID: serviceID,
+ PMMAgentID: pmmAgentID,
+ PushMetricsEnabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, *listAgents.Payload)
+ defer removeAllAgentsInList(t, listAgents)
+ })
+
+ t.Run("With MetricsModePull", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-for-basic-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-name-for-basic-name")
+
+ params := &mongodb.AddMongoDBParams{
+ Context: pmmapitests.Context,
+ Body: mongodb.AddMongoDBBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 27017,
+
+ SkipConnectionCheck: true,
+ MetricsMode: pointer.ToString("PULL"),
+ },
+ }
+ addMongoDBOK, err := client.Default.MongoDB.AddMongoDB(params)
+ require.NoError(t, err)
+ require.NotNil(t, addMongoDBOK)
+ require.NotNil(t, addMongoDBOK.Payload.Service)
+ serviceID := addMongoDBOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ Mongodb: &services.GetServiceOKBodyMongodb{
+ ServiceID: serviceID,
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 27017,
+ },
+ }, *serviceOK.Payload)
+
+ // Check that mongodb exporter is added by default.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, agents.ListAgentsOKBody{
+ MongodbExporter: []*agents.MongodbExporterItems0{
+ {
+ AgentID: listAgents.Payload.MongodbExporter[0].AgentID,
+ ServiceID: serviceID,
+ PMMAgentID: pmmAgentID,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, *listAgents.Payload)
+ defer removeAllAgentsInList(t, listAgents)
+ })
+
+ t.Run("With MetricsModeAuto", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-for-basic-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-name-for-basic-name")
+
+ params := &mongodb.AddMongoDBParams{
+ Context: pmmapitests.Context,
+ Body: mongodb.AddMongoDBBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 27017,
+
+ SkipConnectionCheck: true,
+ MetricsMode: pointer.ToString("AUTO"),
+ },
+ }
+ addMongoDBOK, err := client.Default.MongoDB.AddMongoDB(params)
+ require.NoError(t, err)
+ require.NotNil(t, addMongoDBOK)
+ require.NotNil(t, addMongoDBOK.Payload.Service)
+ serviceID := addMongoDBOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ Mongodb: &services.GetServiceOKBodyMongodb{
+ ServiceID: serviceID,
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 27017,
+ },
+ }, *serviceOK.Payload)
+
+ // Check that mongodb exporter is added by default.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, agents.ListAgentsOKBody{
+ MongodbExporter: []*agents.MongodbExporterItems0{
+ {
+ AgentID: listAgents.Payload.MongodbExporter[0].AgentID,
+ ServiceID: serviceID,
+ PMMAgentID: pmmAgentID,
+ PushMetricsEnabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, *listAgents.Payload)
+ defer removeAllAgentsInList(t, listAgents)
+ })
+}
+
+func TestRemoveMongoDB(t *testing.T) {
+ addMongoDB := func(t *testing.T, serviceName, nodeName string, withAgents bool) (nodeID string, pmmAgentID string, serviceID string) {
+ t.Helper()
+ nodeID, pmmAgentID = RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+
+ params := &mongodb.AddMongoDBParams{
+ Context: pmmapitests.Context,
+ Body: mongodb.AddMongoDBBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 27017,
+ Username: "username",
+ Password: "password",
+ QANMongodbProfiler: withAgents,
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addMongoDBOK, err := client.Default.MongoDB.AddMongoDB(params)
+ require.NoError(t, err)
+ require.NotNil(t, addMongoDBOK)
+ require.NotNil(t, addMongoDBOK.Payload.Service)
+ serviceID = addMongoDBOK.Payload.Service.ServiceID
+ return
+ }
+
+ t.Run("By name", func(t *testing.T) {
+ serviceName := pmmapitests.TestString(t, "service-remove-by-name")
+ nodeName := pmmapitests.TestString(t, "node-remove-by-name")
+ nodeID, pmmAgentID, serviceID := addMongoDB(t, serviceName, nodeName, true)
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{
+ Body: service.RemoveServiceBody{
+ ServiceName: serviceName,
+ ServiceType: pointer.ToString(service.RemoveServiceBodyServiceTypeMONGODBSERVICE),
+ },
+ Context: pmmapitests.Context,
+ })
+ noError := assert.NoError(t, err)
+ notNil := assert.NotNil(t, removeServiceOK)
+ if !noError || !notNil {
+ defer pmmapitests.RemoveServices(t, serviceID)
+ }
+
+ // Check that the service removed with agents.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Service with ID %q not found.", serviceID)
+ assert.Nil(t, listAgents)
+ })
+
+ t.Run("By ID", func(t *testing.T) {
+ serviceName := pmmapitests.TestString(t, "service-remove-by-id")
+ nodeName := pmmapitests.TestString(t, "node-remove-by-id")
+ nodeID, pmmAgentID, serviceID := addMongoDB(t, serviceName, nodeName, true)
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{
+ Body: service.RemoveServiceBody{
+ ServiceID: serviceID,
+ ServiceType: pointer.ToString(service.RemoveServiceBodyServiceTypeMONGODBSERVICE),
+ },
+ Context: pmmapitests.Context,
+ })
+ noError := assert.NoError(t, err)
+ notNil := assert.NotNil(t, removeServiceOK)
+ if !noError || !notNil {
+ defer pmmapitests.RemoveServices(t, serviceID)
+ }
+
+ // Check that the service removed with agents.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Service with ID %q not found.", serviceID)
+ assert.Nil(t, listAgents)
+ })
+
+ t.Run("Both params", func(t *testing.T) {
+ serviceName := pmmapitests.TestString(t, "service-remove-both-params")
+ nodeName := pmmapitests.TestString(t, "node-remove-both-params")
+ nodeID, pmmAgentID, serviceID := addMongoDB(t, serviceName, nodeName, false)
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer pmmapitests.RemoveServices(t, serviceID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{
+ Body: service.RemoveServiceBody{
+ ServiceID: serviceID,
+ ServiceName: serviceName,
+ ServiceType: pointer.ToString(service.RemoveServiceBodyServiceTypeMYSQLSERVICE),
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.Nil(t, removeServiceOK)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "service_id or service_name expected; not both")
+ })
+
+ t.Run("Wrong type", func(t *testing.T) {
+ serviceName := pmmapitests.TestString(t, "service-remove-wrong-type")
+ nodeName := pmmapitests.TestString(t, "node-remove-wrong-type")
+ nodeID, pmmAgentID, serviceID := addMongoDB(t, serviceName, nodeName, false)
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer pmmapitests.RemoveServices(t, serviceID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{
+ Body: service.RemoveServiceBody{
+ ServiceID: serviceID,
+ ServiceType: pointer.ToString(service.RemoveServiceBodyServiceTypePOSTGRESQLSERVICE),
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.Nil(t, removeServiceOK)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "wrong service type")
+ })
+}
diff --git a/api-tests/management/mysql_test.go b/api-tests/management/mysql_test.go
new file mode 100644
index 0000000000..b33e2a9baf
--- /dev/null
+++ b/api-tests/management/mysql_test.go
@@ -0,0 +1,990 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package management
+
+import (
+ "testing"
+
+ "github.com/AlekSi/pointer"
+ inventoryClient "github.com/percona/pmm/api/inventorypb/json/client"
+ "github.com/percona/pmm/api/inventorypb/json/client/agents"
+ "github.com/percona/pmm/api/inventorypb/json/client/services"
+ "github.com/percona/pmm/api/managementpb/json/client"
+ mysql "github.com/percona/pmm/api/managementpb/json/client/my_sql"
+ "github.com/percona/pmm/api/managementpb/json/client/node"
+ "github.com/percona/pmm/api/managementpb/json/client/service"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestAddMySQL(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-for-basic-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-for-basic-name")
+
+ params := &mysql.AddMySQLParams{
+ Context: pmmapitests.Context,
+ Body: mysql.AddMySQLBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ Username: "username",
+
+ SkipConnectionCheck: true,
+ DisableCollectors: []string{"global_status", "perf_schema.tablelocks"},
+ },
+ }
+ addMySQLOK, err := client.Default.MySQL.AddMySQL(params)
+ require.NoError(t, err)
+ require.NotNil(t, addMySQLOK)
+ require.NotNil(t, addMySQLOK.Payload.Service)
+ serviceID := addMySQLOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ Mysql: &services.GetServiceOKBodyMysql{
+ ServiceID: serviceID,
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ },
+ }, *serviceOK.Payload)
+
+ // Check that mysqld exporter is added by default.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, agents.ListAgentsOKBody{
+ MysqldExporter: []*agents.MysqldExporterItems0{
+ {
+ AgentID: listAgents.Payload.MysqldExporter[0].AgentID,
+ ServiceID: serviceID,
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ TablestatsGroupTableLimit: 1000,
+ DisabledCollectors: []string{"global_status", "perf_schema.tablelocks"},
+ PushMetricsEnabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, *listAgents.Payload)
+ defer removeAllAgentsInList(t, listAgents)
+ })
+
+ t.Run("With agents", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-for-all-fields-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-for-all-fields-name")
+
+ params := &mysql.AddMySQLParams{
+ Context: pmmapitests.Context,
+ Body: mysql.AddMySQLBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ Username: "username",
+ Password: "password",
+ QANMysqlSlowlog: true,
+ QANMysqlPerfschema: true,
+
+ SkipConnectionCheck: true,
+ TablestatsGroupTableLimit: -1,
+ },
+ }
+ addMySQLOK, err := client.Default.MySQL.AddMySQL(params)
+ require.NoError(t, err)
+ require.NotNil(t, addMySQLOK)
+ require.NotNil(t, addMySQLOK.Payload.Service)
+ serviceID := addMySQLOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ Mysql: &services.GetServiceOKBodyMysql{
+ ServiceID: serviceID,
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ },
+ }, *serviceOK.Payload)
+
+ // Check that exporters are added.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, listAgents)
+ defer removeAllAgentsInList(t, listAgents)
+ require.Len(t, listAgents.Payload.MysqldExporter, 1)
+ require.Len(t, listAgents.Payload.QANMysqlSlowlogAgent, 1)
+ require.Len(t, listAgents.Payload.QANMysqlPerfschemaAgent, 1)
+ assert.Equal(t, agents.ListAgentsOKBody{
+ MysqldExporter: []*agents.MysqldExporterItems0{
+ {
+ AgentID: listAgents.Payload.MysqldExporter[0].AgentID,
+ ServiceID: serviceID,
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ TablestatsGroupTableLimit: -1,
+ TablestatsGroupDisabled: true,
+ PushMetricsEnabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ QANMysqlSlowlogAgent: []*agents.QANMysqlSlowlogAgentItems0{
+ {
+ AgentID: listAgents.Payload.QANMysqlSlowlogAgent[0].AgentID,
+ ServiceID: serviceID,
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ MaxSlowlogFileSize: "1073741824",
+ Status: &AgentStatusUnknown,
+ },
+ },
+ QANMysqlPerfschemaAgent: []*agents.QANMysqlPerfschemaAgentItems0{
+ {
+ AgentID: listAgents.Payload.QANMysqlPerfschemaAgent[0].AgentID,
+ ServiceID: serviceID,
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, *listAgents.Payload)
+ })
+
+ t.Run("With labels", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-for-all-fields-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-for-all-fields-name")
+
+ params := &mysql.AddMySQLParams{
+ Context: pmmapitests.Context,
+ Body: mysql.AddMySQLBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ Username: "username",
+ Password: "password",
+ Environment: "some-environment",
+ Cluster: "cluster-name",
+ ReplicationSet: "replication-set",
+ CustomLabels: map[string]string{"bar": "foo"},
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addMySQLOK, err := client.Default.MySQL.AddMySQL(params)
+ require.NoError(t, err)
+ require.NotNil(t, addMySQLOK)
+ require.NotNil(t, addMySQLOK.Payload.Service)
+ serviceID := addMySQLOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+ defer removeServiceAgents(t, serviceID)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ Mysql: &services.GetServiceOKBodyMysql{
+ ServiceID: serviceID,
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ Environment: "some-environment",
+ Cluster: "cluster-name",
+ ReplicationSet: "replication-set",
+ CustomLabels: map[string]string{"bar": "foo"},
+ },
+ }, *serviceOK.Payload)
+ })
+
+ t.Run("With the same name", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-for-the-same-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-for-the-same-name")
+
+ params := &mysql.AddMySQLParams{
+ Context: pmmapitests.Context,
+ Body: mysql.AddMySQLBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ Username: "username",
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addMySQLOK, err := client.Default.MySQL.AddMySQL(params)
+ require.NoError(t, err)
+ require.NotNil(t, addMySQLOK)
+ require.NotNil(t, addMySQLOK.Payload.Service)
+ serviceID := addMySQLOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+ defer removeServiceAgents(t, serviceID)
+
+ params = &mysql.AddMySQLParams{
+ Context: pmmapitests.Context,
+ Body: mysql.AddMySQLBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "11.11.11.11",
+ Port: 3307,
+ Username: "username",
+ },
+ }
+ addMySQLOK, err = client.Default.MySQL.AddMySQL(params)
+ require.Nil(t, addMySQLOK)
+ pmmapitests.AssertAPIErrorf(t, err, 409, codes.AlreadyExists, `Service with name %q already exists.`, serviceName)
+ })
+
+ t.Run("With add_node block", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-for-basic-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ nodeNameAddNode := pmmapitests.TestString(t, "node-for-add-node-name")
+ serviceName := pmmapitests.TestString(t, "service-name-for-basic-name")
+
+ params := &mysql.AddMySQLParams{
+ Context: pmmapitests.Context,
+ Body: mysql.AddMySQLBody{
+ AddNode: &mysql.AddMySQLParamsBodyAddNode{
+ NodeType: pointer.ToString(mysql.AddMySQLParamsBodyAddNodeNodeTypeGENERICNODE),
+ NodeName: nodeNameAddNode,
+ },
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 27017,
+ Username: "username",
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addMySQLOK, err := client.Default.MySQL.AddMySQL(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "add_node structure can be used only for remote nodes")
+
+ params = &mysql.AddMySQLParams{
+ Context: pmmapitests.Context,
+ Body: mysql.AddMySQLBody{
+ AddNode: &mysql.AddMySQLParamsBodyAddNode{
+ NodeType: pointer.ToString(mysql.AddMySQLParamsBodyAddNodeNodeTypeREMOTERDSNODE),
+ NodeName: nodeNameAddNode,
+ },
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 27017,
+ Username: "username",
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addMySQLOK, err = client.Default.MySQL.AddMySQL(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "add_node structure can be used only for remote nodes")
+
+ params = &mysql.AddMySQLParams{
+ Context: pmmapitests.Context,
+ Body: mysql.AddMySQLBody{
+ AddNode: &mysql.AddMySQLParamsBodyAddNode{
+ NodeType: pointer.ToString(mysql.AddMySQLParamsBodyAddNodeNodeTypeREMOTENODE),
+ NodeName: nodeNameAddNode,
+ },
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 27017,
+ Username: "username",
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addMySQLOK, err = client.Default.MySQL.AddMySQL(params)
+ require.NoError(t, err)
+ require.NotNil(t, addMySQLOK)
+ require.NotNil(t, addMySQLOK.Payload.Service)
+ serviceID := addMySQLOK.Payload.Service.ServiceID
+
+ newNodeID := addMySQLOK.Payload.Service.NodeID
+ require.NotEqual(t, nodeID, newNodeID)
+ defer pmmapitests.RemoveNodes(t, newNodeID)
+ defer pmmapitests.RemoveServices(t, serviceID)
+ defer removeServiceAgents(t, serviceID)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ Mysql: &services.GetServiceOKBodyMysql{
+ ServiceID: serviceID,
+ NodeID: newNodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 27017,
+ },
+ }, *serviceOK.Payload)
+
+ // Check that mysql exporter is added by default.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, agents.ListAgentsOKBody{
+ MysqldExporter: []*agents.MysqldExporterItems0{
+ {
+ AgentID: listAgents.Payload.MysqldExporter[0].AgentID,
+ ServiceID: serviceID,
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ TablestatsGroupTableLimit: 1000,
+ PushMetricsEnabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, *listAgents.Payload)
+ defer removeAllAgentsInList(t, listAgents)
+ })
+
+ t.Run("With Wrong Node Type", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "generic-node-for-wrong-node-type")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ remoteNodeOKBody := pmmapitests.AddRemoteNode(t, pmmapitests.TestString(t, "Remote Node for wrong type test"))
+ remoteNodeID := remoteNodeOKBody.Remote.NodeID
+ defer pmmapitests.RemoveNodes(t, remoteNodeID)
+
+ serviceName := pmmapitests.TestString(t, "service-name")
+ params := &mysql.AddMySQLParams{
+ Context: pmmapitests.Context,
+ Body: mysql.AddMySQLBody{
+ NodeID: remoteNodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addMySQLOK, err := client.Default.MySQL.AddMySQL(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "node_id or node_name can be used only for generic nodes or container nodes")
+ assert.Nil(t, addMySQLOK)
+ })
+
+ t.Run("Empty Service Name", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ params := &mysql.AddMySQLParams{
+ Context: pmmapitests.Context,
+ Body: mysql.AddMySQLBody{NodeID: nodeID},
+ }
+ addMySQLOK, err := client.Default.MySQL.AddMySQL(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field ServiceName: value '' must not be an empty string")
+ assert.Nil(t, addMySQLOK)
+ })
+
+ t.Run("Empty Address And Socket", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-name")
+ params := &mysql.AddMySQLParams{
+ Context: pmmapitests.Context,
+ Body: mysql.AddMySQLBody{
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ Password: "password",
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ },
+ }
+ addMySQLOK, err := client.Default.MySQL.AddMySQL(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Neither socket nor address passed.")
+ assert.Nil(t, addMySQLOK)
+ })
+
+ t.Run("Empty Port", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-name")
+ params := &mysql.AddMySQLParams{
+ Context: pmmapitests.Context,
+ Body: mysql.AddMySQLBody{
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ Password: "password",
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ },
+ }
+ addMySQLOK, err := client.Default.MySQL.AddMySQL(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Port are expected to be passed with address.")
+ assert.Nil(t, addMySQLOK)
+ })
+
+ t.Run("Address And Socket Conflict.", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-name")
+ params := &mysql.AddMySQLParams{
+ Context: pmmapitests.Context,
+ Body: mysql.AddMySQLBody{
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ Password: "password",
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ Socket: "/var/run/mysqld/mysqld.sock",
+ },
+ }
+ addMySQLOK, err := client.Default.MySQL.AddMySQL(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Socket and address cannot be specified together.")
+ assert.Nil(t, addMySQLOK)
+ })
+
+ t.Run("Empty Pmm Agent ID", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-name")
+ params := &mysql.AddMySQLParams{
+ Context: pmmapitests.Context,
+ Body: mysql.AddMySQLBody{
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ },
+ }
+ addMySQLOK, err := client.Default.MySQL.AddMySQL(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field PmmAgentId: value '' must not be an empty string")
+ assert.Nil(t, addMySQLOK)
+ })
+
+ t.Run("Empty username", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-name")
+ params := &mysql.AddMySQLParams{
+ Context: pmmapitests.Context,
+ Body: mysql.AddMySQLBody{
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ PMMAgentID: pmmAgentID,
+ },
+ }
+ addMySQLOK, err := client.Default.MySQL.AddMySQL(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field Username: value '' must not be an empty string")
+ assert.Nil(t, addMySQLOK)
+ })
+
+ t.Run("With MetricsModePush", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-for-basic-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-for-basic-name")
+
+ params := &mysql.AddMySQLParams{
+ Context: pmmapitests.Context,
+ Body: mysql.AddMySQLBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ Username: "username",
+
+ SkipConnectionCheck: true,
+ MetricsMode: pointer.ToString("PUSH"),
+ },
+ }
+ addMySQLOK, err := client.Default.MySQL.AddMySQL(params)
+ require.NoError(t, err)
+ require.NotNil(t, addMySQLOK)
+ require.NotNil(t, addMySQLOK.Payload.Service)
+ serviceID := addMySQLOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ Mysql: &services.GetServiceOKBodyMysql{
+ ServiceID: serviceID,
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ },
+ }, *serviceOK.Payload)
+
+ // Check that mysqld exporter is added by default.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, agents.ListAgentsOKBody{
+ MysqldExporter: []*agents.MysqldExporterItems0{
+ {
+ AgentID: listAgents.Payload.MysqldExporter[0].AgentID,
+ ServiceID: serviceID,
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ TablestatsGroupTableLimit: 1000,
+ PushMetricsEnabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, *listAgents.Payload)
+ defer removeAllAgentsInList(t, listAgents)
+ })
+
+ t.Run("With MetricsModePull", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-for-basic-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-for-basic-name")
+
+ params := &mysql.AddMySQLParams{
+ Context: pmmapitests.Context,
+ Body: mysql.AddMySQLBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ Username: "username",
+
+ SkipConnectionCheck: true,
+ MetricsMode: pointer.ToString("PULL"),
+ },
+ }
+ addMySQLOK, err := client.Default.MySQL.AddMySQL(params)
+ require.NoError(t, err)
+ require.NotNil(t, addMySQLOK)
+ require.NotNil(t, addMySQLOK.Payload.Service)
+ serviceID := addMySQLOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ Mysql: &services.GetServiceOKBodyMysql{
+ ServiceID: serviceID,
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ },
+ }, *serviceOK.Payload)
+
+ // Check that mysqld exporter is added by default.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, agents.ListAgentsOKBody{
+ MysqldExporter: []*agents.MysqldExporterItems0{
+ {
+ AgentID: listAgents.Payload.MysqldExporter[0].AgentID,
+ ServiceID: serviceID,
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ TablestatsGroupTableLimit: 1000,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, *listAgents.Payload)
+ defer removeAllAgentsInList(t, listAgents)
+ })
+
+ t.Run("With MetricsModeAuto", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-for-basic-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-for-basic-name")
+
+ params := &mysql.AddMySQLParams{
+ Context: pmmapitests.Context,
+ Body: mysql.AddMySQLBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ Username: "username",
+
+ SkipConnectionCheck: true,
+ MetricsMode: pointer.ToString("AUTO"),
+ },
+ }
+ addMySQLOK, err := client.Default.MySQL.AddMySQL(params)
+ require.NoError(t, err)
+ require.NotNil(t, addMySQLOK)
+ require.NotNil(t, addMySQLOK.Payload.Service)
+ serviceID := addMySQLOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ Mysql: &services.GetServiceOKBodyMysql{
+ ServiceID: serviceID,
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ },
+ }, *serviceOK.Payload)
+
+ // Check that mysqld exporter is added by default.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, agents.ListAgentsOKBody{
+ MysqldExporter: []*agents.MysqldExporterItems0{
+ {
+ AgentID: listAgents.Payload.MysqldExporter[0].AgentID,
+ ServiceID: serviceID,
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ TablestatsGroupTableLimit: 1000,
+ PushMetricsEnabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, *listAgents.Payload)
+ defer removeAllAgentsInList(t, listAgents)
+ })
+}
+
+func TestRemoveMySQL(t *testing.T) {
+ addMySQL := func(t *testing.T, serviceName, nodeName string, withAgents bool) (nodeID string, pmmAgentID string, serviceID string) {
+ t.Helper()
+ nodeID, pmmAgentID = RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+
+ params := &mysql.AddMySQLParams{
+ Context: pmmapitests.Context,
+ Body: mysql.AddMySQLBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ Username: "username",
+ Password: "password",
+ QANMysqlSlowlog: withAgents,
+ QANMysqlPerfschema: withAgents,
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addMySQLOK, err := client.Default.MySQL.AddMySQL(params)
+ require.NoError(t, err)
+ require.NotNil(t, addMySQLOK)
+ require.NotNil(t, addMySQLOK.Payload.Service)
+ serviceID = addMySQLOK.Payload.Service.ServiceID
+ return
+ }
+
+ t.Run("By name", func(t *testing.T) {
+ serviceName := pmmapitests.TestString(t, "service-remove-by-name")
+ nodeName := pmmapitests.TestString(t, "node-remove-by-name")
+ nodeID, pmmAgentID, serviceID := addMySQL(t, serviceName, nodeName, true)
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{
+ Body: service.RemoveServiceBody{
+ ServiceName: serviceName,
+ ServiceType: pointer.ToString(service.RemoveServiceBodyServiceTypeMYSQLSERVICE),
+ },
+ Context: pmmapitests.Context,
+ })
+ noError := assert.NoError(t, err)
+ notNil := assert.NotNil(t, removeServiceOK)
+ if !noError || !notNil {
+ defer pmmapitests.RemoveServices(t, serviceID)
+ }
+
+ // Check that the service removed with agents.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Service with ID %q not found.", serviceID)
+ assert.Nil(t, listAgents)
+ })
+
+ t.Run("By ID", func(t *testing.T) {
+ serviceName := pmmapitests.TestString(t, "service-remove-by-id")
+ nodeName := pmmapitests.TestString(t, "node-remove-by-id")
+ nodeID, pmmAgentID, serviceID := addMySQL(t, serviceName, nodeName, true)
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{
+ Body: service.RemoveServiceBody{
+ ServiceID: serviceID,
+ ServiceType: pointer.ToString(service.RemoveServiceBodyServiceTypeMYSQLSERVICE),
+ },
+ Context: pmmapitests.Context,
+ })
+ noError := assert.NoError(t, err)
+ notNil := assert.NotNil(t, removeServiceOK)
+ if !noError || !notNil {
+ defer pmmapitests.RemoveServices(t, serviceID)
+ }
+
+ // Check that the service removed with agents.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Service with ID %q not found.", serviceID)
+ assert.Nil(t, listAgents)
+ })
+
+ t.Run("Both params", func(t *testing.T) {
+ serviceName := pmmapitests.TestString(t, "service-remove-both-params")
+ nodeName := pmmapitests.TestString(t, "node-remove-both-params")
+ nodeID, pmmAgentID, serviceID := addMySQL(t, serviceName, nodeName, false)
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer pmmapitests.RemoveServices(t, serviceID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{
+ Body: service.RemoveServiceBody{
+ ServiceID: serviceID,
+ ServiceName: serviceName,
+ ServiceType: pointer.ToString(service.RemoveServiceBodyServiceTypeMYSQLSERVICE),
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.Nil(t, removeServiceOK)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "service_id or service_name expected; not both")
+ })
+
+ t.Run("Wrong type", func(t *testing.T) {
+ serviceName := pmmapitests.TestString(t, "service-remove-wrong-type")
+ nodeName := pmmapitests.TestString(t, "node-remove-wrong-type")
+ nodeID, pmmAgentID, serviceID := addMySQL(t, serviceName, nodeName, false)
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer pmmapitests.RemoveServices(t, serviceID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{
+ Body: service.RemoveServiceBody{
+ ServiceID: serviceID,
+ ServiceType: pointer.ToString(service.RemoveServiceBodyServiceTypePOSTGRESQLSERVICE),
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.Nil(t, removeServiceOK)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "wrong service type")
+ })
+
+ t.Run("No params", func(t *testing.T) {
+ removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{
+ Body: service.RemoveServiceBody{},
+ Context: pmmapitests.Context,
+ })
+ assert.Nil(t, removeServiceOK)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "service_id or service_name expected")
+ })
+}
diff --git a/api-tests/management/nodes_test.go b/api-tests/management/nodes_test.go
new file mode 100644
index 0000000000..a6e9190a60
--- /dev/null
+++ b/api-tests/management/nodes_test.go
@@ -0,0 +1,446 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package management
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/AlekSi/pointer"
+ inventoryClient "github.com/percona/pmm/api/inventorypb/json/client"
+ "github.com/percona/pmm/api/inventorypb/json/client/agents"
+ "github.com/percona/pmm/api/inventorypb/json/client/nodes"
+ "github.com/percona/pmm/api/managementpb/json/client"
+ "github.com/percona/pmm/api/managementpb/json/client/node"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestNodeRegister(t *testing.T) {
+ t.Run("Generic Node", func(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+ // Check Node is created
+ assertNodeCreated(t, nodeID, nodes.GetNodeOKBody{
+ Generic: &nodes.GetNodeOKBodyGeneric{
+ NodeID: nodeID,
+ NodeName: nodeName,
+ },
+ })
+
+ // Check PMM Agent is created
+ assertPMMAgentCreated(t, nodeID, pmmAgentID)
+
+ // Check Node Exporter is created
+ assertNodeExporterCreated(t, pmmAgentID)
+ })
+
+ t.Run("Reregister with same node name (no re-register - should fail)", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name-for-all-fields")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ Address: "node-address-1",
+ Region: "region-1",
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ body := node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ Address: "node-address-1",
+ Region: "region-1",
+ }
+ params := node.RegisterNodeParams{
+ Context: pmmapitests.Context,
+ Body: body,
+ }
+ _, err := client.Default.Node.RegisterNode(¶ms)
+ wantErr := fmt.Sprintf("Node with name %q already exists.", nodeName)
+ pmmapitests.AssertAPIErrorf(t, err, 409, codes.AlreadyExists, wantErr)
+ })
+
+ t.Run("Reregister with same node name (re-register)", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name-for-all-fields")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ Address: "node-address-2",
+ Region: "region-2",
+ })
+ assert.NotEmpty(t, nodeID)
+ assert.NotEmpty(t, pmmAgentID)
+
+ body := node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ Address: "node-address-2",
+ Region: "region-3",
+ Reregister: true,
+ }
+ params := node.RegisterNodeParams{
+ Context: pmmapitests.Context,
+ Body: body,
+ }
+ node, err := client.Default.Node.RegisterNode(¶ms)
+ assert.NoError(t, err)
+
+ defer pmmapitests.RemoveNodes(t, node.Payload.GenericNode.NodeID)
+ defer RemovePMMAgentWithSubAgents(t, node.Payload.PMMAgent.AgentID)
+ assertNodeExporterCreated(t, node.Payload.PMMAgent.AgentID)
+ })
+
+ t.Run("Reregister with different node name (no re-register - should fail)", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name-for-all-fields")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ Address: "node-address-3",
+ Region: "region-3",
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ body := node.RegisterNodeBody{
+ NodeName: nodeName + "_new",
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ Address: "node-address-3",
+ Region: "region-3",
+ }
+ params := node.RegisterNodeParams{
+ Context: pmmapitests.Context,
+ Body: body,
+ }
+ _, err := client.Default.Node.RegisterNode(¶ms)
+ wantErr := fmt.Sprintf("Node with instance %q and region %q already exists.", body.Address, body.Region)
+ pmmapitests.AssertAPIErrorf(t, err, 409, codes.AlreadyExists, wantErr)
+ })
+
+ t.Run("Reregister with different node name (re-register)", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name-for-all-fields")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ Address: "node-address-4",
+ Region: "region-4",
+ })
+
+ assert.NotEmpty(t, nodeID)
+ assert.NotEmpty(t, pmmAgentID)
+
+ body := node.RegisterNodeBody{
+ NodeName: nodeName + "_new",
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ Address: "node-address-4",
+ Region: "region-4",
+ Reregister: true,
+ }
+ params := node.RegisterNodeParams{
+ Context: pmmapitests.Context,
+ Body: body,
+ }
+ node, err := client.Default.Node.RegisterNode(¶ms)
+ assert.NoError(t, err)
+
+ defer pmmapitests.RemoveNodes(t, node.Payload.GenericNode.NodeID)
+ _, ok := assertNodeExporterCreated(t, node.Payload.PMMAgent.AgentID)
+ if ok {
+ defer RemovePMMAgentWithSubAgents(t, node.Payload.PMMAgent.AgentID)
+ }
+ })
+
+ t.Run("With all fields", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ machineID := pmmapitests.TestString(t, "machine-id")
+ nodeModel := pmmapitests.TestString(t, "node-model")
+ body := node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ MachineID: machineID,
+ NodeModel: nodeModel,
+ Az: "eu",
+ Region: "us-west",
+ Address: "10.10.10.10",
+ Distro: "Linux",
+ CustomLabels: map[string]string{"foo": "bar"},
+ DisableCollectors: []string{"diskstats", "filesystem", "standard.process"},
+ }
+ nodeID, pmmAgentID := RegisterGenericNode(t, body)
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ // Check Node is created
+ assertNodeCreated(t, nodeID, nodes.GetNodeOKBody{
+ Generic: &nodes.GetNodeOKBodyGeneric{
+ NodeID: nodeID,
+ NodeName: nodeName,
+ MachineID: machineID,
+ NodeModel: nodeModel,
+ Az: "eu",
+ Region: "us-west",
+ Address: "10.10.10.10",
+ Distro: "Linux",
+ CustomLabels: map[string]string{"foo": "bar"},
+ },
+ })
+
+ // Check PMM Agent is created
+ assertPMMAgentCreated(t, nodeID, pmmAgentID)
+
+ // Check Node Exporter is created
+ listAgentsOK, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Body: agents.ListAgentsBody{
+ PMMAgentID: pmmAgentID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.Len(t, listAgentsOK.Payload.NodeExporter, 1)
+ nodeExporterAgentID := listAgentsOK.Payload.NodeExporter[0].AgentID
+ ok := assert.Equal(t, agents.NodeExporterItems0{
+ PMMAgentID: pmmAgentID,
+ AgentID: nodeExporterAgentID,
+ DisabledCollectors: []string{"diskstats", "filesystem", "standard.process"},
+ PushMetricsEnabled: true,
+ Status: &AgentStatusUnknown,
+ }, *listAgentsOK.Payload.NodeExporter[0])
+
+ if ok {
+ defer pmmapitests.RemoveAgents(t, nodeExporterAgentID)
+ }
+ })
+
+ t.Run("Re-register", func(t *testing.T) {
+ t.Skip("Re-register logic is not defined yet. https://jira.percona.com/browse/PMM-3717")
+
+ nodeName := pmmapitests.TestString(t, "node-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ // Check Node is created
+ assertNodeCreated(t, nodeID, nodes.GetNodeOKBody{
+ Generic: &nodes.GetNodeOKBodyGeneric{
+ NodeID: nodeID,
+ NodeName: nodeName,
+ },
+ })
+
+ // Re-register node
+ machineID := pmmapitests.TestString(t, "machine-id")
+ nodeModel := pmmapitests.TestString(t, "node-model")
+ newNodeID, newPMMAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ MachineID: machineID,
+ NodeModel: nodeModel,
+ Az: "eu",
+ Region: "us-west",
+ Address: "10.10.10.10",
+ Distro: "Linux",
+ CustomLabels: map[string]string{"foo": "bar"},
+ })
+ if !assert.Equal(t, nodeID, newNodeID) {
+ defer pmmapitests.RemoveNodes(t, newNodeID)
+ }
+ if !assert.Equal(t, pmmAgentID, newPMMAgentID) {
+ defer pmmapitests.RemoveAgents(t, newPMMAgentID)
+ }
+
+ // Check Node fields is updated
+ assertNodeCreated(t, nodeID, nodes.GetNodeOKBody{
+ Generic: &nodes.GetNodeOKBodyGeneric{
+ NodeID: nodeID,
+ NodeName: nodeName,
+ MachineID: machineID,
+ NodeModel: nodeModel,
+ Az: "eu",
+ Region: "us-west",
+ Address: "10.10.10.10",
+ Distro: "Linux",
+ CustomLabels: map[string]string{"foo": "bar"},
+ },
+ })
+ })
+ })
+
+ t.Run("Container Node", func(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ nodeID, pmmAgentID := registerContainerNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeCONTAINERNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ // Check Node is created
+ assertNodeCreated(t, nodeID, nodes.GetNodeOKBody{
+ Container: &nodes.GetNodeOKBodyContainer{
+ NodeID: nodeID,
+ NodeName: nodeName,
+ },
+ })
+
+ // Check PMM Agent is created
+ assertPMMAgentCreated(t, nodeID, pmmAgentID)
+
+ // Check Node Exporter is created
+ nodeExporterAgentID, ok := assertNodeExporterCreated(t, pmmAgentID)
+ if ok {
+ defer pmmapitests.RemoveAgents(t, nodeExporterAgentID)
+ }
+ })
+
+ t.Run("With all fields", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ nodeModel := pmmapitests.TestString(t, "node-model")
+ containerID := pmmapitests.TestString(t, "container-id")
+ containerName := pmmapitests.TestString(t, "container-name")
+ body := node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeCONTAINERNODE),
+ NodeModel: nodeModel,
+ ContainerID: containerID,
+ ContainerName: containerName,
+ Az: "eu",
+ Region: "us-west",
+ Address: "10.10.10.10",
+ CustomLabels: map[string]string{"foo": "bar"},
+ }
+ nodeID, pmmAgentID := registerContainerNode(t, body)
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ // Check Node is created
+ assertNodeCreated(t, nodeID, nodes.GetNodeOKBody{
+ Container: &nodes.GetNodeOKBodyContainer{
+ NodeID: nodeID,
+ NodeName: nodeName,
+ NodeModel: nodeModel,
+ ContainerID: containerID,
+ ContainerName: containerName,
+ Az: "eu",
+ Region: "us-west",
+ Address: "10.10.10.10",
+ CustomLabels: map[string]string{"foo": "bar"},
+ },
+ })
+
+ // Check PMM Agent is created
+ assertPMMAgentCreated(t, nodeID, pmmAgentID)
+
+ // Check Node Exporter is created
+ nodeExporterAgentID, ok := assertNodeExporterCreated(t, pmmAgentID)
+ if ok {
+ defer pmmapitests.RemoveAgents(t, nodeExporterAgentID)
+ }
+ })
+
+ t.Run("Re-register", func(t *testing.T) {
+ t.Skip("Re-register logic is not defined yet. https://jira.percona.com/browse/PMM-3717")
+
+ nodeName := pmmapitests.TestString(t, "node-name")
+ nodeID, pmmAgentID := registerContainerNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeCONTAINERNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ // Check Node is created
+ assertNodeCreated(t, nodeID, nodes.GetNodeOKBody{
+ Generic: &nodes.GetNodeOKBodyGeneric{
+ NodeID: nodeID,
+ NodeName: nodeName,
+ },
+ })
+
+ // Re-register node
+ nodeModel := pmmapitests.TestString(t, "node-model")
+ containerID := pmmapitests.TestString(t, "container-id")
+ containerName := pmmapitests.TestString(t, "container-name")
+ newNodeID, newPMMAgentID := registerContainerNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeCONTAINERNODE),
+ ContainerID: containerID,
+ ContainerName: containerName,
+ NodeModel: nodeModel,
+ Az: "eu",
+ Region: "us-west",
+ Address: "10.10.10.10",
+ CustomLabels: map[string]string{"foo": "bar"},
+ })
+ if !assert.Equal(t, nodeID, newNodeID) {
+ defer pmmapitests.RemoveNodes(t, newNodeID)
+ }
+ if !assert.Equal(t, pmmAgentID, newPMMAgentID) {
+ defer pmmapitests.RemoveAgents(t, newPMMAgentID)
+ }
+
+ // Check Node fields is updated
+ assertNodeCreated(t, nodeID, nodes.GetNodeOKBody{
+ Container: &nodes.GetNodeOKBodyContainer{
+ NodeID: nodeID,
+ NodeName: nodeName,
+ ContainerID: containerID,
+ ContainerName: containerName,
+ NodeModel: nodeModel,
+ Az: "eu",
+ Region: "us-west",
+ Address: "10.10.10.10",
+ CustomLabels: map[string]string{"foo": "bar"},
+ },
+ })
+ })
+ })
+
+ t.Run("Empty node name", func(t *testing.T) {
+ params := node.RegisterNodeParams{
+ Context: pmmapitests.Context,
+ Body: node.RegisterNodeBody{},
+ }
+ registerOK, err := client.Default.Node.RegisterNode(¶ms)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field NodeName: value '' must not be an empty string")
+ require.Nil(t, registerOK)
+ })
+
+ t.Run("Unsupported node type", func(t *testing.T) {
+ params := node.RegisterNodeParams{
+ Context: pmmapitests.Context,
+ Body: node.RegisterNodeBody{
+ NodeName: pmmapitests.TestString(t, "node-name"),
+ },
+ }
+ registerOK, err := client.Default.Node.RegisterNode(¶ms)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, `Unsupported Node type "NODE_TYPE_INVALID".`)
+ require.Nil(t, registerOK)
+ })
+}
diff --git a/api-tests/management/postgresql_test.go b/api-tests/management/postgresql_test.go
new file mode 100644
index 0000000000..017ae2c28d
--- /dev/null
+++ b/api-tests/management/postgresql_test.go
@@ -0,0 +1,941 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package management
+
+import (
+ "testing"
+
+ "github.com/AlekSi/pointer"
+ inventoryClient "github.com/percona/pmm/api/inventorypb/json/client"
+ "github.com/percona/pmm/api/inventorypb/json/client/agents"
+ "github.com/percona/pmm/api/inventorypb/json/client/services"
+ "github.com/percona/pmm/api/managementpb/json/client"
+ "github.com/percona/pmm/api/managementpb/json/client/node"
+ postgresql "github.com/percona/pmm/api/managementpb/json/client/postgre_sql"
+ "github.com/percona/pmm/api/managementpb/json/client/service"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestAddPostgreSQL(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-for-basic-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-for-basic-name")
+
+ params := &postgresql.AddPostgreSQLParams{
+ Context: pmmapitests.Context,
+ Body: postgresql.AddPostgreSQLBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 5432,
+ Username: "username",
+
+ SkipConnectionCheck: true,
+ DisableCollectors: []string{"custom_query.ml", "custom_query.mr.directory"},
+ },
+ }
+ addPostgreSQLOK, err := client.Default.PostgreSQL.AddPostgreSQL(params)
+ require.NoError(t, err)
+ require.NotNil(t, addPostgreSQLOK)
+ require.NotNil(t, addPostgreSQLOK.Payload.Service)
+ serviceID := addPostgreSQLOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ Postgresql: &services.GetServiceOKBodyPostgresql{
+ ServiceID: serviceID,
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 5432,
+ },
+ }, *serviceOK.Payload)
+
+ // Check that no one exporter is added.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, agents.ListAgentsOKBody{
+ PostgresExporter: []*agents.PostgresExporterItems0{
+ {
+ AgentID: listAgents.Payload.PostgresExporter[0].AgentID,
+ ServiceID: serviceID,
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ DisabledCollectors: []string{"custom_query.ml", "custom_query.mr.directory"},
+ PushMetricsEnabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, *listAgents.Payload)
+ defer removeAllAgentsInList(t, listAgents)
+ })
+
+ t.Run("With agents", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-for-all-fields-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-for-all-fields-name")
+
+ params := &postgresql.AddPostgreSQLParams{
+ Context: pmmapitests.Context,
+ Body: postgresql.AddPostgreSQLBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 5432,
+ Username: "username",
+ Password: "password",
+ QANPostgresqlPgstatementsAgent: true,
+ QANPostgresqlPgstatmonitorAgent: true,
+ DisableQueryExamples: true,
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addPostgreSQLOK, err := client.Default.PostgreSQL.AddPostgreSQL(params)
+ require.NoError(t, err)
+ require.NotNil(t, addPostgreSQLOK)
+ require.NotNil(t, addPostgreSQLOK.Payload.Service)
+ serviceID := addPostgreSQLOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ Postgresql: &services.GetServiceOKBodyPostgresql{
+ ServiceID: serviceID,
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 5432,
+ },
+ }, *serviceOK.Payload)
+
+ // Check that exporters are added.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, listAgents)
+ defer removeAllAgentsInList(t, listAgents)
+ require.Len(t, listAgents.Payload.PostgresExporter, 1)
+ require.Len(t, listAgents.Payload.QANPostgresqlPgstatementsAgent, 1)
+ require.Len(t, listAgents.Payload.QANPostgresqlPgstatmonitorAgent, 1)
+ assert.Equal(t, agents.ListAgentsOKBody{
+ PostgresExporter: []*agents.PostgresExporterItems0{
+ {
+ AgentID: listAgents.Payload.PostgresExporter[0].AgentID,
+ ServiceID: serviceID,
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ PushMetricsEnabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ QANPostgresqlPgstatementsAgent: []*agents.QANPostgresqlPgstatementsAgentItems0{
+ {
+ AgentID: listAgents.Payload.QANPostgresqlPgstatementsAgent[0].AgentID,
+ ServiceID: serviceID,
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ Status: &AgentStatusUnknown,
+ },
+ },
+ QANPostgresqlPgstatmonitorAgent: []*agents.QANPostgresqlPgstatmonitorAgentItems0{
+ {
+ AgentID: listAgents.Payload.QANPostgresqlPgstatmonitorAgent[0].AgentID,
+ ServiceID: serviceID,
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ QueryExamplesDisabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, *listAgents.Payload)
+ })
+
+ t.Run("With labels", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-for-all-fields-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-for-all-fields-name")
+
+ params := &postgresql.AddPostgreSQLParams{
+ Context: pmmapitests.Context,
+ Body: postgresql.AddPostgreSQLBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 5432,
+ Username: "username",
+ Environment: "some-environment",
+ CustomLabels: map[string]string{"bar": "foo"},
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addPostgreSQLOK, err := client.Default.PostgreSQL.AddPostgreSQL(params)
+ require.NoError(t, err)
+ require.NotNil(t, addPostgreSQLOK)
+ require.NotNil(t, addPostgreSQLOK.Payload.Service)
+ serviceID := addPostgreSQLOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+ defer removeServiceAgents(t, serviceID)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ Postgresql: &services.GetServiceOKBodyPostgresql{
+ ServiceID: serviceID,
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 5432,
+ Environment: "some-environment",
+ CustomLabels: map[string]string{"bar": "foo"},
+ },
+ }, *serviceOK.Payload)
+ })
+
+ t.Run("With the same name", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-for-the-same-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-for-the-same-name")
+
+ params := &postgresql.AddPostgreSQLParams{
+ Context: pmmapitests.Context,
+ Body: postgresql.AddPostgreSQLBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Username: "username",
+ Address: "10.10.10.10",
+ Port: 5432,
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addPostgreSQLOK, err := client.Default.PostgreSQL.AddPostgreSQL(params)
+ require.NoError(t, err)
+ require.NotNil(t, addPostgreSQLOK)
+ require.NotNil(t, addPostgreSQLOK.Payload.Service)
+ serviceID := addPostgreSQLOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+ defer removeServiceAgents(t, serviceID)
+
+ params = &postgresql.AddPostgreSQLParams{
+ Context: pmmapitests.Context,
+ Body: postgresql.AddPostgreSQLBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Username: "username",
+ Address: "11.11.11.11",
+ Port: 5433,
+ },
+ }
+ addPostgreSQLOK, err = client.Default.PostgreSQL.AddPostgreSQL(params)
+ require.Nil(t, addPostgreSQLOK)
+ pmmapitests.AssertAPIErrorf(t, err, 409, codes.AlreadyExists, `Service with name %q already exists.`, serviceName)
+ })
+
+ t.Run("With add_node block", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-for-basic-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ nodeNameAddNode := pmmapitests.TestString(t, "node-for-add-node-name")
+ serviceName := pmmapitests.TestString(t, "service-name-for-basic-name")
+
+ params := &postgresql.AddPostgreSQLParams{
+ Context: pmmapitests.Context,
+ Body: postgresql.AddPostgreSQLBody{
+ AddNode: &postgresql.AddPostgreSQLParamsBodyAddNode{
+ NodeType: pointer.ToString(postgresql.AddPostgreSQLParamsBodyAddNodeNodeTypeGENERICNODE),
+ NodeName: nodeNameAddNode,
+ },
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 27017,
+ Username: "username",
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addPostgreSQLOK, err := client.Default.PostgreSQL.AddPostgreSQL(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "add_node structure can be used only for remote nodes")
+
+ params = &postgresql.AddPostgreSQLParams{
+ Context: pmmapitests.Context,
+ Body: postgresql.AddPostgreSQLBody{
+ AddNode: &postgresql.AddPostgreSQLParamsBodyAddNode{
+ NodeType: pointer.ToString(postgresql.AddPostgreSQLParamsBodyAddNodeNodeTypeREMOTERDSNODE),
+ NodeName: nodeNameAddNode,
+ },
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 27017,
+ Username: "username",
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addPostgreSQLOK, err = client.Default.PostgreSQL.AddPostgreSQL(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "add_node structure can be used only for remote nodes")
+
+ params = &postgresql.AddPostgreSQLParams{
+ Context: pmmapitests.Context,
+ Body: postgresql.AddPostgreSQLBody{
+ AddNode: &postgresql.AddPostgreSQLParamsBodyAddNode{
+ NodeType: pointer.ToString(postgresql.AddPostgreSQLParamsBodyAddNodeNodeTypeREMOTENODE),
+ NodeName: nodeNameAddNode,
+ },
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 27017,
+ Username: "username",
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addPostgreSQLOK, err = client.Default.PostgreSQL.AddPostgreSQL(params)
+ require.NoError(t, err)
+ require.NotNil(t, addPostgreSQLOK)
+ require.NotNil(t, addPostgreSQLOK.Payload.Service)
+ serviceID := addPostgreSQLOK.Payload.Service.ServiceID
+
+ newNodeID := addPostgreSQLOK.Payload.Service.NodeID
+ require.NotEqual(t, nodeID, newNodeID)
+ defer pmmapitests.RemoveNodes(t, newNodeID)
+ defer pmmapitests.RemoveServices(t, serviceID)
+ defer removeServiceAgents(t, serviceID)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ Postgresql: &services.GetServiceOKBodyPostgresql{
+ ServiceID: serviceID,
+ NodeID: newNodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 27017,
+ },
+ }, *serviceOK.Payload)
+
+ // Check that postgresql exporter is added by default.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, agents.ListAgentsOKBody{
+ PostgresExporter: []*agents.PostgresExporterItems0{
+ {
+ AgentID: listAgents.Payload.PostgresExporter[0].AgentID,
+ ServiceID: serviceID,
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ PushMetricsEnabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, *listAgents.Payload)
+ defer removeAllAgentsInList(t, listAgents)
+ })
+
+ t.Run("With Wrong Node Type", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "generic-node-for-wrong-node-type")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ remoteNodeOKBody := pmmapitests.AddRemoteNode(t, pmmapitests.TestString(t, "Remote Node for wrong type test"))
+ remoteNodeID := remoteNodeOKBody.Remote.NodeID
+ defer pmmapitests.RemoveNodes(t, remoteNodeID)
+
+ serviceName := pmmapitests.TestString(t, "service-name")
+ params := &postgresql.AddPostgreSQLParams{
+ Context: pmmapitests.Context,
+ Body: postgresql.AddPostgreSQLBody{
+ NodeID: remoteNodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addPostgreSQLOK, err := client.Default.PostgreSQL.AddPostgreSQL(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "node_id or node_name can be used only for generic nodes or container nodes")
+ assert.Nil(t, addPostgreSQLOK)
+ })
+
+ t.Run("Empty Service Name", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ params := &postgresql.AddPostgreSQLParams{
+ Context: pmmapitests.Context,
+ Body: postgresql.AddPostgreSQLBody{NodeID: nodeID},
+ }
+ addPostgreSQLOK, err := client.Default.PostgreSQL.AddPostgreSQL(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field ServiceName: value '' must not be an empty string")
+ assert.Nil(t, addPostgreSQLOK)
+ })
+
+ t.Run("Empty Address", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-name")
+ params := &postgresql.AddPostgreSQLParams{
+ Context: pmmapitests.Context,
+ Body: postgresql.AddPostgreSQLBody{
+ PMMAgentID: pmmAgentID,
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Username: "username",
+ },
+ }
+ addPostgreSQLOK, err := client.Default.PostgreSQL.AddPostgreSQL(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Neither socket nor address passed.")
+ assert.Nil(t, addPostgreSQLOK)
+ })
+
+ t.Run("Empty Port", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-name")
+ params := &postgresql.AddPostgreSQLParams{
+ Context: pmmapitests.Context,
+ Body: postgresql.AddPostgreSQLBody{
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ Address: "10.10.10.10",
+ },
+ }
+ addPostgreSQLOK, err := client.Default.PostgreSQL.AddPostgreSQL(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Port are expected to be passed with address.")
+ assert.Nil(t, addPostgreSQLOK)
+ })
+
+ t.Run("Empty Pmm Agent ID", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-name")
+ params := &postgresql.AddPostgreSQLParams{
+ Context: pmmapitests.Context,
+ Body: postgresql.AddPostgreSQLBody{
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 5432,
+ },
+ }
+ addPostgreSQLOK, err := client.Default.PostgreSQL.AddPostgreSQL(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field PmmAgentId: value '' must not be an empty string")
+ assert.Nil(t, addPostgreSQLOK)
+ })
+
+ t.Run("Address And Socket Conflict.", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-name")
+ params := &postgresql.AddPostgreSQLParams{
+ Context: pmmapitests.Context,
+ Body: postgresql.AddPostgreSQLBody{
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ Password: "password",
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 5432,
+ Socket: "/var/run/postgresql",
+ },
+ }
+ addProxySQLOK, err := client.Default.PostgreSQL.AddPostgreSQL(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Socket and address cannot be specified together.")
+ assert.Nil(t, addProxySQLOK)
+ })
+
+ t.Run("With MetricsModePush", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-for-basic-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-for-basic-name")
+
+ params := &postgresql.AddPostgreSQLParams{
+ Context: pmmapitests.Context,
+ Body: postgresql.AddPostgreSQLBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 5432,
+ Username: "username",
+
+ SkipConnectionCheck: true,
+ MetricsMode: pointer.ToString("PUSH"),
+ },
+ }
+ addPostgreSQLOK, err := client.Default.PostgreSQL.AddPostgreSQL(params)
+ require.NoError(t, err)
+ require.NotNil(t, addPostgreSQLOK)
+ require.NotNil(t, addPostgreSQLOK.Payload.Service)
+ serviceID := addPostgreSQLOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ Postgresql: &services.GetServiceOKBodyPostgresql{
+ ServiceID: serviceID,
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 5432,
+ },
+ }, *serviceOK.Payload)
+
+ // Check that no one exporter is added.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, agents.ListAgentsOKBody{
+ PostgresExporter: []*agents.PostgresExporterItems0{
+ {
+ AgentID: listAgents.Payload.PostgresExporter[0].AgentID,
+ ServiceID: serviceID,
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ PushMetricsEnabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, *listAgents.Payload)
+ defer removeAllAgentsInList(t, listAgents)
+ })
+
+ t.Run("With MetricsModePull", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-for-basic-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-for-basic-name")
+
+ params := &postgresql.AddPostgreSQLParams{
+ Context: pmmapitests.Context,
+ Body: postgresql.AddPostgreSQLBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 5432,
+ Username: "username",
+
+ SkipConnectionCheck: true,
+ MetricsMode: pointer.ToString("PULL"),
+ },
+ }
+ addPostgreSQLOK, err := client.Default.PostgreSQL.AddPostgreSQL(params)
+ require.NoError(t, err)
+ require.NotNil(t, addPostgreSQLOK)
+ require.NotNil(t, addPostgreSQLOK.Payload.Service)
+ serviceID := addPostgreSQLOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ Postgresql: &services.GetServiceOKBodyPostgresql{
+ ServiceID: serviceID,
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 5432,
+ },
+ }, *serviceOK.Payload)
+
+ // Check that no one exporter is added.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, agents.ListAgentsOKBody{
+ PostgresExporter: []*agents.PostgresExporterItems0{
+ {
+ AgentID: listAgents.Payload.PostgresExporter[0].AgentID,
+ ServiceID: serviceID,
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, *listAgents.Payload)
+ defer removeAllAgentsInList(t, listAgents)
+ })
+
+ t.Run("With MetricsModeAuto", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-for-basic-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-for-basic-name")
+
+ params := &postgresql.AddPostgreSQLParams{
+ Context: pmmapitests.Context,
+ Body: postgresql.AddPostgreSQLBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 5432,
+ Username: "username",
+
+ SkipConnectionCheck: true,
+ MetricsMode: pointer.ToString("AUTO"),
+ },
+ }
+ addPostgreSQLOK, err := client.Default.PostgreSQL.AddPostgreSQL(params)
+ require.NoError(t, err)
+ require.NotNil(t, addPostgreSQLOK)
+ require.NotNil(t, addPostgreSQLOK.Payload.Service)
+ serviceID := addPostgreSQLOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ Postgresql: &services.GetServiceOKBodyPostgresql{
+ ServiceID: serviceID,
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 5432,
+ },
+ }, *serviceOK.Payload)
+
+ // Check that no one exporter is added.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, agents.ListAgentsOKBody{
+ PostgresExporter: []*agents.PostgresExporterItems0{
+ {
+ AgentID: listAgents.Payload.PostgresExporter[0].AgentID,
+ ServiceID: serviceID,
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ PushMetricsEnabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, *listAgents.Payload)
+ defer removeAllAgentsInList(t, listAgents)
+ })
+}
+
+func TestRemovePostgreSQL(t *testing.T) {
+ addPostgreSQL := func(t *testing.T, serviceName, nodeName string, withAgents bool) (nodeID string, pmmAgentID string, serviceID string) {
+ nodeID, pmmAgentID = RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+
+ params := &postgresql.AddPostgreSQLParams{
+ Context: pmmapitests.Context,
+ Body: postgresql.AddPostgreSQLBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 5432,
+ Username: "username",
+ Password: "password",
+ QANPostgresqlPgstatementsAgent: withAgents,
+ QANPostgresqlPgstatmonitorAgent: withAgents,
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addPostgreSQLOK, err := client.Default.PostgreSQL.AddPostgreSQL(params)
+ require.NoError(t, err)
+ require.NotNil(t, addPostgreSQLOK)
+ require.NotNil(t, addPostgreSQLOK.Payload.Service)
+ serviceID = addPostgreSQLOK.Payload.Service.ServiceID
+ return
+ }
+
+ t.Run("By name", func(t *testing.T) {
+ serviceName := pmmapitests.TestString(t, "service-remove-by-name")
+ nodeName := pmmapitests.TestString(t, "node-remove-by-name")
+ nodeID, pmmAgentID, serviceID := addPostgreSQL(t, serviceName, nodeName, true)
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{
+ Body: service.RemoveServiceBody{
+ ServiceName: serviceName,
+ ServiceType: pointer.ToString(service.RemoveServiceBodyServiceTypePOSTGRESQLSERVICE),
+ },
+ Context: pmmapitests.Context,
+ })
+ noError := assert.NoError(t, err)
+ notNil := assert.NotNil(t, removeServiceOK)
+ if !noError || !notNil {
+ defer pmmapitests.RemoveServices(t, serviceID)
+ }
+
+ // Check that the service removed with agents.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Service with ID %q not found.", serviceID)
+ assert.Nil(t, listAgents)
+ })
+
+ t.Run("By ID", func(t *testing.T) {
+ serviceName := pmmapitests.TestString(t, "service-remove-by-id")
+ nodeName := pmmapitests.TestString(t, "node-remove-by-id")
+ nodeID, pmmAgentID, serviceID := addPostgreSQL(t, serviceName, nodeName, true)
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{
+ Body: service.RemoveServiceBody{
+ ServiceID: serviceID,
+ ServiceType: pointer.ToString(service.RemoveServiceBodyServiceTypePOSTGRESQLSERVICE),
+ },
+ Context: pmmapitests.Context,
+ })
+ noError := assert.NoError(t, err)
+ notNil := assert.NotNil(t, removeServiceOK)
+ if !noError || !notNil {
+ defer pmmapitests.RemoveServices(t, serviceID)
+ }
+
+ // Check that the service removed with agents.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Service with ID %q not found.", serviceID)
+ assert.Nil(t, listAgents)
+ })
+
+ t.Run("Both params", func(t *testing.T) {
+ serviceName := pmmapitests.TestString(t, "service-remove-both-params")
+ nodeName := pmmapitests.TestString(t, "node-remove-both-params")
+ nodeID, pmmAgentID, serviceID := addPostgreSQL(t, serviceName, nodeName, false)
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer pmmapitests.RemoveServices(t, serviceID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{
+ Body: service.RemoveServiceBody{
+ ServiceID: serviceID,
+ ServiceName: serviceName,
+ ServiceType: pointer.ToString(service.RemoveServiceBodyServiceTypePOSTGRESQLSERVICE),
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.Nil(t, removeServiceOK)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "service_id or service_name expected; not both")
+ })
+
+ t.Run("Wrong type", func(t *testing.T) {
+ serviceName := pmmapitests.TestString(t, "service-remove-wrong-type")
+ nodeName := pmmapitests.TestString(t, "node-remove-wrong-type")
+ nodeID, pmmAgentID, serviceID := addPostgreSQL(t, serviceName, nodeName, false)
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer pmmapitests.RemoveServices(t, serviceID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{
+ Body: service.RemoveServiceBody{
+ ServiceID: serviceID,
+ ServiceType: pointer.ToString(service.RemoveServiceBodyServiceTypeMYSQLSERVICE),
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.Nil(t, removeServiceOK)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "wrong service type")
+ })
+}
diff --git a/api-tests/management/proxysql_test.go b/api-tests/management/proxysql_test.go
new file mode 100644
index 0000000000..6cef230583
--- /dev/null
+++ b/api-tests/management/proxysql_test.go
@@ -0,0 +1,734 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package management
+
+import (
+ "testing"
+
+ "github.com/AlekSi/pointer"
+ inventoryClient "github.com/percona/pmm/api/inventorypb/json/client"
+ "github.com/percona/pmm/api/inventorypb/json/client/agents"
+ "github.com/percona/pmm/api/inventorypb/json/client/services"
+ "github.com/percona/pmm/api/managementpb/json/client"
+ "github.com/percona/pmm/api/managementpb/json/client/node"
+ proxysql "github.com/percona/pmm/api/managementpb/json/client/proxy_sql"
+ "github.com/percona/pmm/api/managementpb/json/client/service"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestAddProxySQL(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-for-basic-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-for-basic-name")
+
+ params := &proxysql.AddProxySQLParams{
+ Context: pmmapitests.Context,
+ Body: proxysql.AddProxySQLBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ Username: "username",
+
+ SkipConnectionCheck: true,
+ DisableCollectors: []string{"mysql_status", "mysql_connection_pool"},
+ },
+ }
+ addProxySQLOK, err := client.Default.ProxySQL.AddProxySQL(params)
+ require.NoError(t, err)
+ require.NotNil(t, addProxySQLOK)
+ require.NotNil(t, addProxySQLOK.Payload.Service)
+ serviceID := addProxySQLOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ Proxysql: &services.GetServiceOKBodyProxysql{
+ ServiceID: serviceID,
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ },
+ }, *serviceOK.Payload)
+
+ // Check that proxysql exporter is added by default.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, agents.ListAgentsOKBody{
+ ProxysqlExporter: []*agents.ProxysqlExporterItems0{
+ {
+ AgentID: listAgents.Payload.ProxysqlExporter[0].AgentID,
+ ServiceID: serviceID,
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ DisabledCollectors: []string{"mysql_status", "mysql_connection_pool"},
+ PushMetricsEnabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, *listAgents.Payload)
+ defer removeAllAgentsInList(t, listAgents)
+ })
+
+ t.Run("With agents", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-for-all-fields-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-for-all-fields-name")
+
+ params := &proxysql.AddProxySQLParams{
+ Context: pmmapitests.Context,
+ Body: proxysql.AddProxySQLBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ Username: "username",
+ Password: "password",
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addProxySQLOK, err := client.Default.ProxySQL.AddProxySQL(params)
+ require.NoError(t, err)
+ require.NotNil(t, addProxySQLOK)
+ require.NotNil(t, addProxySQLOK.Payload.Service)
+ serviceID := addProxySQLOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ Proxysql: &services.GetServiceOKBodyProxysql{
+ ServiceID: serviceID,
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ },
+ }, *serviceOK.Payload)
+
+ // Check that exporters are added.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, listAgents)
+ defer removeAllAgentsInList(t, listAgents)
+ require.Len(t, listAgents.Payload.ProxysqlExporter, 1)
+ assert.Equal(t, agents.ListAgentsOKBody{
+ ProxysqlExporter: []*agents.ProxysqlExporterItems0{
+ {
+ AgentID: listAgents.Payload.ProxysqlExporter[0].AgentID,
+ ServiceID: serviceID,
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ PushMetricsEnabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, *listAgents.Payload)
+ })
+
+ t.Run("With labels", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-for-all-fields-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-for-all-fields-name")
+
+ params := &proxysql.AddProxySQLParams{
+ Context: pmmapitests.Context,
+ Body: proxysql.AddProxySQLBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ Username: "username",
+ Password: "password",
+ Environment: "some-environment",
+ Cluster: "cluster-name",
+ ReplicationSet: "replication-set",
+ CustomLabels: map[string]string{"bar": "foo"},
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addProxySQLOK, err := client.Default.ProxySQL.AddProxySQL(params)
+ require.NoError(t, err)
+ require.NotNil(t, addProxySQLOK)
+ require.NotNil(t, addProxySQLOK.Payload.Service)
+ serviceID := addProxySQLOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+ defer removeServiceAgents(t, serviceID)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ assert.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ Proxysql: &services.GetServiceOKBodyProxysql{
+ ServiceID: serviceID,
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ Environment: "some-environment",
+ Cluster: "cluster-name",
+ ReplicationSet: "replication-set",
+ CustomLabels: map[string]string{"bar": "foo"},
+ },
+ }, *serviceOK.Payload)
+ })
+
+ t.Run("With the same name", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-for-the-same-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-for-the-same-name")
+
+ params := &proxysql.AddProxySQLParams{
+ Context: pmmapitests.Context,
+ Body: proxysql.AddProxySQLBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ Username: "username",
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addProxySQLOK, err := client.Default.ProxySQL.AddProxySQL(params)
+ require.NoError(t, err)
+ require.NotNil(t, addProxySQLOK)
+ require.NotNil(t, addProxySQLOK.Payload.Service)
+ serviceID := addProxySQLOK.Payload.Service.ServiceID
+ defer pmmapitests.RemoveServices(t, serviceID)
+ defer removeServiceAgents(t, serviceID)
+
+ params = &proxysql.AddProxySQLParams{
+ Context: pmmapitests.Context,
+ Body: proxysql.AddProxySQLBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "11.11.11.11",
+ Port: 3307,
+ Username: "username",
+ },
+ }
+ addProxySQLOK, err = client.Default.ProxySQL.AddProxySQL(params)
+ require.Nil(t, addProxySQLOK)
+ pmmapitests.AssertAPIErrorf(t, err, 409, codes.AlreadyExists, `Service with name %q already exists.`, serviceName)
+ })
+
+ t.Run("With add_node block", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-for-basic-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ nodeNameAddNode := pmmapitests.TestString(t, "node-for-add-node-name")
+ serviceName := pmmapitests.TestString(t, "service-name-for-basic-name")
+
+ params := &proxysql.AddProxySQLParams{
+ Context: pmmapitests.Context,
+ Body: proxysql.AddProxySQLBody{
+ AddNode: &proxysql.AddProxySQLParamsBodyAddNode{
+ NodeType: pointer.ToString(proxysql.AddProxySQLParamsBodyAddNodeNodeTypeGENERICNODE),
+ NodeName: nodeNameAddNode,
+ },
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 27017,
+ Username: "username",
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addProxySQLOK, err := client.Default.ProxySQL.AddProxySQL(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "add_node structure can be used only for remote nodes")
+
+ params = &proxysql.AddProxySQLParams{
+ Context: pmmapitests.Context,
+ Body: proxysql.AddProxySQLBody{
+ AddNode: &proxysql.AddProxySQLParamsBodyAddNode{
+ NodeType: pointer.ToString(proxysql.AddProxySQLParamsBodyAddNodeNodeTypeREMOTERDSNODE),
+ NodeName: nodeNameAddNode,
+ },
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 27017,
+ Username: "username",
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addProxySQLOK, err = client.Default.ProxySQL.AddProxySQL(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "add_node structure can be used only for remote nodes")
+
+ params = &proxysql.AddProxySQLParams{
+ Context: pmmapitests.Context,
+ Body: proxysql.AddProxySQLBody{
+ AddNode: &proxysql.AddProxySQLParamsBodyAddNode{
+ NodeType: pointer.ToString(proxysql.AddProxySQLParamsBodyAddNodeNodeTypeREMOTENODE),
+ NodeName: nodeNameAddNode,
+ },
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 27017,
+ Username: "username",
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addProxySQLOK, err = client.Default.ProxySQL.AddProxySQL(params)
+ require.NoError(t, err)
+ require.NotNil(t, addProxySQLOK)
+ require.NotNil(t, addProxySQLOK.Payload.Service)
+ serviceID := addProxySQLOK.Payload.Service.ServiceID
+
+ newNodeID := addProxySQLOK.Payload.Service.NodeID
+ require.NotEqual(t, nodeID, newNodeID)
+ defer pmmapitests.RemoveNodes(t, newNodeID)
+ defer pmmapitests.RemoveServices(t, serviceID)
+ defer removeServiceAgents(t, serviceID)
+
+ // Check that service is created and its fields.
+ serviceOK, err := inventoryClient.Default.Services.GetService(&services.GetServiceParams{
+ Body: services.GetServiceBody{
+ ServiceID: serviceID,
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.NoError(t, err)
+ require.NotNil(t, serviceOK)
+ assert.Equal(t, services.GetServiceOKBody{
+ Proxysql: &services.GetServiceOKBodyProxysql{
+ ServiceID: serviceID,
+ NodeID: newNodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 27017,
+ },
+ }, *serviceOK.Payload)
+
+ // Check that proxysql exporter is added by default.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, agents.ListAgentsOKBody{
+ ProxysqlExporter: []*agents.ProxysqlExporterItems0{
+ {
+ AgentID: listAgents.Payload.ProxysqlExporter[0].AgentID,
+ ServiceID: serviceID,
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ PushMetricsEnabled: true,
+ Status: &AgentStatusUnknown,
+ },
+ },
+ }, *listAgents.Payload)
+ defer removeAllAgentsInList(t, listAgents)
+ })
+
+ t.Run("With Wrong Node Type", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "generic-node-for-wrong-node-type")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ remoteNodeOKBody := pmmapitests.AddRemoteNode(t, pmmapitests.TestString(t, "Remote Node for wrong type test"))
+ remoteNodeID := remoteNodeOKBody.Remote.NodeID
+ defer pmmapitests.RemoveNodes(t, remoteNodeID)
+
+ serviceName := pmmapitests.TestString(t, "service-name")
+ params := &proxysql.AddProxySQLParams{
+ Context: pmmapitests.Context,
+ Body: proxysql.AddProxySQLBody{
+ NodeID: remoteNodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addProxySQLOK, err := client.Default.ProxySQL.AddProxySQL(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "node_id or node_name can be used only for generic nodes or container nodes")
+ assert.Nil(t, addProxySQLOK)
+ })
+
+ t.Run("Empty Service Name", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ params := &proxysql.AddProxySQLParams{
+ Context: pmmapitests.Context,
+ Body: proxysql.AddProxySQLBody{NodeID: nodeID},
+ }
+ addProxySQLOK, err := client.Default.ProxySQL.AddProxySQL(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field ServiceName: value '' must not be an empty string")
+ assert.Nil(t, addProxySQLOK)
+ })
+
+ t.Run("Empty Address And Socket", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-name")
+ params := &proxysql.AddProxySQLParams{
+ Context: pmmapitests.Context,
+ Body: proxysql.AddProxySQLBody{
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ },
+ }
+ addProxySQLOK, err := client.Default.ProxySQL.AddProxySQL(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Neither socket nor address passed.")
+ assert.Nil(t, addProxySQLOK)
+ })
+
+ t.Run("Empty Port", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-name")
+ params := &proxysql.AddProxySQLParams{
+ Context: pmmapitests.Context,
+ Body: proxysql.AddProxySQLBody{
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ Address: "10.10.10.10",
+ },
+ }
+ addProxySQLOK, err := client.Default.ProxySQL.AddProxySQL(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Port are expected to be passed with address.")
+ assert.Nil(t, addProxySQLOK)
+ })
+
+ t.Run("Address And Socket Conflict.", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-name")
+ params := &proxysql.AddProxySQLParams{
+ Context: pmmapitests.Context,
+ Body: proxysql.AddProxySQLBody{
+ PMMAgentID: pmmAgentID,
+ Username: "username",
+ Password: "password",
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 6032,
+ Socket: "/tmp/proxysql_admin.sock",
+ },
+ }
+ addProxySQLOK, err := client.Default.ProxySQL.AddProxySQL(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Socket and address cannot be specified together.")
+ assert.Nil(t, addProxySQLOK)
+ })
+
+ t.Run("Empty Pmm Agent ID", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-name")
+ params := &proxysql.AddProxySQLParams{
+ Context: pmmapitests.Context,
+ Body: proxysql.AddProxySQLBody{
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ },
+ }
+ addProxySQLOK, err := client.Default.ProxySQL.AddProxySQL(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field PmmAgentId: value '' must not be an empty string")
+ assert.Nil(t, addProxySQLOK)
+ })
+
+ t.Run("Empty username", func(t *testing.T) {
+ nodeName := pmmapitests.TestString(t, "node-name")
+ nodeID, pmmAgentID := RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ serviceName := pmmapitests.TestString(t, "service-name")
+ params := &proxysql.AddProxySQLParams{
+ Context: pmmapitests.Context,
+ Body: proxysql.AddProxySQLBody{
+ NodeID: nodeID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ PMMAgentID: pmmAgentID,
+ },
+ }
+ addProxySQLOK, err := client.Default.ProxySQL.AddProxySQL(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field Username: value '' must not be an empty string")
+ assert.Nil(t, addProxySQLOK)
+ })
+}
+
+func TestRemoveProxySQL(t *testing.T) {
+ addProxySQL := func(t *testing.T, serviceName, nodeName string) (nodeID string, pmmAgentID string, serviceID string) {
+ t.Helper()
+ nodeID, pmmAgentID = RegisterGenericNode(t, node.RegisterNodeBody{
+ NodeName: nodeName,
+ NodeType: pointer.ToString(node.RegisterNodeBodyNodeTypeGENERICNODE),
+ })
+
+ params := &proxysql.AddProxySQLParams{
+ Context: pmmapitests.Context,
+ Body: proxysql.AddProxySQLBody{
+ NodeID: nodeID,
+ PMMAgentID: pmmAgentID,
+ ServiceName: serviceName,
+ Address: "10.10.10.10",
+ Port: 3306,
+ Username: "username",
+ Password: "password",
+
+ SkipConnectionCheck: true,
+ },
+ }
+ addProxySQLOK, err := client.Default.ProxySQL.AddProxySQL(params)
+ require.NoError(t, err)
+ require.NotNil(t, addProxySQLOK)
+ require.NotNil(t, addProxySQLOK.Payload.Service)
+ serviceID = addProxySQLOK.Payload.Service.ServiceID
+ return
+ }
+
+ t.Run("By name", func(t *testing.T) {
+ serviceName := pmmapitests.TestString(t, "service-remove-by-name")
+ nodeName := pmmapitests.TestString(t, "node-remove-by-name")
+ nodeID, pmmAgentID, serviceID := addProxySQL(t, serviceName, nodeName)
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{
+ Body: service.RemoveServiceBody{
+ ServiceName: serviceName,
+ ServiceType: pointer.ToString(service.RemoveServiceBodyServiceTypePROXYSQLSERVICE),
+ },
+ Context: pmmapitests.Context,
+ })
+ noError := assert.NoError(t, err)
+ notNil := assert.NotNil(t, removeServiceOK)
+ if !noError || !notNil {
+ defer pmmapitests.RemoveServices(t, serviceID)
+ }
+
+ // Check that the service removed with agents.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Service with ID %q not found.", serviceID)
+ assert.Nil(t, listAgents)
+ })
+
+ t.Run("By ID", func(t *testing.T) {
+ serviceName := pmmapitests.TestString(t, "service-remove-by-id")
+ nodeName := pmmapitests.TestString(t, "node-remove-by-id")
+ nodeID, pmmAgentID, serviceID := addProxySQL(t, serviceName, nodeName)
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{
+ Body: service.RemoveServiceBody{
+ ServiceID: serviceID,
+ ServiceType: pointer.ToString(service.RemoveServiceBodyServiceTypePROXYSQLSERVICE),
+ },
+ Context: pmmapitests.Context,
+ })
+ noError := assert.NoError(t, err)
+ notNil := assert.NotNil(t, removeServiceOK)
+ if !noError || !notNil {
+ defer pmmapitests.RemoveServices(t, serviceID)
+ }
+
+ // Check that the service removed with agents.
+ listAgents, err := inventoryClient.Default.Agents.ListAgents(&agents.ListAgentsParams{
+ Context: pmmapitests.Context,
+ Body: agents.ListAgentsBody{
+ ServiceID: serviceID,
+ },
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, "Service with ID %q not found.", serviceID)
+ assert.Nil(t, listAgents)
+ })
+
+ t.Run("Both params", func(t *testing.T) {
+ serviceName := pmmapitests.TestString(t, "service-remove-both-params")
+ nodeName := pmmapitests.TestString(t, "node-remove-both-params")
+ nodeID, pmmAgentID, serviceID := addProxySQL(t, serviceName, nodeName)
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer pmmapitests.RemoveServices(t, serviceID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{
+ Body: service.RemoveServiceBody{
+ ServiceID: serviceID,
+ ServiceName: serviceName,
+ ServiceType: pointer.ToString(service.RemoveServiceBodyServiceTypePROXYSQLSERVICE),
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.Nil(t, removeServiceOK)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "service_id or service_name expected; not both")
+ })
+
+ t.Run("Wrong type", func(t *testing.T) {
+ serviceName := pmmapitests.TestString(t, "service-remove-wrong-type")
+ nodeName := pmmapitests.TestString(t, "node-remove-wrong-type")
+ nodeID, pmmAgentID, serviceID := addProxySQL(t, serviceName, nodeName)
+ defer pmmapitests.RemoveNodes(t, nodeID)
+ defer pmmapitests.RemoveServices(t, serviceID)
+ defer RemovePMMAgentWithSubAgents(t, pmmAgentID)
+
+ removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{
+ Body: service.RemoveServiceBody{
+ ServiceID: serviceID,
+ ServiceType: pointer.ToString(service.RemoveServiceBodyServiceTypePOSTGRESQLSERVICE),
+ },
+ Context: pmmapitests.Context,
+ })
+ assert.Nil(t, removeServiceOK)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "wrong service type")
+ })
+
+ t.Run("No params", func(t *testing.T) {
+ removeServiceOK, err := client.Default.Service.RemoveService(&service.RemoveServiceParams{
+ Body: service.RemoveServiceBody{},
+ Context: pmmapitests.Context,
+ })
+ assert.Nil(t, removeServiceOK)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "service_id or service_name expected")
+ })
+}
diff --git a/api-tests/management/rds_test.go b/api-tests/management/rds_test.go
new file mode 100644
index 0000000000..ff990c3f05
--- /dev/null
+++ b/api-tests/management/rds_test.go
@@ -0,0 +1,182 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package management
+
+import (
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/AlekSi/pointer"
+ inventoryClient "github.com/percona/pmm/api/inventorypb/json/client"
+ "github.com/percona/pmm/api/inventorypb/json/client/agents"
+ "github.com/percona/pmm/api/inventorypb/json/client/nodes"
+ "github.com/percona/pmm/api/managementpb/json/client"
+ "github.com/percona/pmm/api/managementpb/json/client/rds"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestRDSDiscovery(t *testing.T) {
+ t.Run("Basic", func(t *testing.T) {
+ accessKey, secretKey := os.Getenv("AWS_ACCESS_KEY"), os.Getenv("AWS_SECRET_KEY")
+ if accessKey == "" || secretKey == "" {
+ // TODO remove skip once secrets are added
+ t.Skip("Environment variables AWS_ACCESS_KEY / AWS_SECRET_KEY are not defined, skipping test")
+ }
+
+ params := &rds.DiscoverRDSParams{
+ Body: rds.DiscoverRDSBody{
+ AWSAccessKey: accessKey,
+ AWSSecretKey: secretKey,
+ },
+ Context: pmmapitests.Context,
+ }
+ discoverOK, err := client.Default.RDS.DiscoverRDS(params)
+ require.NoError(t, err)
+ require.NotNil(t, discoverOK.Payload)
+ assert.NotEmpty(t, discoverOK.Payload.RDSInstances)
+
+ // TODO Better tests: https://jira.percona.com/browse/PMM-4896
+ })
+}
+
+func TestAddRds(t *testing.T) {
+ t.Run("BasicAddRDS", func(t *testing.T) {
+ params := &rds.AddRDSParams{
+ Body: rds.AddRDSBody{
+ Region: "region",
+ Az: "az",
+ InstanceID: "d752f1a9-31c9-4b8c-bb2d-d26bc000001",
+ NodeModel: "some-model",
+ Address: "some.example.rds",
+ Port: 3306,
+ Engine: pointer.ToString("DISCOVER_RDS_MYSQL"),
+ NodeName: "some-node-name-000001",
+ ServiceName: "test-add-rds-service000001",
+ Environment: "some-env",
+ Cluster: "cluster-01",
+ ReplicationSet: "rs-01",
+ Username: "some-username",
+ Password: "some-password",
+ AWSAccessKey: "my-aws-access-key",
+ AWSSecretKey: "my-aws-secret-key",
+ RDSExporter: true,
+ QANMysqlPerfschema: true,
+ CustomLabels: map[string]string{},
+ SkipConnectionCheck: true,
+ TLS: false,
+ TLSSkipVerify: false,
+ DisableQueryExamples: false,
+ TablestatsGroupTableLimit: 2000,
+ DisableBasicMetrics: true,
+ DisableEnhancedMetrics: true,
+ },
+ Context: pmmapitests.Context,
+ }
+ addRDSOK, err := client.Default.RDS.AddRDS(params)
+ require.NoError(t, err)
+ require.NotNil(t, addRDSOK.Payload)
+
+ body := addRDSOK.Payload
+ assert.True(t, body.RDSExporter.BasicMetricsDisabled)
+ assert.True(t, body.RDSExporter.EnhancedMetricsDisabled)
+
+ pmmapitests.RemoveAgents(t, body.MysqldExporter.AgentID)
+ pmmapitests.RemoveAgents(t, body.QANMysqlPerfschema.AgentID)
+ pmmapitests.RemoveServices(t, body.Mysql.ServiceID)
+
+ _, err = inventoryClient.Default.Agents.GetAgent(&agents.GetAgentParams{
+ Body: agents.GetAgentBody{
+ AgentID: body.RDSExporter.AgentID,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, fmt.Sprintf(`Agent with ID "%s" not found.`, body.RDSExporter.AgentID))
+
+ _, err = inventoryClient.Default.Nodes.GetNode(&nodes.GetNodeParams{
+ Body: nodes.GetNodeBody{
+ NodeID: body.Mysql.NodeID,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, fmt.Sprintf(`Node with ID "%s" not found.`, body.Mysql.NodeID))
+ })
+
+ t.Run("AddRDSPostgres", func(t *testing.T) {
+ params := &rds.AddRDSParams{
+ Body: rds.AddRDSBody{
+ Region: "region",
+ Az: "az",
+ InstanceID: "d752f1a9-31c9-4b8c-bb2d-d26bc000009",
+ NodeModel: "some-model",
+ Address: "some.example.rds",
+ Port: 5432,
+ Engine: pointer.ToString("DISCOVER_RDS_POSTGRESQL"),
+ NodeName: "some-node-name-000009",
+ ServiceName: "test-add-rds-service000009",
+ Environment: "some-env",
+ Cluster: "cluster-01",
+ ReplicationSet: "rs-01",
+ Username: "some-username",
+ Password: "some-password",
+ AWSAccessKey: "my-aws-access-key",
+ AWSSecretKey: "my-aws-secret-key",
+ RDSExporter: true,
+ CustomLabels: map[string]string{},
+ SkipConnectionCheck: true,
+ TLS: false,
+ TLSSkipVerify: false,
+ TablestatsGroupTableLimit: 2000,
+ DisableBasicMetrics: true,
+ DisableEnhancedMetrics: true,
+ QANPostgresqlPgstatements: true,
+ },
+ Context: pmmapitests.Context,
+ }
+ addRDSOK, err := client.Default.RDS.AddRDS(params)
+ require.NoError(t, err)
+ require.NotNil(t, addRDSOK.Payload)
+
+ body := addRDSOK.Payload
+ assert.True(t, body.RDSExporter.BasicMetricsDisabled)
+ assert.True(t, body.RDSExporter.EnhancedMetricsDisabled)
+
+ pmmapitests.RemoveAgents(t, body.PostgresqlExporter.AgentID)
+ pmmapitests.RemoveAgents(t, body.QANPostgresqlPgstatements.AgentID)
+ pmmapitests.RemoveServices(t, body.Postgresql.ServiceID)
+
+ _, err = inventoryClient.Default.Agents.GetAgent(&agents.GetAgentParams{
+ Body: agents.GetAgentBody{
+ AgentID: body.RDSExporter.AgentID,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, fmt.Sprintf(`Agent with ID "%s" not found.`, body.RDSExporter.AgentID))
+
+ _, err = inventoryClient.Default.Nodes.GetNode(&nodes.GetNodeParams{
+ Body: nodes.GetNodeBody{
+ NodeID: body.Postgresql.NodeID,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 404, codes.NotFound, fmt.Sprintf(`Node with ID "%s" not found.`, body.Postgresql.NodeID))
+ })
+}
diff --git a/api-tests/rand.go b/api-tests/rand.go
new file mode 100644
index 0000000000..60c715d0c7
--- /dev/null
+++ b/api-tests/rand.go
@@ -0,0 +1,58 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+// This file contains implementation for concurrent safe RNG.
+package pmmapitests
+
+import (
+ "math/rand"
+ "sync"
+)
+
+// ConcurrentRand wraps rand.Rand with mutex.
+type ConcurrentRand struct {
+ m sync.Mutex
+ rand *rand.Rand
+}
+
+// NewConcurrentRand constructs new ConcurrentRand with provided seed.
+func NewConcurrentRand(seed int64) *ConcurrentRand {
+ r := &ConcurrentRand{
+ rand: rand.New(rand.NewSource(seed)),
+ }
+ return r
+}
+
+// Seed uses the provided seed value to initialize the generator to a deterministic state.
+func (r *ConcurrentRand) Seed(seed int64) {
+ r.m.Lock()
+ defer r.m.Unlock()
+ r.rand.Seed(seed)
+}
+
+// Int63 returns a non-negative pseudo-random 63-bit integer as an int64.
+func (r *ConcurrentRand) Int63() int64 {
+ r.m.Lock()
+ defer r.m.Unlock()
+ return r.rand.Int63()
+}
+
+// Uint64 returns a pseudo-random 64-bit value as a uint64.
+func (r *ConcurrentRand) Uint64() uint64 {
+ r.m.Lock()
+ defer r.m.Unlock()
+ return r.rand.Uint64()
+}
diff --git a/api-tests/server/alertmanager_test.go b/api-tests/server/alertmanager_test.go
new file mode 100644
index 0000000000..5ab4d1fa6b
--- /dev/null
+++ b/api-tests/server/alertmanager_test.go
@@ -0,0 +1,83 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package server
+
+import (
+ "testing"
+ "time"
+
+ "github.com/percona/pmm/api/alertmanager/amclient"
+ "github.com/percona/pmm/api/alertmanager/amclient/alert"
+ serverClient "github.com/percona/pmm/api/serverpb/json/client"
+ "github.com/percona/pmm/api/serverpb/json/client/server"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestAlertManager(t *testing.T) {
+ t.Run("TestEndsAtForFailedChecksAlerts", func(t *testing.T) {
+ if !pmmapitests.RunSTTTests {
+ t.Skip("Skipping STT tests until we have environment: https://jira.percona.com/browse/PMM-5106")
+ }
+
+ defer restoreSettingsDefaults(t)
+
+ // Enabling STT
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ EnableStt: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.True(t, res.Payload.Settings.SttEnabled)
+
+ // sync with pmm-managed
+ const (
+ resolveTimeoutFactor = 3
+ defaultResendInterval = 2 * time.Second
+ )
+
+ // 120 sec ping for failed checks alerts to appear in alertmanager
+ for i := 0; i < 120; i++ {
+ res, err := amclient.Default.Alert.GetAlerts(&alert.GetAlertsParams{
+ Filter: []string{"stt_check=1"},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ if len(res.Payload) == 0 {
+ time.Sleep(1 * time.Second)
+ continue
+ }
+
+ require.NotEmpty(t, res.Payload, "No alerts met")
+
+ // TODO: Expand this test once we are silencing/removing alerts.
+ alertTTL := resolveTimeoutFactor * defaultResendInterval
+ for _, v := range res.Payload {
+ // Since the `EndsAt` timestamp is always resolveTimeoutFactor times the
+ // `resendInterval` in the future from `UpdatedAt`
+ // we check whether they lie in that time alertTTL.
+ assert.WithinDuration(t, time.Time(*v.EndsAt), time.Time(*v.UpdatedAt), alertTTL)
+ assert.Greater(t, v.EndsAt, v.UpdatedAt)
+ }
+ break
+ }
+ })
+}
diff --git a/api-tests/server/auth_test.go b/api-tests/server/auth_test.go
new file mode 100644
index 0000000000..fcd8468e85
--- /dev/null
+++ b/api-tests/server/auth_test.go
@@ -0,0 +1,503 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package server
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/http/httputil"
+ "net/url"
+ "strconv"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/AlekSi/pointer"
+ serverClient "github.com/percona/pmm/api/serverpb/json/client"
+ "github.com/percona/pmm/api/serverpb/json/client/server"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestAuth(t *testing.T) {
+ t.Run("AuthErrors", func(t *testing.T) {
+ for user, code := range map[*url.Userinfo]int{
+ nil: 401,
+ url.UserPassword("bad", "wrong"): 401,
+ } {
+ user := user
+ code := code
+ t.Run(fmt.Sprintf("%s/%d", user, code), func(t *testing.T) {
+ t.Parallel()
+
+ // copy BaseURL and replace auth
+ baseURL, err := url.Parse(pmmapitests.BaseURL.String())
+ require.NoError(t, err)
+ baseURL.User = user
+
+ uri := baseURL.ResolveReference(&url.URL{
+ Path: "v1/version",
+ })
+ t.Logf("URI: %s", uri)
+ resp, err := http.Get(uri.String())
+ require.NoError(t, err)
+ defer resp.Body.Close() //nolint:errcheck
+
+ b, err := httputil.DumpResponse(resp, true)
+ require.NoError(t, err)
+ assert.Equal(t, code, resp.StatusCode, "response:\n%s", b)
+ require.False(t, bytes.Contains(b, []byte(``)), "response:\n%s", b)
+ })
+ }
+ })
+
+ t.Run("NormalErrors", func(t *testing.T) {
+ for grpcCode, httpCode := range map[codes.Code]int{
+ codes.Unauthenticated: 401,
+ codes.PermissionDenied: 403,
+ } {
+ grpcCode := grpcCode
+ httpCode := httpCode
+ t.Run(fmt.Sprintf("%s/%d", grpcCode, httpCode), func(t *testing.T) {
+ t.Parallel()
+
+ res, err := serverClient.Default.Server.Version(&server.VersionParams{
+ Dummy: pointer.ToString(fmt.Sprintf("grpccode-%d", grpcCode)),
+ Context: pmmapitests.Context,
+ })
+ assert.Empty(t, res)
+ pmmapitests.AssertAPIErrorf(t, err, httpCode, grpcCode, "gRPC code %d (%s)", grpcCode, grpcCode)
+ })
+ }
+ })
+}
+
+func TestSetup(t *testing.T) {
+ // make a BaseURL without authentication
+ baseURL, err := url.Parse(pmmapitests.BaseURL.String())
+ require.NoError(t, err)
+ baseURL.User = nil
+
+ // make client that does not follow redirects
+ client := &http.Client{
+ CheckRedirect: func(req *http.Request, via []*http.Request) error {
+ return http.ErrUseLastResponse
+ },
+ }
+
+ t.Run("WebPage", func(t *testing.T) {
+ t.Parallel()
+
+ uri := baseURL.ResolveReference(&url.URL{
+ Path: "/setup",
+ })
+ t.Logf("URI: %s", uri)
+ req, err := http.NewRequestWithContext(pmmapitests.Context, "GET", uri.String(), nil)
+ require.NoError(t, err)
+ req.Header.Set("X-Test-Must-Setup", "1")
+
+ resp, b := doRequest(t, client, req)
+ assert.Equal(t, 200, resp.StatusCode, "response:\n%s", b)
+ assert.True(t, strings.HasPrefix(string(b), ``), string(b))
+ })
+
+ t.Run("Redirect", func(t *testing.T) {
+ paths := map[string]int{
+ "graph": 303,
+ "graph/": 303,
+ "prometheus": 303,
+ "prometheus/": 303,
+ "swagger": 200,
+ "swagger/": 301,
+
+ "v1/readyz": 200,
+ "v1/AWSInstanceCheck": 405, // only POST is expected
+ "v1/version": 401, // Grafana authentication required
+ }
+ for path, code := range paths {
+ path, code := path, code
+ t.Run(fmt.Sprintf("%s=%d", path, code), func(t *testing.T) {
+ t.Parallel()
+
+ uri := baseURL.ResolveReference(&url.URL{
+ Path: path,
+ })
+ t.Logf("URI: %s", uri)
+ req, err := http.NewRequestWithContext(pmmapitests.Context, "GET", uri.String(), nil)
+ require.NoError(t, err)
+ req.Header.Set("X-Test-Must-Setup", "1")
+
+ resp, b := doRequest(t, client, req)
+ assert.Equal(t, code, resp.StatusCode, "response:\n%s", b)
+ if code == 303 {
+ assert.Equal(t, "/setup", resp.Header.Get("Location"))
+ }
+ })
+ }
+ })
+
+ t.Run("API", func(t *testing.T) {
+ t.Parallel()
+
+ uri := baseURL.ResolveReference(&url.URL{
+ Path: "v1/AWSInstanceCheck",
+ })
+ t.Logf("URI: %s", uri)
+ b, err := json.Marshal(server.AWSInstanceCheckBody{
+ InstanceID: "123",
+ })
+ require.NoError(t, err)
+ req, err := http.NewRequestWithContext(pmmapitests.Context, "POST", uri.String(), bytes.NewReader(b))
+ require.NoError(t, err)
+ req.Header.Set("X-Test-Must-Setup", "1")
+
+ resp, b := doRequest(t, client, req)
+ assert.Equal(t, 200, resp.StatusCode, "response:\n%s", b)
+ assert.Equal(t, "{\n\n}", string(b), "response:\n%s", b)
+ })
+}
+
+func TestSwagger(t *testing.T) {
+ for _, path := range []string{
+ "swagger",
+ "swagger/",
+ "swagger.json",
+ "swagger/swagger.json",
+ } {
+ path := path
+
+ t.Run(path, func(t *testing.T) {
+ t.Run("NoAuth", func(t *testing.T) {
+ t.Parallel()
+
+ // make a BaseURL without authentication
+ baseURL, err := url.Parse(pmmapitests.BaseURL.String())
+ require.NoError(t, err)
+ baseURL.User = nil
+
+ uri := baseURL.ResolveReference(&url.URL{
+ Path: path,
+ })
+ t.Logf("URI: %s", uri)
+ req, err := http.NewRequestWithContext(pmmapitests.Context, "GET", uri.String(), nil)
+ require.NoError(t, err)
+
+ resp, _ := doRequest(t, http.DefaultClient, req)
+ require.NoError(t, err)
+ assert.Equal(t, 200, resp.StatusCode)
+ })
+
+ t.Run("Auth", func(t *testing.T) {
+ t.Parallel()
+
+ uri := pmmapitests.BaseURL.ResolveReference(&url.URL{
+ Path: path,
+ })
+ t.Logf("URI: %s", uri)
+ req, err := http.NewRequestWithContext(pmmapitests.Context, "GET", uri.String(), nil)
+ require.NoError(t, err)
+
+ resp, _ := doRequest(t, http.DefaultClient, req)
+ require.NoError(t, err)
+ assert.Equal(t, 200, resp.StatusCode)
+ })
+ })
+ }
+}
+
+func TestPermissions(t *testing.T) {
+ ts := strconv.FormatInt(time.Now().Unix(), 10)
+ none := "none-" + ts
+ viewer := "viewer-" + ts
+ editor := "editor-" + ts
+ admin := "admin-" + ts
+
+ noneID := createUser(t, none)
+ defer deleteUser(t, noneID)
+
+ viewerID := createUserWithRole(t, viewer, "Viewer")
+ defer deleteUser(t, viewerID)
+
+ editorID := createUserWithRole(t, editor, "Editor")
+ defer deleteUser(t, editorID)
+
+ adminID := createUserWithRole(t, admin, "Admin")
+ defer deleteUser(t, adminID)
+
+ viewerAPIKeyID, viewerAPIKey := createAPIKeyWithRole(t, "api-"+viewer, "Viewer")
+ defer deleteAPIKey(t, viewerAPIKeyID)
+
+ editorAPIKeyID, editorAPIKey := createAPIKeyWithRole(t, "api-"+editor, "Editor")
+ defer deleteAPIKey(t, editorAPIKeyID)
+
+ adminAPIKeyID, adminAPIKey := createAPIKeyWithRole(t, "api-"+admin, "Admin")
+ defer deleteAPIKey(t, adminAPIKeyID)
+
+ type userCase struct {
+ userType string
+ login string
+ apiKey string
+ statusCode int
+ }
+
+ tests := []struct {
+ name string
+ url string
+ method string
+ userCase []userCase
+ }{
+ {name: "settings", url: "/v1/Settings/Get", method: "POST", userCase: []userCase{
+ {userType: "default", login: none, statusCode: 401},
+ {userType: "viewer", login: viewer, apiKey: viewerAPIKey, statusCode: 401},
+ {userType: "editor", login: editor, apiKey: editorAPIKey, statusCode: 401},
+ {userType: "admin", login: admin, apiKey: adminAPIKey, statusCode: 200},
+ }},
+ {name: "alerts-default", url: "/alertmanager/api/v2/alerts", method: "GET", userCase: []userCase{
+ {userType: "default", login: none, statusCode: 401},
+ {userType: "viewer", login: viewer, apiKey: viewerAPIKey, statusCode: 401},
+ {userType: "editor", login: editor, apiKey: editorAPIKey, statusCode: 401},
+ {userType: "admin", login: admin, apiKey: adminAPIKey, statusCode: 200},
+ }},
+ {name: "platform-sign-up", url: "/v1/Platform/SignUp", method: "POST", userCase: []userCase{
+ {userType: "default", login: none, statusCode: 401},
+ {userType: "viewer", login: viewer, apiKey: viewerAPIKey, statusCode: 401},
+ {userType: "editor", login: editor, apiKey: editorAPIKey, statusCode: 401},
+ {userType: "admin", login: admin, apiKey: adminAPIKey, statusCode: 400}, // We send bad request, but have access to endpoint
+ }},
+ {name: "platform-sign-in", url: "/v1/Platform/SignIn", method: "POST", userCase: []userCase{
+ {userType: "default", login: none, statusCode: 401},
+ {userType: "viewer", login: viewer, apiKey: viewerAPIKey, statusCode: 401},
+ {userType: "editor", login: editor, apiKey: editorAPIKey, statusCode: 401},
+ {userType: "admin", login: admin, apiKey: adminAPIKey, statusCode: 400}, // We send bad request, but have access to endpoint
+ }},
+ {name: "platform-sign-out", url: "/v1/Platform/SignOut", method: "POST", userCase: []userCase{
+ {userType: "default", login: none, statusCode: 401},
+ {userType: "viewer", login: viewer, apiKey: viewerAPIKey, statusCode: 401},
+ {userType: "editor", login: editor, apiKey: editorAPIKey, statusCode: 401},
+ {userType: "admin", login: admin, apiKey: adminAPIKey, statusCode: 400}, // We send bad request, but have access to endpoint
+ }},
+ }
+
+ for _, test := range tests {
+ test := test
+ t.Run(test.name, func(t *testing.T) {
+ for _, user := range test.userCase {
+ user := user
+ t.Run(fmt.Sprintf("Basic auth %s", user.userType), func(t *testing.T) {
+ // make a BaseURL without authentication
+ u, err := url.Parse(pmmapitests.BaseURL.String())
+ require.NoError(t, err)
+ u.User = url.UserPassword(user.login, user.login)
+ u.Path = test.url
+
+ req, err := http.NewRequestWithContext(pmmapitests.Context, test.method, u.String(), nil)
+ require.NoError(t, err)
+
+ resp, err := http.DefaultClient.Do(req)
+ require.NoError(t, err)
+ defer resp.Body.Close() //nolint:errcheck
+
+ assert.Equal(t, user.statusCode, resp.StatusCode)
+ })
+
+ t.Run(fmt.Sprintf("API Key auth %s", user.userType), func(t *testing.T) {
+ if user.apiKey == "" {
+ t.Skip("API Key is not exist")
+ }
+ // make a BaseURL without authentication
+ u, err := url.Parse(pmmapitests.BaseURL.String())
+ require.NoError(t, err)
+ u.User = nil
+ u.Path = test.url
+
+ req, err := http.NewRequestWithContext(pmmapitests.Context, test.method, u.String(), nil)
+ require.NoError(t, err)
+
+ req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", user.apiKey))
+
+ resp, err := http.DefaultClient.Do(req)
+ require.NoError(t, err)
+ defer resp.Body.Close() //nolint:errcheck
+
+ assert.Equal(t, user.statusCode, resp.StatusCode)
+ })
+
+ t.Run(fmt.Sprintf("API Key Basic auth %s", user.userType), func(t *testing.T) {
+ if user.apiKey == "" {
+ t.Skip("API Key is not exist")
+ }
+ // make a BaseURL without authentication
+ u, err := url.Parse(pmmapitests.BaseURL.String())
+ require.NoError(t, err)
+ u.User = url.UserPassword("api_key", user.apiKey)
+ u.Path = test.url
+
+ req, err := http.NewRequestWithContext(pmmapitests.Context, test.method, u.String(), nil)
+ require.NoError(t, err)
+
+ resp, err := http.DefaultClient.Do(req)
+ require.NoError(t, err)
+ defer resp.Body.Close() //nolint:errcheck
+
+ assert.Equal(t, user.statusCode, resp.StatusCode)
+ })
+ }
+ })
+ }
+}
+
+func doRequest(t testing.TB, client *http.Client, req *http.Request) (*http.Response, []byte) {
+ resp, err := client.Do(req)
+ require.NoError(t, err)
+
+ defer resp.Body.Close() //nolint:errcheck
+
+ b, err := ioutil.ReadAll(resp.Body)
+ require.NoError(t, err)
+
+ return resp, b
+}
+
+func createUserWithRole(t *testing.T, login, role string) int {
+ userID := createUser(t, login)
+ setRole(t, userID, role)
+
+ return userID
+}
+
+func deleteUser(t *testing.T, userID int) {
+ u, err := url.Parse(pmmapitests.BaseURL.String())
+ require.NoError(t, err)
+ u.Path = "/graph/api/admin/users/" + strconv.Itoa(userID)
+
+ req, err := http.NewRequestWithContext(pmmapitests.Context, http.MethodDelete, u.String(), nil)
+ require.NoError(t, err)
+
+ resp, b := doRequest(t, http.DefaultClient, req)
+ require.Equalf(t, http.StatusOK, resp.StatusCode, "failed to delete user, status code: %d, response: %s", resp.StatusCode, b)
+}
+
+func createUser(t *testing.T, login string) int {
+ u, err := url.Parse(pmmapitests.BaseURL.String())
+ require.NoError(t, err)
+ u.Path = "/graph/api/admin/users"
+
+ // https://grafana.com/docs/http_api/admin/#global-users
+ data, err := json.Marshal(map[string]string{
+ "name": login,
+ "email": login + "@percona.invalid",
+ "login": login,
+ "password": login,
+ })
+ require.NoError(t, err)
+
+ req, err := http.NewRequestWithContext(pmmapitests.Context, http.MethodPost, u.String(), bytes.NewReader(data))
+ require.NoError(t, err)
+
+ req.Header.Set("Content-Type", "application/json; charset=utf-8")
+
+ resp, b := doRequest(t, http.DefaultClient, req)
+ require.Equalf(t, http.StatusOK, resp.StatusCode, "failed to create user, status code: %d, response: %s", resp.StatusCode, b)
+
+ var m map[string]interface{}
+ err = json.Unmarshal(b, &m)
+ require.NoError(t, err)
+
+ return int(m["id"].(float64))
+}
+
+func setRole(t *testing.T, userID int, role string) {
+ u, err := url.Parse(pmmapitests.BaseURL.String())
+ require.NoError(t, err)
+ u.Path = "/graph/api/org/users/" + strconv.Itoa(userID)
+
+ // https://grafana.com/docs/http_api/org/#updates-the-given-user
+ data, err := json.Marshal(map[string]string{
+ "role": role,
+ })
+ require.NoError(t, err)
+
+ req, err := http.NewRequestWithContext(pmmapitests.Context, http.MethodPatch, u.String(), bytes.NewReader(data))
+ require.NoError(t, err)
+
+ req.Header.Set("Content-Type", "application/json; charset=utf-8")
+ resp, b := doRequest(t, http.DefaultClient, req)
+
+ require.Equalf(t, http.StatusOK, resp.StatusCode, "failed to set role for user, response: %s", b)
+}
+
+func deleteAPIKey(t *testing.T, apiKeyID int) {
+ // https://grafana.com/docs/grafana/latest/http_api/auth/#delete-api-key
+ u, err := url.Parse(pmmapitests.BaseURL.String())
+ require.NoError(t, err)
+ u.Path = "/graph/api/auth/keys/" + strconv.Itoa(apiKeyID)
+
+ req, err := http.NewRequestWithContext(pmmapitests.Context, http.MethodDelete, u.String(), nil)
+ require.NoError(t, err)
+
+ resp, b := doRequest(t, http.DefaultClient, req)
+ require.Equalf(t, http.StatusOK, resp.StatusCode, "failed to delete API Key, status code: %d, response: %s", resp.StatusCode, b)
+}
+
+func createAPIKeyWithRole(t *testing.T, name, role string) (int, string) {
+ u, err := url.Parse(pmmapitests.BaseURL.String())
+ require.NoError(t, err)
+ u.Path = "/graph/api/auth/keys"
+
+ // https://grafana.com/docs/grafana/latest/http_api/auth/#create-api-key
+ data, err := json.Marshal(map[string]string{
+ "name": name,
+ "role": role,
+ })
+ require.NoError(t, err)
+
+ req, err := http.NewRequestWithContext(pmmapitests.Context, http.MethodPost, u.String(), bytes.NewReader(data))
+ require.NoError(t, err)
+
+ req.Header.Set("Content-Type", "application/json; charset=utf-8")
+
+ resp, b := doRequest(t, http.DefaultClient, req)
+ require.Equalf(t, http.StatusOK, resp.StatusCode, "failed to create API key, status code: %d, response: %s", resp.StatusCode, b)
+
+ var m map[string]interface{}
+ err = json.Unmarshal(b, &m)
+ require.NoError(t, err)
+ apiKey := m["key"].(string)
+
+ u.User = nil
+ u.Path = "/graph/api/auth/key"
+ req, err = http.NewRequestWithContext(pmmapitests.Context, http.MethodGet, u.String(), bytes.NewReader(data))
+ require.NoError(t, err)
+ req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
+
+ resp, b = doRequest(t, http.DefaultClient, req)
+ require.Equalf(t, http.StatusOK, resp.StatusCode, "failed to get API key, status code: %d, response: %s", resp.StatusCode, b)
+
+ var k map[string]interface{}
+ err = json.Unmarshal(b, &k)
+ require.NoError(t, err)
+
+ apiKeyID := int(k["id"].(float64))
+
+ return apiKeyID, apiKey
+}
diff --git a/api-tests/server/checks_test.go b/api-tests/server/checks_test.go
new file mode 100644
index 0000000000..aaa62aac94
--- /dev/null
+++ b/api-tests/server/checks_test.go
@@ -0,0 +1,300 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package server
+
+import (
+ "testing"
+
+ "github.com/AlekSi/pointer"
+ managementClient "github.com/percona/pmm/api/managementpb/json/client"
+ "github.com/percona/pmm/api/managementpb/json/client/security_checks"
+ serverClient "github.com/percona/pmm/api/serverpb/json/client"
+ "github.com/percona/pmm/api/serverpb/json/client/server"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestStartChecks(t *testing.T) {
+ client := serverClient.Default.Server
+
+ t.Run("with enabled STT", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+ // Enabled STT
+ res, err := client.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ EnableStt: true,
+ EnableTelemetry: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.True(t, res.Payload.Settings.SttEnabled)
+ assert.True(t, res.Payload.Settings.TelemetryEnabled)
+
+ resp, err := managementClient.Default.SecurityChecks.StartSecurityChecks(nil)
+ require.NoError(t, err)
+ assert.NotNil(t, resp)
+ })
+
+ t.Run("with disabled STT", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+ // Disabled STT
+ res, err := client.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ DisableStt: true,
+ EnableTelemetry: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.False(t, res.Payload.Settings.SttEnabled)
+ assert.True(t, res.Payload.Settings.TelemetryEnabled)
+
+ resp, err := managementClient.Default.SecurityChecks.StartSecurityChecks(nil)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.FailedPrecondition, `STT is disabled.`)
+ assert.Nil(t, resp)
+ })
+}
+
+func TestGetSecurityCheckResults(t *testing.T) {
+ if !pmmapitests.RunSTTTests {
+ t.Skip("Skipping STT tests until we have environment: https://jira.percona.com/browse/PMM-5106")
+ }
+
+ client := serverClient.Default.Server
+
+ t.Run("with disabled STT", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+ // Disabled STT
+ res, err := client.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ DisableStt: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.False(t, res.Payload.Settings.SttEnabled)
+
+ results, err := managementClient.Default.SecurityChecks.GetSecurityCheckResults(nil)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.FailedPrecondition, `STT is disabled.`)
+ assert.Nil(t, results)
+ })
+
+ t.Run("with enabled STT", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+ // Enabled STT
+ res, err := client.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ EnableStt: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.True(t, res.Payload.Settings.SttEnabled)
+
+ resp, err := managementClient.Default.SecurityChecks.StartSecurityChecks(nil)
+ require.NoError(t, err)
+ assert.NotNil(t, resp)
+
+ results, err := managementClient.Default.SecurityChecks.GetSecurityCheckResults(nil)
+ require.NoError(t, err)
+ assert.NotNil(t, results)
+ })
+}
+
+func TestListSecurityChecks(t *testing.T) {
+ client := serverClient.Default.Server
+
+ defer restoreSettingsDefaults(t)
+ // Enable STT
+ res, err := client.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ EnableStt: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.True(t, res.Payload.Settings.SttEnabled)
+
+ resp, err := managementClient.Default.SecurityChecks.ListSecurityChecks(nil)
+ require.NoError(t, err)
+ assert.NotNil(t, resp)
+ assert.NotEmpty(t, resp.Payload.Checks)
+ for _, c := range resp.Payload.Checks {
+ assert.NotEmpty(t, c.Name, "%+v", c)
+ assert.NotEmpty(t, c.Summary, "%+v", c)
+ assert.NotEmpty(t, c.Description, "%+v", c)
+ }
+}
+
+func TestChangeSecurityChecks(t *testing.T) {
+ client := serverClient.Default.Server
+
+ t.Run("enable disable", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+ // Enable STT
+ res, err := client.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ EnableStt: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.True(t, res.Payload.Settings.SttEnabled)
+
+ resp, err := managementClient.Default.SecurityChecks.ListSecurityChecks(nil)
+ require.NoError(t, err)
+ require.NotEmpty(t, resp.Payload.Checks)
+
+ var check *security_checks.ChecksItems0
+
+ // enable ⥁ disable loop, it checks current state of first returned check and changes its state,
+ // then in second iteration it returns state to its origin.
+ for i := 0; i < 2; i++ {
+ check = resp.Payload.Checks[0]
+ params := &security_checks.ChangeSecurityChecksParams{
+ Body: security_checks.ChangeSecurityChecksBody{
+ Params: []*security_checks.ParamsItems0{
+ {
+ Name: check.Name,
+ Disable: !check.Disabled,
+ Enable: check.Disabled,
+ },
+ },
+ },
+ Context: pmmapitests.Context,
+ }
+
+ _, err = managementClient.Default.SecurityChecks.ChangeSecurityChecks(params)
+ require.NoError(t, err)
+
+ resp, err = managementClient.Default.SecurityChecks.ListSecurityChecks(nil)
+ require.NoError(t, err)
+ require.NotEmpty(t, resp.Payload.Checks)
+
+ for _, c := range resp.Payload.Checks {
+ if c.Name == check.Name {
+ assert.Equal(t, !check.Disabled, c.Disabled)
+ break
+ }
+ }
+ }
+ })
+
+ t.Run("change interval error", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+ // Enable STT
+ res, err := client.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ EnableStt: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.True(t, res.Payload.Settings.SttEnabled)
+
+ resp, err := managementClient.Default.SecurityChecks.ListSecurityChecks(nil)
+ require.NoError(t, err)
+ require.NotEmpty(t, resp.Payload.Checks)
+ assert.Equal(t, "STANDARD", *resp.Payload.Checks[0].Interval)
+
+ check := resp.Payload.Checks[0]
+ interval := "unknown_interval"
+ params := &security_checks.ChangeSecurityChecksParams{
+ Body: security_checks.ChangeSecurityChecksBody{
+ Params: []*security_checks.ParamsItems0{
+ {
+ Name: check.Name,
+ Interval: &interval,
+ },
+ },
+ },
+ Context: pmmapitests.Context,
+ }
+
+ _, err = managementClient.Default.SecurityChecks.ChangeSecurityChecks(params)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "unknown value \"\\\"unknown_interval\\\"\" for enum management.SecurityCheckInterval")
+ })
+
+ t.Run("change interval normal", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+ defer restoreCheckIntervalDefaults(t)
+ // Enable STT
+ res, err := client.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ EnableStt: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.True(t, res.Payload.Settings.SttEnabled)
+
+ resp, err := managementClient.Default.SecurityChecks.ListSecurityChecks(nil)
+ require.NoError(t, err)
+ require.NotEmpty(t, resp.Payload.Checks)
+ assert.Equal(t, "STANDARD", string(*resp.Payload.Checks[0].Interval))
+
+ // convert all checks to RARE interval
+ for _, check := range resp.Payload.Checks {
+ params := &security_checks.ChangeSecurityChecksParams{
+ Body: security_checks.ChangeSecurityChecksBody{
+ Params: []*security_checks.ParamsItems0{
+ {
+ Name: check.Name,
+ Interval: pointer.ToString(security_checks.ParamsItems0IntervalRARE),
+ },
+ },
+ },
+ Context: pmmapitests.Context,
+ }
+
+ _, err = managementClient.Default.SecurityChecks.ChangeSecurityChecks(params)
+ require.NoError(t, err)
+ }
+
+ resp, err = managementClient.Default.SecurityChecks.ListSecurityChecks(nil)
+ require.NoError(t, err)
+ require.NotEmpty(t, resp.Payload.Checks)
+
+ for _, check := range resp.Payload.Checks {
+ assert.Equal(t, "RARE", *check.Interval)
+ }
+
+ t.Run("intervals should be preserved on restart", func(t *testing.T) {
+ // Enable STT
+ res, err := client.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ EnableStt: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.True(t, res.Payload.Settings.SttEnabled)
+
+ _, err = managementClient.Default.SecurityChecks.StartSecurityChecks(nil)
+ require.NoError(t, err)
+
+ resp, err := managementClient.Default.SecurityChecks.ListSecurityChecks(nil)
+ require.NoError(t, err)
+ require.NotEmpty(t, resp.Payload.Checks)
+ assert.Equal(t, "RARE", *resp.Payload.Checks[0].Interval)
+ })
+ })
+}
diff --git a/api-tests/server/helpers.go b/api-tests/server/helpers.go
new file mode 100644
index 0000000000..5256ea4e02
--- /dev/null
+++ b/api-tests/server/helpers.go
@@ -0,0 +1,105 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package server
+
+import (
+ "testing"
+
+ "github.com/AlekSi/pointer"
+ managementClient "github.com/percona/pmm/api/managementpb/json/client"
+ "github.com/percona/pmm/api/managementpb/json/client/security_checks"
+ serverClient "github.com/percona/pmm/api/serverpb/json/client"
+ "github.com/percona/pmm/api/serverpb/json/client/server"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func restoreSettingsDefaults(t *testing.T) {
+ t.Helper()
+
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ DisableStt: true,
+ EnableTelemetry: true,
+ MetricsResolutions: &server.ChangeSettingsParamsBodyMetricsResolutions{
+ Hr: "5s",
+ Mr: "10s",
+ Lr: "60s",
+ },
+ SttCheckIntervals: &server.ChangeSettingsParamsBodySttCheckIntervals{
+ FrequentInterval: "14400s",
+ StandardInterval: "86400s",
+ RareInterval: "280800s",
+ },
+ DataRetention: "2592000s",
+ AWSPartitions: []string{"aws"},
+ RemoveAlertManagerURL: true,
+ RemoveAlertManagerRules: true,
+ RemoveEmailAlertingSettings: true,
+ RemoveSlackAlertingSettings: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, true, res.Payload.Settings.TelemetryEnabled)
+ assert.Equal(t, false, res.Payload.Settings.SttEnabled)
+ expectedResolutions := &server.ChangeSettingsOKBodySettingsMetricsResolutions{
+ Hr: "5s",
+ Mr: "10s",
+ Lr: "60s",
+ }
+ assert.Equal(t, expectedResolutions, res.Payload.Settings.MetricsResolutions)
+ expectedSTTIntervals := &server.ChangeSettingsOKBodySettingsSttCheckIntervals{
+ FrequentInterval: "14400s",
+ StandardInterval: "86400s",
+ RareInterval: "280800s",
+ }
+ assert.Equal(t, expectedSTTIntervals, res.Payload.Settings.SttCheckIntervals)
+ assert.Equal(t, "2592000s", res.Payload.Settings.DataRetention)
+ assert.Equal(t, []string{"aws"}, res.Payload.Settings.AWSPartitions)
+ assert.Equal(t, "", res.Payload.Settings.AlertManagerURL)
+ assert.Equal(t, "", res.Payload.Settings.AlertManagerRules)
+}
+
+func restoreCheckIntervalDefaults(t *testing.T) {
+ t.Helper()
+
+ resp, err := managementClient.Default.SecurityChecks.ListSecurityChecks(nil)
+ require.NoError(t, err)
+ require.NotEmpty(t, resp.Payload.Checks)
+
+ var params *security_checks.ChangeSecurityChecksParams
+
+ for _, check := range resp.Payload.Checks {
+ params = &security_checks.ChangeSecurityChecksParams{
+ Body: security_checks.ChangeSecurityChecksBody{
+ Params: []*security_checks.ParamsItems0{
+ {
+ Name: check.Name,
+ Interval: pointer.ToString(security_checks.ParamsItems0IntervalSTANDARD),
+ },
+ },
+ },
+ Context: pmmapitests.Context,
+ }
+
+ _, err = managementClient.Default.SecurityChecks.ChangeSecurityChecks(params)
+ require.NoError(t, err)
+ }
+}
diff --git a/api-tests/server/logs_test.go b/api-tests/server/logs_test.go
new file mode 100644
index 0000000000..2e682b9bf3
--- /dev/null
+++ b/api-tests/server/logs_test.go
@@ -0,0 +1,98 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package server
+
+import (
+ "archive/zip"
+ "bytes"
+ "os"
+ "sort"
+ "testing"
+
+ serverClient "github.com/percona/pmm/api/serverpb/json/client"
+ "github.com/percona/pmm/api/serverpb/json/client/server"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestDownloadLogs(t *testing.T) {
+ var buf bytes.Buffer
+ res, err := serverClient.Default.Server.Logs(&server.LogsParams{
+ Context: pmmapitests.Context,
+ }, &buf)
+ require.NoError(t, err)
+ require.NotNil(t, res)
+
+ r := bytes.NewReader(buf.Bytes())
+ zipR, err := zip.NewReader(r, r.Size())
+ assert.NoError(t, err)
+
+ expected := []string{
+ "alertmanager.base.yml",
+ "alertmanager.ini",
+ "alertmanager.log",
+ "alertmanager.yml",
+ "clickhouse-server.log",
+ "client/list.txt",
+ "client/pmm-admin-version.txt",
+ "client/pmm-agent-config.yaml",
+ "client/pmm-agent-version.txt",
+ "client/status.json",
+ "cron.log",
+ "dashboard-upgrade.log",
+ "grafana.log",
+ "installed.json",
+ "nginx.conf",
+ "nginx.log",
+ "pmm-agent.log",
+ "pmm-agent.yaml",
+ "pmm-managed.log",
+ "pmm-ssl.conf",
+ "pmm-version.txt",
+ "pmm.conf",
+ "pmm.ini",
+ "postgresql.log",
+ "prometheus.base.yml",
+ "qan-api2.ini",
+ "qan-api2.log",
+ "supervisorctl_status.log",
+ "supervisord.conf",
+ "supervisord.log",
+ "systemctl_status.log",
+ "victoriametrics-promscrape.yml",
+ "victoriametrics.ini",
+ "victoriametrics.log",
+ "victoriametrics_targets.json",
+ "vmalert.ini",
+ "vmalert.log",
+ }
+
+ if os.Getenv("PERCONA_TEST_DBAAS") == "1" {
+ expected = append(expected, "dbaas-controller.log")
+ sort.Strings(expected)
+ }
+
+ actual := make([]string, len(zipR.File))
+ for i, file := range zipR.File {
+ actual[i] = file.Name
+ }
+
+ sort.Strings(actual)
+ assert.Equal(t, expected, actual)
+}
diff --git a/api-tests/server/panics_test.go b/api-tests/server/panics_test.go
new file mode 100644
index 0000000000..49ac35afe9
--- /dev/null
+++ b/api-tests/server/panics_test.go
@@ -0,0 +1,45 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package server
+
+import (
+ "testing"
+
+ "google.golang.org/grpc/codes"
+
+ serverClient "github.com/percona/pmm/api/serverpb/json/client"
+ "github.com/percona/pmm/api/serverpb/json/client/server"
+ "github.com/stretchr/testify/assert"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestPanics(t *testing.T) {
+ for _, mode := range []string{"panic-error", "panic-fmterror", "panic-string"} {
+ mode := mode
+ t.Run(mode, func(t *testing.T) {
+ t.Parallel()
+
+ res, err := serverClient.Default.Server.Version(&server.VersionParams{
+ Dummy: &mode,
+ Context: pmmapitests.Context,
+ })
+ assert.Empty(t, res)
+ pmmapitests.AssertAPIErrorf(t, err, 500, codes.Internal, "Internal server error.")
+ })
+ }
+}
diff --git a/api-tests/server/platform_auth_test.go b/api-tests/server/platform_auth_test.go
new file mode 100644
index 0000000000..38ee7ddf48
--- /dev/null
+++ b/api-tests/server/platform_auth_test.go
@@ -0,0 +1,234 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package server
+
+import (
+ "os"
+ "os/user"
+ "strings"
+ "testing"
+
+ "github.com/brianvoe/gofakeit/v6"
+ serverClient "github.com/percona/pmm/api/serverpb/json/client"
+ "github.com/percona/pmm/api/serverpb/json/client/server"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+// Tests in this file cover Percona Platform authentication.
+
+func TestPlatform(t *testing.T) {
+ client := serverClient.Default.Server
+
+ t.Run("signUp", func(t *testing.T) {
+ t.Run("normal", func(t *testing.T) {
+ email, _, firstName, lastName := genCredentials(t)
+ _, err := client.PlatformSignUp(&server.PlatformSignUpParams{
+ Body: server.PlatformSignUpBody{
+ Email: email,
+ FirstName: firstName,
+ LastName: lastName,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ })
+
+ t.Run("invalid email", func(t *testing.T) {
+ _, _, firstName, lastName := genCredentials(t)
+ _, err := client.PlatformSignUp(&server.PlatformSignUpParams{
+ Body: server.PlatformSignUpBody{
+ Email: "not-email",
+ FirstName: firstName,
+ LastName: lastName,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Error Creating Your Account.")
+ })
+
+ t.Run("empty email", func(t *testing.T) {
+ _, _, firstName, lastName := genCredentials(t)
+ _, err := client.PlatformSignUp(&server.PlatformSignUpParams{
+ Body: server.PlatformSignUpBody{
+ Email: "",
+ FirstName: firstName,
+ LastName: lastName,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field Email: value '' must not be an empty string")
+ })
+
+ t.Run("empty first name", func(t *testing.T) {
+ email, _, _, lastName := genCredentials(t)
+ _, err := client.PlatformSignUp(&server.PlatformSignUpParams{
+ Body: server.PlatformSignUpBody{
+ Email: email,
+ FirstName: "",
+ LastName: lastName,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Error Creating Your Account.")
+ })
+
+ t.Run("empty last name", func(t *testing.T) {
+ email, _, firstName, _ := genCredentials(t)
+ _, err := client.PlatformSignUp(&server.PlatformSignUpParams{
+ Body: server.PlatformSignUpBody{
+ Email: email,
+ FirstName: firstName,
+ LastName: "",
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Error Creating Your Account.")
+ })
+ })
+
+ t.Run("signIn", func(t *testing.T) {
+ t.Skip("Skip till https://jira.percona.com/browse/SAAS-514 is implemented.")
+ email, password, _, _ := genCredentials(t)
+
+ _, err := client.PlatformSignUp(&server.PlatformSignUpParams{
+ Body: server.PlatformSignUpBody{
+ Email: email,
+ Password: password,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ t.Run("normal", func(t *testing.T) {
+ _, err = client.PlatformSignIn(&server.PlatformSignInParams{
+ Body: server.PlatformSignInBody{
+ Email: email,
+ Password: password,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ })
+
+ t.Run("wrong email", func(t *testing.T) {
+ _, err = client.PlatformSignIn(&server.PlatformSignInParams{
+ Body: server.PlatformSignInBody{
+ Email: "wrong@example.com",
+ Password: password,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Incorrect username or password.")
+ })
+
+ t.Run("wrong password", func(t *testing.T) {
+ _, err = client.PlatformSignIn(&server.PlatformSignInParams{
+ Body: server.PlatformSignInBody{
+ Email: email,
+ Password: "WrongPassword12345",
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "Incorrect username or password.")
+ })
+
+ t.Run("empty email", func(t *testing.T) {
+ _, err = client.PlatformSignIn(&server.PlatformSignInParams{
+ Body: server.PlatformSignInBody{
+ Email: "",
+ Password: password,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field Email: value '' must not be an empty string")
+ })
+
+ t.Run("empty password", func(t *testing.T) {
+ _, err = client.PlatformSignIn(&server.PlatformSignInParams{
+ Body: server.PlatformSignInBody{
+ Email: email,
+ Password: "",
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, "invalid field Password: value '' must not be an empty string")
+ })
+ })
+
+ t.Run("signOut", func(t *testing.T) {
+ t.Skip("Skip till https://jira.percona.com/browse/SAAS-514 is implemented.")
+ email, password, _, _ := genCredentials(t)
+
+ _, err := client.PlatformSignUp(&server.PlatformSignUpParams{
+ Body: server.PlatformSignUpBody{
+ Email: email,
+ Password: password,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ t.Run("normal", func(t *testing.T) {
+ _, err = client.PlatformSignIn(&server.PlatformSignInParams{
+ Body: server.PlatformSignInBody{
+ Email: email,
+ Password: password,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ _, err = client.PlatformSignOut(&server.PlatformSignOutParams{
+ Body: server.PlatformSignInBody{
+ Email: email,
+ Password: password,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ })
+
+ t.Run("no active session", func(t *testing.T) {
+ _, err = client.PlatformSignOut(&server.PlatformSignOutParams{
+ Body: server.PlatformSignInBody{
+ Email: email,
+ Password: password,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.FailedPrecondition, "No active sessions.")
+ })
+ })
+}
+
+// genCredentials creates test user email, password, firstName and lastName.
+func genCredentials(t *testing.T) (string, string, string, string) {
+ hostname, err := os.Hostname()
+ require.NoError(t, err)
+
+ u, err := user.Current()
+ require.NoError(t, err)
+
+ email := strings.Join([]string{u.Username, hostname, gofakeit.Email(), "test"}, ".")
+ password := gofakeit.Password(true, true, true, false, false, 14)
+ firstName := gofakeit.FirstName()
+ lastName := gofakeit.LastName()
+ return email, password, firstName, lastName
+}
diff --git a/api-tests/server/readyz_test.go b/api-tests/server/readyz_test.go
new file mode 100644
index 0000000000..3ff8d4c8be
--- /dev/null
+++ b/api-tests/server/readyz_test.go
@@ -0,0 +1,60 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package server
+
+import (
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestReadyz(t *testing.T) {
+ paths := []string{
+ "ping",
+ "v1/readyz",
+ }
+ for _, path := range paths {
+ path := path
+ t.Run(path, func(t *testing.T) {
+ t.Parallel()
+
+ // make a BaseURL without authentication
+ baseURL, err := url.Parse(pmmapitests.BaseURL.String())
+ require.NoError(t, err)
+ baseURL.User = nil
+
+ uri := baseURL.ResolveReference(&url.URL{
+ Path: path,
+ })
+
+ t.Logf("URI: %s", uri)
+ resp, err := http.Get(uri.String())
+ require.NoError(t, err)
+ defer resp.Body.Close() //nolint:errcheck
+ b, err := ioutil.ReadAll(resp.Body)
+ require.NoError(t, err)
+ assert.Equal(t, 200, resp.StatusCode, "response:\n%s", b)
+ assert.Equal(t, "{\n\n}", string(b))
+ })
+ }
+}
diff --git a/api-tests/server/settings_test.go b/api-tests/server/settings_test.go
new file mode 100644
index 0000000000..78e7899abe
--- /dev/null
+++ b/api-tests/server/settings_test.go
@@ -0,0 +1,968 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package server
+
+import (
+ "bytes"
+ "encoding/json"
+ "io/ioutil"
+ "net/http"
+ "net/http/httputil"
+ "net/url"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/brianvoe/gofakeit/v6"
+ "github.com/percona/pmm/api/alertmanager/amclient"
+ "github.com/percona/pmm/api/alertmanager/amclient/alert"
+ serverClient "github.com/percona/pmm/api/serverpb/json/client"
+ "github.com/percona/pmm/api/serverpb/json/client/server"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestSettings(t *testing.T) {
+ t.Run("GetSettings", func(t *testing.T) {
+ res, err := serverClient.Default.Server.GetSettings(nil)
+ require.NoError(t, err)
+ assert.True(t, res.Payload.Settings.TelemetryEnabled)
+ assert.False(t, res.Payload.Settings.SttEnabled)
+ expected := &server.GetSettingsOKBodySettingsMetricsResolutions{
+ Hr: "5s",
+ Mr: "10s",
+ Lr: "60s",
+ }
+ assert.Equal(t, expected, res.Payload.Settings.MetricsResolutions)
+ expectedSTTCheckIntervals := &server.GetSettingsOKBodySettingsSttCheckIntervals{
+ FrequentInterval: "14400s",
+ StandardInterval: "86400s",
+ RareInterval: "280800s",
+ }
+ assert.Equal(t, expectedSTTCheckIntervals, res.Payload.Settings.SttCheckIntervals)
+ assert.Equal(t, "2592000s", res.Payload.Settings.DataRetention)
+ assert.Equal(t, []string{"aws"}, res.Payload.Settings.AWSPartitions)
+ assert.True(t, res.Payload.Settings.AlertingEnabled)
+ assert.Empty(t, res.Payload.Settings.EmailAlertingSettings)
+ assert.Empty(t, res.Payload.Settings.SlackAlertingSettings)
+
+ t.Run("ChangeSettings", func(t *testing.T) {
+
+ defer restoreSettingsDefaults(t)
+
+ t.Run("ValidAlertingSettingsUpdate", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ email := gofakeit.Email()
+ smarthost := "0.0.0.0:8080"
+ username := "username"
+ password := "password"
+ identity := "identity"
+ secret := "secret"
+ slackURL := gofakeit.URL()
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ EnableAlerting: true,
+ EmailAlertingSettings: &server.ChangeSettingsParamsBodyEmailAlertingSettings{
+ From: email,
+ Smarthost: smarthost,
+ Username: username,
+ Password: password,
+ Identity: identity,
+ Secret: secret,
+ },
+ SlackAlertingSettings: &server.ChangeSettingsParamsBodySlackAlertingSettings{
+ URL: slackURL,
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.True(t, res.Payload.Settings.AlertingEnabled)
+ assert.Equal(t, email, res.Payload.Settings.EmailAlertingSettings.From)
+ assert.Equal(t, smarthost, res.Payload.Settings.EmailAlertingSettings.Smarthost)
+ assert.Equal(t, username, res.Payload.Settings.EmailAlertingSettings.Username)
+ // check that we don't expose password through the API.
+ assert.Empty(t, res.Payload.Settings.EmailAlertingSettings.Password)
+ assert.Equal(t, identity, res.Payload.Settings.EmailAlertingSettings.Identity)
+ assert.Equal(t, secret, res.Payload.Settings.EmailAlertingSettings.Secret)
+ assert.Equal(t, slackURL, res.Payload.Settings.SlackAlertingSettings.URL)
+ })
+
+ t.Run("InvalidBothEnableAndDisableAlerting", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ // since alerting is already enabled on managed by default using
+ // ENABLE_ALERTING env var, just passing DisableAlerting param satisfies
+ // the condition of both enable and disable alerting being true
+ DisableAlerting: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.FailedPrecondition, `Alerting is enabled via ENABLE_ALERTING environment variable.`)
+ assert.Empty(t, res)
+ })
+
+ t.Run("InvalidBothSlackAlertingSettingsAndRemoveSlackAlertingSettings", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ SlackAlertingSettings: &server.ChangeSettingsParamsBodySlackAlertingSettings{
+ URL: gofakeit.URL(),
+ },
+ RemoveSlackAlertingSettings: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, `Both slack_alerting_settings and remove_slack_alerting_settings are present.`)
+ assert.Empty(t, res)
+ })
+
+ t.Run("InvalidBothEmailAlertingSettingsAndRemoveEmailAlertingSettings", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ EmailAlertingSettings: &server.ChangeSettingsParamsBodyEmailAlertingSettings{
+ From: gofakeit.Email(),
+ Smarthost: "0.0.0.0:8080",
+ Username: "username",
+ Password: "password",
+ Identity: "identity",
+ Secret: "secret",
+ },
+ RemoveEmailAlertingSettings: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, `Both email_alerting_settings and remove_email_alerting_settings are present.`)
+ assert.Empty(t, res)
+ })
+
+ t.Run("InvalidBothEnableAndDisableSTT", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ EnableStt: true,
+ DisableStt: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, `Both enable_stt and disable_stt are present.`)
+ assert.Empty(t, res)
+ })
+
+ t.Run("EnableSTTAndEnableTelemetry", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ EnableStt: true,
+ EnableTelemetry: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.True(t, res.Payload.Settings.SttEnabled)
+ assert.True(t, res.Payload.Settings.TelemetryEnabled)
+ assert.Empty(t, err)
+
+ resg, err := serverClient.Default.Server.GetSettings(nil)
+ require.NoError(t, err)
+ assert.True(t, resg.Payload.Settings.TelemetryEnabled)
+ assert.True(t, resg.Payload.Settings.SttEnabled)
+ })
+
+ t.Run("EnableSTTAndDisableTelemetry", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ EnableStt: true,
+ DisableTelemetry: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, `Cannot enable STT while disabling telemetry.`)
+ assert.Empty(t, res)
+ })
+
+ t.Run("DisableSTTAndEnableTelemetry", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ DisableStt: true,
+ EnableTelemetry: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.False(t, res.Payload.Settings.SttEnabled)
+ assert.True(t, res.Payload.Settings.TelemetryEnabled)
+ assert.Empty(t, err)
+
+ resg, err := serverClient.Default.Server.GetSettings(nil)
+ require.NoError(t, err)
+ assert.True(t, resg.Payload.Settings.TelemetryEnabled)
+ assert.False(t, resg.Payload.Settings.SttEnabled)
+ })
+
+ t.Run("DisableSTTAndDisableTelemetry", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ DisableStt: true,
+ DisableTelemetry: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.False(t, res.Payload.Settings.SttEnabled)
+ assert.False(t, res.Payload.Settings.TelemetryEnabled)
+ assert.Empty(t, err)
+
+ resg, err := serverClient.Default.Server.GetSettings(nil)
+ require.NoError(t, err)
+ assert.False(t, resg.Payload.Settings.TelemetryEnabled)
+ assert.False(t, resg.Payload.Settings.SttEnabled)
+ })
+
+ t.Run("EnableSTTWhileTelemetryEnabled", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ // Ensure Telemetry is enabled
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ EnableTelemetry: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.True(t, res.Payload.Settings.TelemetryEnabled)
+
+ res, err = serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ EnableStt: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ assert.True(t, res.Payload.Settings.SttEnabled)
+ assert.Empty(t, err)
+
+ resg, err := serverClient.Default.Server.GetSettings(nil)
+ require.NoError(t, err)
+ assert.True(t, resg.Payload.Settings.TelemetryEnabled)
+ assert.True(t, resg.Payload.Settings.SttEnabled)
+ })
+
+ t.Run("VerifyFailedChecksInAlertmanager", func(t *testing.T) {
+ if !pmmapitests.RunSTTTests {
+ t.Skip("Skipping STT tests until we have environment: https://jira.percona.com/browse/PMM-5106")
+ }
+
+ defer restoreSettingsDefaults(t)
+
+ // Enabling STT
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ EnableStt: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.True(t, res.Payload.Settings.TelemetryEnabled)
+
+ // 120 sec ping for failed checks alerts to appear in alertmanager
+ var alertsCount int
+ for i := 0; i < 120; i++ {
+ res, err := amclient.Default.Alert.GetAlerts(&alert.GetAlertsParams{
+ Filter: []string{"stt_check=1"},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ if len(res.Payload) == 0 {
+ time.Sleep(1 * time.Second)
+ continue
+ }
+
+ for _, v := range res.Payload {
+ t.Logf("%+v", v)
+
+ assert.Contains(t, v.Annotations, "summary")
+
+ assert.Equal(t, "1", v.Labels["stt_check"])
+
+ assert.Contains(t, v.Labels, "agent_id")
+ assert.Contains(t, v.Labels, "agent_type")
+ assert.Contains(t, v.Labels, "alert_id")
+ assert.Contains(t, v.Labels, "alertname")
+ assert.Contains(t, v.Labels, "node_id")
+ assert.Contains(t, v.Labels, "node_name")
+ assert.Contains(t, v.Labels, "node_type")
+ assert.Contains(t, v.Labels, "service_id")
+ assert.Contains(t, v.Labels, "service_name")
+ assert.Contains(t, v.Labels, "service_type")
+ assert.Contains(t, v.Labels, "severity")
+ }
+ alertsCount = len(res.Payload)
+ break
+ }
+ assert.Greater(t, alertsCount, 0, "No alerts met")
+ })
+
+ t.Run("DisableSTTWhileItIsDisabled", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ DisableStt: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.False(t, res.Payload.Settings.SttEnabled)
+ assert.Empty(t, err)
+
+ resg, err := serverClient.Default.Server.GetSettings(nil)
+ require.NoError(t, err)
+ assert.True(t, resg.Payload.Settings.TelemetryEnabled)
+ assert.False(t, resg.Payload.Settings.SttEnabled)
+ })
+
+ t.Run("STTEnabledState", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ EnableStt: true,
+ },
+ Context: pmmapitests.Context,
+ })
+
+ require.NoError(t, err)
+ assert.True(t, res.Payload.Settings.SttEnabled)
+ assert.Empty(t, err)
+
+ resg, err := serverClient.Default.Server.GetSettings(nil)
+ require.NoError(t, err)
+ assert.True(t, resg.Payload.Settings.TelemetryEnabled)
+ assert.True(t, resg.Payload.Settings.SttEnabled)
+
+ t.Run("EnableSTTWhileItIsEnabled", func(t *testing.T) {
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ EnableStt: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.True(t, res.Payload.Settings.SttEnabled)
+ assert.Empty(t, err)
+
+ resg, err := serverClient.Default.Server.GetSettings(nil)
+ require.NoError(t, err)
+ assert.True(t, resg.Payload.Settings.TelemetryEnabled)
+ assert.True(t, resg.Payload.Settings.SttEnabled)
+ })
+
+ t.Run("DisableTelemetryWhileSTTEnabled", func(t *testing.T) {
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ DisableTelemetry: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, `Cannot disable telemetry while STT is enabled.`)
+ assert.Empty(t, res)
+ })
+ })
+
+ t.Run("TelemetryDisabledState", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ DisableTelemetry: true,
+ },
+ Context: pmmapitests.Context,
+ })
+
+ require.NoError(t, err)
+ assert.False(t, res.Payload.Settings.TelemetryEnabled)
+ assert.Empty(t, err)
+
+ resg, err := serverClient.Default.Server.GetSettings(nil)
+ require.NoError(t, err)
+ assert.False(t, resg.Payload.Settings.TelemetryEnabled)
+ assert.False(t, resg.Payload.Settings.SttEnabled)
+
+ t.Run("EnableSTTWhileTelemetryDisabled", func(t *testing.T) {
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ EnableStt: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, `Cannot enable STT while telemetry is disabled.`)
+ assert.Empty(t, res)
+
+ resg, err := serverClient.Default.Server.GetSettings(nil)
+ require.NoError(t, err)
+ assert.False(t, resg.Payload.Settings.TelemetryEnabled)
+ assert.False(t, resg.Payload.Settings.SttEnabled)
+ })
+
+ t.Run("EnableTelemetryWhileItIsDisabled", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ EnableTelemetry: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.True(t, res.Payload.Settings.TelemetryEnabled)
+
+ resg, err := serverClient.Default.Server.GetSettings(nil)
+ require.NoError(t, err)
+ assert.True(t, resg.Payload.Settings.TelemetryEnabled)
+ assert.False(t, resg.Payload.Settings.SttEnabled)
+ })
+ })
+
+ t.Run("InvalidBothEnableAndDisableTelemetry", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ EnableTelemetry: true,
+ DisableTelemetry: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, `Both enable_telemetry and disable_telemetry are present.`)
+ assert.Empty(t, res)
+ })
+
+ t.Run("InvalidPartition", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ AWSPartitions: []string{"aws-123"},
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, `aws_partitions: partition "aws-123" is invalid`)
+ assert.Empty(t, res)
+ })
+
+ t.Run("TooManyPartitions", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ AWSPartitions: []string{"aws", "aws", "aws", "aws", "aws", "aws"},
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, `aws_partitions: list is too long`)
+ assert.Empty(t, res)
+ })
+
+ t.Run("HRInvalid", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ MetricsResolutions: &server.ChangeSettingsParamsBodyMetricsResolutions{
+ Hr: "1",
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, `bad Duration: time: missing unit in duration "1"`)
+ assert.Empty(t, res)
+ })
+
+ t.Run("HRTooSmall", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ MetricsResolutions: &server.ChangeSettingsParamsBodyMetricsResolutions{
+ Hr: "0.5s",
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, `hr: minimal resolution is 1s`)
+ assert.Empty(t, res)
+ })
+
+ t.Run("HRFractional", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ MetricsResolutions: &server.ChangeSettingsParamsBodyMetricsResolutions{
+ Hr: "1.5s",
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, `hr: should be a natural number of seconds`)
+ assert.Empty(t, res)
+ })
+
+ t.Run("STTCheckIntervalInvalid", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ SttCheckIntervals: &server.ChangeSettingsParamsBodySttCheckIntervals{
+ FrequentInterval: "1",
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, `bad Duration: time: missing unit in duration "1"`)
+ assert.Empty(t, res)
+ })
+
+ t.Run("STTCheckIntervalTooSmall", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ SttCheckIntervals: &server.ChangeSettingsParamsBodySttCheckIntervals{
+ StandardInterval: "0.9s",
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, `standard_interval: minimal resolution is 1s`)
+ assert.Empty(t, res)
+ })
+
+ t.Run("STTCheckIntervalFractional", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ SttCheckIntervals: &server.ChangeSettingsParamsBodySttCheckIntervals{
+ RareInterval: "1.5s",
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, `rare_interval: should be a natural number of seconds`)
+ assert.Empty(t, res)
+ })
+
+ t.Run("DataRetentionInvalid", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ DataRetention: "1",
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, `bad Duration: time: missing unit in duration "1"`)
+ assert.Empty(t, res)
+ })
+
+ t.Run("DataRetentionInvalidToSmall", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ DataRetention: "10s",
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, `data_retention: minimal resolution is 24h`)
+ assert.Empty(t, res)
+ })
+
+ t.Run("DataRetentionFractional", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ DataRetention: "36h",
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, `data_retention: should be a natural number of days`)
+ assert.Empty(t, res)
+ })
+
+ t.Run("InvalidSSHKey", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ SSHKey: "some-invalid-ssh-key",
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, `Invalid SSH key.`)
+ assert.Empty(t, res)
+ })
+
+ t.Run("NoAdminUserForSSH", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ sshKey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQClY/8sz3w03vA2bY6mBFgUzrvb2FIoHw8ZjUXGGClJzJg5HC3jW1m5df7TOIkx0bt6Da2UOhuCvS4o27IT1aiHXVFydppp6ghQRB6saiiW2TKlQ7B+mXatwVaOIkO381kEjgijAs0LJnNRGpqQW0ZEAxVMz4a8puaZmVNicYSVYs4kV3QZsHuqn7jHbxs5NGAO+uRRSjcuPXregsyd87RAUHkGmNrwNFln/XddMzdGMwqZOuZWuxIXBqSrSX927XGHAJlUaOmLz5etZXHzfAY1Zxfu39r66Sx95bpm3JBmc/Ewfr8T2WL0cqynkpH+3QQBCjweTHzBE+lpXHdR2se1 qsandbox"
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ SSHKey: sshKey,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 500, codes.Internal, `Internal server error.`)
+ assert.Empty(t, res)
+ })
+
+ t.Run("OK", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ DisableTelemetry: true,
+ MetricsResolutions: &server.ChangeSettingsParamsBodyMetricsResolutions{
+ Hr: "2s",
+ Mr: "15s",
+ Lr: "2m",
+ },
+ DataRetention: "240h",
+ AWSPartitions: []string{"aws-cn", "aws", "aws-cn"}, // duplicates are ok
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.False(t, res.Payload.Settings.TelemetryEnabled)
+ expected := &server.ChangeSettingsOKBodySettingsMetricsResolutions{
+ Hr: "2s",
+ Mr: "15s",
+ Lr: "120s",
+ }
+ assert.Equal(t, expected, res.Payload.Settings.MetricsResolutions)
+ assert.Equal(t, []string{"aws", "aws-cn"}, res.Payload.Settings.AWSPartitions)
+
+ getRes, err := serverClient.Default.Server.GetSettings(nil)
+ require.NoError(t, err)
+ assert.False(t, getRes.Payload.Settings.TelemetryEnabled)
+ getExpected := &server.GetSettingsOKBodySettingsMetricsResolutions{
+ Hr: "2s",
+ Mr: "15s",
+ Lr: "120s",
+ }
+ assert.Equal(t, getExpected, getRes.Payload.Settings.MetricsResolutions)
+ assert.Equal(t, "864000s", res.Payload.Settings.DataRetention)
+ assert.Equal(t, []string{"aws", "aws-cn"}, res.Payload.Settings.AWSPartitions)
+
+ t.Run("DefaultsAreNotRestored", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.False(t, res.Payload.Settings.TelemetryEnabled)
+ expected := &server.ChangeSettingsOKBodySettingsMetricsResolutions{
+ Hr: "2s",
+ Mr: "15s",
+ Lr: "120s",
+ }
+ assert.Equal(t, expected, res.Payload.Settings.MetricsResolutions)
+ assert.Equal(t, []string{"aws", "aws-cn"}, res.Payload.Settings.AWSPartitions)
+
+ // Check if the values were persisted
+ getRes, err := serverClient.Default.Server.GetSettings(nil)
+ require.NoError(t, err)
+ assert.False(t, getRes.Payload.Settings.TelemetryEnabled)
+ getExpected := &server.GetSettingsOKBodySettingsMetricsResolutions{
+ Hr: "2s",
+ Mr: "15s",
+ Lr: "120s",
+ }
+ assert.Equal(t, getExpected, getRes.Payload.Settings.MetricsResolutions)
+ assert.Equal(t, "864000s", res.Payload.Settings.DataRetention)
+ assert.Equal(t, []string{"aws", "aws-cn"}, res.Payload.Settings.AWSPartitions)
+ })
+ })
+
+ t.Run("STTCheckIntervalsValid", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ SttCheckIntervals: &server.ChangeSettingsParamsBodySttCheckIntervals{
+ RareInterval: "8h",
+ StandardInterval: "30m",
+ FrequentInterval: "20s",
+ },
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ expected := &server.ChangeSettingsOKBodySettingsSttCheckIntervals{
+ RareInterval: "28800s",
+ StandardInterval: "1800s",
+ FrequentInterval: "20s",
+ }
+ assert.Equal(t, expected, res.Payload.Settings.SttCheckIntervals)
+
+ getRes, err := serverClient.Default.Server.GetSettings(nil)
+ require.NoError(t, err)
+ getExpected := &server.GetSettingsOKBodySettingsSttCheckIntervals{
+ RareInterval: "28800s",
+ StandardInterval: "1800s",
+ FrequentInterval: "20s",
+ }
+ assert.Equal(t, getExpected, getRes.Payload.Settings.SttCheckIntervals)
+
+ t.Run("DefaultsAreNotRestored", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ expected := &server.ChangeSettingsOKBodySettingsSttCheckIntervals{
+ RareInterval: "28800s",
+ StandardInterval: "1800s",
+ FrequentInterval: "20s",
+ }
+ assert.Equal(t, expected, res.Payload.Settings.SttCheckIntervals)
+
+ // Check if the values were persisted
+ getRes, err := serverClient.Default.Server.GetSettings(nil)
+ require.NoError(t, err)
+ getExpected := &server.GetSettingsOKBodySettingsSttCheckIntervals{
+ RareInterval: "28800s",
+ StandardInterval: "1800s",
+ FrequentInterval: "20s",
+ }
+ assert.Equal(t, getExpected, getRes.Payload.Settings.SttCheckIntervals)
+ })
+ })
+
+ t.Run("AlertManager", func(t *testing.T) {
+ t.Run("SetInvalid", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ url := "http://localhost:1234/"
+ rules := `invalid rules`
+
+ _, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ AlertManagerURL: url,
+ AlertManagerRules: rules,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, `Invalid alerting rules.`)
+
+ gets, err := serverClient.Default.Server.GetSettings(nil)
+ require.NoError(t, err)
+ assert.Empty(t, gets.Payload.Settings.AlertManagerURL)
+ assert.Empty(t, gets.Payload.Settings.AlertManagerRules)
+ })
+
+ t.Run("SetAndRemoveInvalid", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ _, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ AlertManagerURL: "invalid url",
+ RemoveAlertManagerURL: true,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.InvalidArgument, `Both alert_manager_url and remove_alert_manager_url are present.`)
+
+ gets, err := serverClient.Default.Server.GetSettings(nil)
+ require.NoError(t, err)
+ assert.Empty(t, gets.Payload.Settings.AlertManagerURL)
+ assert.Empty(t, gets.Payload.Settings.AlertManagerRules)
+ })
+
+ t.Run("SetValid", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ url := "http://localhost:1234/"
+ rules := strings.TrimSpace(`
+groups:
+- name: example
+ rules:
+ - alert: HighRequestLatency
+ expr: job:request_latency_seconds:mean5m{job="myjob"} > 0.5
+ for: 10m
+ labels:
+ severity: page
+ annotations:
+ summary: High request latency
+ `) + "\n"
+
+ res, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{
+ AlertManagerURL: url,
+ AlertManagerRules: rules,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, url, res.Payload.Settings.AlertManagerURL)
+ assert.Equal(t, rules, res.Payload.Settings.AlertManagerRules)
+
+ gets, err := serverClient.Default.Server.GetSettings(nil)
+ require.NoError(t, err)
+ assert.Equal(t, url, gets.Payload.Settings.AlertManagerURL)
+ assert.Equal(t, rules, gets.Payload.Settings.AlertManagerRules)
+
+ t.Run("EmptyShouldNotRemove", func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ _, err := serverClient.Default.Server.ChangeSettings(&server.ChangeSettingsParams{
+ Body: server.ChangeSettingsBody{},
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+
+ gets, err = serverClient.Default.Server.GetSettings(nil)
+ require.NoError(t, err)
+ assert.Equal(t, url, gets.Payload.Settings.AlertManagerURL)
+ assert.Equal(t, rules, gets.Payload.Settings.AlertManagerRules)
+ })
+ })
+ })
+
+ t.Run("grpc-gateway", func(t *testing.T) {
+ // Test with pure JSON without swagger for tracking grpc-gateway behavior:
+ // https://github.com/grpc-ecosystem/grpc-gateway/issues/400
+
+ // do not use generated types as they can do extra work in generated methods
+ type params struct {
+ Settings struct {
+ MetricsResolutions struct {
+ LR string `json:"lr"`
+ } `json:"metrics_resolutions"`
+ } `json:"settings"`
+ }
+ changeURI := pmmapitests.BaseURL.ResolveReference(&url.URL{
+ Path: "v1/Settings/Change",
+ })
+ getURI := pmmapitests.BaseURL.ResolveReference(&url.URL{
+ Path: "v1/Settings/Get",
+ })
+
+ for change, get := range map[string]string{
+ "59s": "59s",
+ "60s": "60s",
+ "61s": "61s",
+ "61": "", // no suffix => error
+ "2m": "120s",
+ "1h": "3600s",
+ "1d": "", // d suffix => error
+ "1w": "", // w suffix => error
+ } {
+ change, get := change, get
+ t.Run(change, func(t *testing.T) {
+ defer restoreSettingsDefaults(t)
+
+ var p params
+ p.Settings.MetricsResolutions.LR = change
+ b, err := json.Marshal(p.Settings)
+ require.NoError(t, err)
+ req, err := http.NewRequest("POST", changeURI.String(), bytes.NewReader(b))
+ require.NoError(t, err)
+ if pmmapitests.Debug {
+ b, err = httputil.DumpRequestOut(req, true)
+ require.NoError(t, err)
+ t.Logf("Request:\n%s", b)
+ }
+
+ resp, err := http.DefaultClient.Do(req)
+ require.NoError(t, err)
+ if pmmapitests.Debug {
+ b, err = httputil.DumpResponse(resp, true)
+ require.NoError(t, err)
+ t.Logf("Response:\n%s", b)
+ }
+ b, err = ioutil.ReadAll(resp.Body)
+ assert.NoError(t, err)
+ resp.Body.Close() //nolint:errcheck
+
+ if get == "" {
+ assert.Equal(t, 400, resp.StatusCode, "response:\n%s", b)
+ return
+ }
+ assert.Equal(t, 200, resp.StatusCode, "response:\n%s", b)
+
+ p.Settings.MetricsResolutions.LR = ""
+ err = json.Unmarshal(b, &p)
+ require.NoError(t, err)
+ assert.Equal(t, get, p.Settings.MetricsResolutions.LR, "Change")
+
+ req, err = http.NewRequest("POST", getURI.String(), nil)
+ require.NoError(t, err)
+ if pmmapitests.Debug {
+ b, err = httputil.DumpRequestOut(req, true)
+ require.NoError(t, err)
+ t.Logf("Request:\n%s", b)
+ }
+
+ resp, err = http.DefaultClient.Do(req)
+ require.NoError(t, err)
+ if pmmapitests.Debug {
+ b, err = httputil.DumpResponse(resp, true)
+ require.NoError(t, err)
+ t.Logf("Response:\n%s", b)
+ }
+ b, err = ioutil.ReadAll(resp.Body)
+ assert.NoError(t, err)
+ resp.Body.Close() //nolint:errcheck
+ assert.Equal(t, 200, resp.StatusCode, "response:\n%s", b)
+
+ p.Settings.MetricsResolutions.LR = ""
+ err = json.Unmarshal(b, &p)
+ require.NoError(t, err)
+ assert.Equal(t, get, p.Settings.MetricsResolutions.LR, "Get")
+ })
+ }
+ })
+ })
+ })
+}
diff --git a/api-tests/server/stt_metrics_test.go b/api-tests/server/stt_metrics_test.go
new file mode 100644
index 0000000000..e71d85c92a
--- /dev/null
+++ b/api-tests/server/stt_metrics_test.go
@@ -0,0 +1,93 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package server
+
+import (
+ "context"
+ "net/url"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/prometheus/client_golang/api"
+ promapi "github.com/prometheus/client_golang/api/prometheus/v1"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestSTTMetrics(t *testing.T) {
+ if !pmmapitests.RunSTTTests {
+ t.Skip("Skipping STT tests until we have environment: https://jira.percona.com/browse/PMM-5106")
+ }
+
+ t.Run("StartSTTChecksAndRecordMetrics", func(t *testing.T) {
+ client, err := api.NewClient(api.Config{
+ Address: pmmapitests.BaseURL.ResolveReference(&url.URL{
+ Path: "/prometheus",
+ }).String(),
+ })
+ require.NoError(t, err)
+ promClient := promapi.NewAPI(client)
+
+ testCases := []struct {
+ query string
+ metricType string
+ expectedValues []string
+ }{
+ {
+ query: "pmm_managed_checks_alerts_generated_total",
+ metricType: "vector",
+ expectedValues: []string{
+ `pmm_managed_checks_alerts_generated_total{check_type="MONGODB_BUILDINFO", instance="pmm-server", job="pmm-managed", service_type="mongodb"} => 0`,
+ `pmm_managed_checks_alerts_generated_total{check_type="MONGODB_GETCMDLINEOPTS", instance="pmm-server", job="pmm-managed", service_type="mongodb"} => 0`,
+ `pmm_managed_checks_alerts_generated_total{check_type="MONGODB_GETPARAMETER", instance="pmm-server", job="pmm-managed", service_type="mongodb"} => 0`,
+ `pmm_managed_checks_alerts_generated_total{check_type="MYSQL_SELECT", instance="pmm-server", job="pmm-managed", service_type="mysql"} => 0`,
+ `pmm_managed_checks_alerts_generated_total{check_type="MYSQL_SHOW", instance="pmm-server", job="pmm-managed", service_type="mysql"} => 0`,
+ `pmm_managed_checks_alerts_generated_total{check_type="POSTGRESQL_SELECT", instance="pmm-server", job="pmm-managed", service_type="postgresql"} => 0`,
+ `pmm_managed_checks_alerts_generated_total{check_type="POSTGRESQL_SHOW", instance="pmm-server", job="pmm-managed", service_type="postgresql"} => 0`},
+ },
+ {
+ query: "pmm_managed_checks_scripts_executed_total",
+ metricType: "vector",
+ expectedValues: []string{
+ `pmm_managed_checks_scripts_executed_total{instance="pmm-server", job="pmm-managed", service_type="mongodb"} => 0`,
+ `pmm_managed_checks_scripts_executed_total{instance="pmm-server", job="pmm-managed", service_type="mysql"} => 0`,
+ `pmm_managed_checks_scripts_executed_total{instance="pmm-server", job="pmm-managed", service_type="postgresql"} => 0`},
+ },
+ }
+
+ for _, tc := range testCases {
+ result, _, err := promClient.Query(context.Background(),
+ tc.query, time.Now())
+
+ var actualValues []string
+ for _, s := range strings.Split(result.String(), "\n") {
+ // remove the timestamp from the values
+ metric := strings.Split(s, " @")
+ actualValues = append(actualValues, metric[0])
+ }
+
+ require.NoError(t, err)
+ assert.NotEmpty(t, result)
+ assert.Len(t, result, len(tc.expectedValues))
+ assert.Equal(t, tc.metricType, result.Type().String())
+ assert.Equal(t, tc.expectedValues, actualValues)
+ }
+ })
+}
diff --git a/api-tests/server/updates_test.go b/api-tests/server/updates_test.go
new file mode 100644
index 0000000000..a37dbb0e83
--- /dev/null
+++ b/api-tests/server/updates_test.go
@@ -0,0 +1,259 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package server
+
+import (
+ "net/url"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/davecgh/go-spew/spew"
+ serverClient "github.com/percona/pmm/api/serverpb/json/client"
+ "github.com/percona/pmm/api/serverpb/json/client/server"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc/codes"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestCheckUpdates(t *testing.T) {
+ // do not run this test in parallel with other tests as it also tests timings
+
+ const fast, slow = 5 * time.Second, 60 * time.Second
+
+ // that call should always be fast
+ version, err := serverClient.Default.Server.Version(server.NewVersionParamsWithTimeout(fast))
+ require.NoError(t, err)
+ if version.Payload.Server == nil || version.Payload.Server.Version == "" {
+ t.Skip("skipping test in developer's environment")
+ }
+
+ params := &server.CheckUpdatesParams{
+ Context: pmmapitests.Context,
+ }
+ params.SetTimeout(slow) // that call can be slow with a cold cache
+ res, err := serverClient.Default.Server.CheckUpdates(params)
+ require.NoError(t, err)
+
+ require.NotEmpty(t, res.Payload.Installed)
+ assert.True(t, strings.HasPrefix(res.Payload.Installed.Version, "2."),
+ "installed.version = %q should have '2.' prefix", res.Payload.Installed.Version)
+ assert.NotEmpty(t, res.Payload.Installed.FullVersion)
+ require.NotEmpty(t, res.Payload.Installed.Timestamp)
+ ts := time.Time(res.Payload.Installed.Timestamp)
+ hour, min, _ := ts.Clock()
+ assert.Zero(t, hour, "installed.timestamp should contain only date")
+ assert.Zero(t, min, "installed.timestamp should contain only date")
+
+ require.NotEmpty(t, res.Payload.Latest)
+ assert.True(t, strings.HasPrefix(res.Payload.Latest.Version, "2."),
+ "latest.version = %q should have '2.' prefix", res.Payload.Latest.Version)
+ assert.NotEmpty(t, res.Payload.Latest.FullVersion)
+ require.NotEmpty(t, res.Payload.Latest.Timestamp)
+ ts = time.Time(res.Payload.Latest.Timestamp)
+ hour, min, _ = ts.Clock()
+ assert.Zero(t, hour, "latest.timestamp should contain only date")
+ assert.Zero(t, min, "latest.timestamp should contain only date")
+
+ if res.Payload.UpdateAvailable {
+ assert.NotEqual(t, res.Payload.Installed.FullVersion, res.Payload.Latest.FullVersion)
+ assert.NotEqual(t, res.Payload.Installed.Timestamp, res.Payload.Latest.Timestamp)
+ assert.True(t, strings.HasPrefix(res.Payload.LatestNewsURL, "https://per.co.na/pmm/2."), "latest_news_url = %q", res.Payload.LatestNewsURL)
+ } else {
+ assert.Equal(t, res.Payload.Installed.FullVersion, res.Payload.Latest.FullVersion)
+ assert.Equal(t, res.Payload.Installed.Timestamp, res.Payload.Latest.Timestamp)
+ assert.Empty(t, res.Payload.LatestNewsURL, "latest_news_url should be empty")
+ }
+ assert.NotEmpty(t, res.Payload.LastCheck)
+
+ t.Run("HotCache", func(t *testing.T) {
+ params = &server.CheckUpdatesParams{
+ Context: pmmapitests.Context,
+ }
+ params.SetTimeout(fast) // that call should be fast with hot cache
+ resAgain, err := serverClient.Default.Server.CheckUpdates(params)
+ require.NoError(t, err)
+
+ assert.Equal(t, res.Payload, resAgain.Payload)
+ })
+
+ t.Run("Force", func(t *testing.T) {
+ params = &server.CheckUpdatesParams{
+ Body: server.CheckUpdatesBody{
+ Force: true,
+ },
+ Context: pmmapitests.Context,
+ }
+ params.SetTimeout(slow) // that call with force can be slow
+ resForce, err := serverClient.Default.Server.CheckUpdates(params)
+ require.NoError(t, err)
+
+ assert.Equal(t, res.Payload.Installed, resForce.Payload.Installed)
+ assert.Equal(t, resForce.Payload.Installed.FullVersion != resForce.Payload.Latest.FullVersion, resForce.Payload.UpdateAvailable)
+ assert.NotEqual(t, res.Payload.LastCheck, resForce.Payload.LastCheck)
+ })
+}
+
+func TestUpdate(t *testing.T) {
+ // do not run this test in parallel with other tests
+
+ if !pmmapitests.RunUpdateTest {
+ t.Skip("skipping PMM Server update test")
+ }
+
+ // check that pmm-managed and pmm-update versions match
+ version, err := serverClient.Default.Server.Version(nil)
+ require.NoError(t, err)
+ require.NotNil(t, version.Payload)
+ t.Logf("Before update: %s", spew.Sdump(version.Payload))
+ assert.True(t, strings.HasPrefix(version.Payload.Managed.Version, version.Payload.Version),
+ "managed.version = %q should have %q prefix", version.Payload.Managed.Version, version.Payload.Version)
+ assert.True(t, strings.HasPrefix(version.Payload.Server.Version, version.Payload.Version),
+ "server.version = %q should have %q prefix", version.Payload.Server.Version, version.Payload.Version)
+
+ // make a new client without authentication
+ baseURL, err := url.Parse(pmmapitests.BaseURL.String())
+ require.NoError(t, err)
+ baseURL.User = nil
+ noAuthClient := serverClient.New(pmmapitests.Transport(baseURL, true), nil)
+
+ // without authentication
+ _, err = noAuthClient.Server.StartUpdate(nil)
+ pmmapitests.AssertAPIErrorf(t, err, 401, codes.Unauthenticated, "Unauthorized")
+
+ // with authentication
+ startRes, err := serverClient.Default.Server.StartUpdate(nil)
+ require.NoError(t, err)
+ authToken := startRes.Payload.AuthToken
+ logOffset := startRes.Payload.LogOffset
+ require.NotEmpty(t, authToken)
+ assert.Zero(t, logOffset)
+
+ _, err = serverClient.Default.Server.StartUpdate(nil)
+ pmmapitests.AssertAPIErrorf(t, err, 400, codes.FailedPrecondition, "Update is already running.")
+
+ // without token
+ _, err = noAuthClient.Server.UpdateStatus(&server.UpdateStatusParams{
+ Body: server.UpdateStatusBody{
+ LogOffset: logOffset,
+ },
+ Context: pmmapitests.Context,
+ })
+ pmmapitests.AssertAPIErrorf(t, err, 403, codes.PermissionDenied, "Invalid authentication token.")
+
+ // read log lines like UI would do, but without delays to increase a chance for race detector to spot something
+ var lastLine string
+ var retries int
+ for {
+ start := time.Now()
+ statusRes, err := noAuthClient.Server.UpdateStatus(&server.UpdateStatusParams{
+ Body: server.UpdateStatusBody{
+ AuthToken: authToken,
+ LogOffset: logOffset,
+ },
+ Context: pmmapitests.Context,
+ })
+ if err != nil {
+ // check that we know and understand all possible errors
+ switch err := err.(type) {
+ case *url.Error:
+ // *net.OpError, http.nothingWrittenError, or just io.EOF
+ case *pmmapitests.ErrFromNginx:
+ // nothing
+ case *server.UpdateStatusDefault:
+ assert.Equal(t, 503, err.Code(), "%[1]T %[1]s", err)
+ default:
+ t.Fatalf("%#v", err)
+ }
+ continue
+ }
+ dur := time.Since(start)
+ t.Logf("%s, offset = %d->%d, done = %t:\n%s", dur, logOffset, statusRes.Payload.LogOffset,
+ statusRes.Payload.Done, strings.Join(statusRes.Payload.LogLines, "\n"))
+
+ if statusRes.Payload.LogOffset == logOffset {
+ // pmm-managed waits up to 30 seconds for new log lines. Usually, that's more than enough for
+ // Ansible playbook to produce a new output, and that test checks that. However, our Jenkins node
+ // is very slow, so we try several times.
+ // That code should be removed once Jenkins performance is fixed.
+ t.Logf("retries = %d", retries)
+ if !statusRes.Payload.Done {
+ retries++
+ if retries < 5 {
+ assert.InDelta(t, (30 * time.Second).Seconds(), dur.Seconds(), (7 * time.Second).Seconds())
+ continue
+ }
+ }
+
+ assert.Empty(t, statusRes.Payload.LogLines, "lines should be empty for the same offset")
+ require.True(t, statusRes.Payload.Done, "lines should be empty only when done")
+ break
+ }
+
+ retries = 0
+ assert.True(t, statusRes.Payload.LogOffset > logOffset,
+ "expected statusRes.Payload.LogOffset (%d) > logOffset (%d)",
+ statusRes.Payload.LogOffset, logOffset,
+ )
+ require.NotEmpty(t, statusRes.Payload.LogLines, "pmm-managed should delay response until some lines are available")
+ logOffset = statusRes.Payload.LogOffset
+ lastLine = statusRes.Payload.LogLines[len(statusRes.Payload.LogLines)-1]
+ }
+
+ t.Logf("lastLine = %q", lastLine)
+ assert.Contains(t, lastLine, "Waiting for Grafana dashboards update to finish...")
+
+ // extra check for done
+ statusRes, err := noAuthClient.Server.UpdateStatus(&server.UpdateStatusParams{
+ Body: server.UpdateStatusBody{
+ AuthToken: authToken,
+ LogOffset: logOffset,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.True(t, statusRes.Payload.Done, "should be done")
+ assert.Empty(t, statusRes.Payload.LogLines, "lines should be empty when done")
+ assert.Equal(t, logOffset, statusRes.Payload.LogOffset)
+
+ // whole log
+ statusRes, err = noAuthClient.Server.UpdateStatus(&server.UpdateStatusParams{
+ Body: server.UpdateStatusBody{
+ AuthToken: authToken,
+ },
+ Context: pmmapitests.Context,
+ })
+ require.NoError(t, err)
+ assert.True(t, statusRes.Payload.Done, "should be done")
+ assert.Equal(t, int(logOffset), len(strings.Join(statusRes.Payload.LogLines, "\n")+"\n"))
+ assert.Equal(t, logOffset, statusRes.Payload.LogOffset)
+ lastLine = statusRes.Payload.LogLines[len(statusRes.Payload.LogLines)-1]
+ t.Logf("lastLine = %q", lastLine)
+ assert.Contains(t, lastLine, "Waiting for Grafana dashboards update to finish...")
+
+ // check that both pmm-managed and pmm-update were updated
+ version, err = serverClient.Default.Server.Version(nil)
+ require.NoError(t, err)
+ require.NotNil(t, version.Payload)
+ t.Logf("After update: %s", spew.Sdump(version.Payload))
+ assert.True(t, strings.HasPrefix(version.Payload.Managed.Version, version.Payload.Version),
+ "managed.version = %q should have %q prefix", version.Payload.Managed.Version, version.Payload.Version)
+ assert.True(t, strings.HasPrefix(version.Payload.Server.Version, version.Payload.Version),
+ "server.version = %q should have %q prefix", version.Payload.Server.Version, version.Payload.Version)
+}
diff --git a/api-tests/server/version_test.go b/api-tests/server/version_test.go
new file mode 100644
index 0000000000..f318f19f83
--- /dev/null
+++ b/api-tests/server/version_test.go
@@ -0,0 +1,92 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package server
+
+import (
+ "encoding/json"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/percona/pmm/api/serverpb/json/client/server"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ pmmapitests "github.com/percona/pmm-managed/api-tests"
+)
+
+func TestVersion(t *testing.T) {
+ paths := []string{
+ "managed/v1/version",
+ "v1/version",
+ }
+ for _, path := range paths {
+ path := path
+ t.Run(path, func(t *testing.T) {
+ t.Parallel()
+
+ uri := pmmapitests.BaseURL.ResolveReference(&url.URL{
+ Path: path,
+ })
+
+ t.Logf("URI: %s", uri)
+ resp, err := http.Get(uri.String())
+ require.NoError(t, err)
+ defer resp.Body.Close() //nolint:errcheck
+ b, err := ioutil.ReadAll(resp.Body)
+ require.NoError(t, err)
+ t.Logf("Response: %s", b)
+ assert.Equal(t, 200, resp.StatusCode)
+
+ var res server.VersionOKBody
+ err = json.Unmarshal(b, &res)
+ require.NoError(t, err)
+
+ require.True(t, strings.HasPrefix(res.Version, "2."),
+ "version = %q must have '2.' prefix for PMM 1.x's pmm-client compatibility checking", res.Version)
+
+ require.NotEmpty(t, res.Managed)
+ assert.True(t, strings.HasPrefix(res.Managed.Version, "2."),
+ "managed.version = %q must have '2.' prefix ", res.Managed.Version)
+ assert.NotEmpty(t, res.Managed.FullVersion)
+
+ // check that timestamp is not XX:00:00
+ require.NotEmpty(t, res.Managed.Timestamp)
+ ts := time.Time(res.Managed.Timestamp)
+ _, min, sec := ts.Clock()
+ assert.True(t, min != 0 || sec != 0, "managed timestamp should not contain only date: %s", ts)
+
+ if res.Server == nil || res.Server.Version == "" {
+ t.Skip("skipping the rest of the test in developer's environment")
+ }
+
+ require.NotEmpty(t, res.Server)
+ assert.True(t, strings.HasPrefix(res.Server.Version, res.Version),
+ "server.version = %q should have %q prefix", res.Server.Version, res.Version)
+ assert.NotEmpty(t, res.Server.FullVersion)
+
+ // check that timestamp is not XX:00:00
+ require.NotEmpty(t, res.Server.Timestamp)
+ ts = time.Time(res.Server.Timestamp)
+ _, min, sec = ts.Clock()
+ assert.True(t, min != 0 || sec != 0, "server timestamp should not contain only date: %s", ts)
+ })
+ }
+}
diff --git a/api-tests/testdata/ia/invalid-template.yaml b/api-tests/testdata/ia/invalid-template.yaml
new file mode 100644
index 0000000000..5cb926e9e2
--- /dev/null
+++ b/api-tests/testdata/ia/invalid-template.yaml
@@ -0,0 +1,25 @@
+---
+templates:
+ - name: %s
+ field: value # Unknown field
+ version: 1
+ summary: MySQL connections in use
+ tiers: [anonymous, registered]
+ expr: %s
+ params:
+ - name: threshold
+ summary: A percentage from configured maximum
+ unit: '%%'
+ type: float
+ range: [0, 100]
+ value: 80
+ for: 300s
+ severity: warning
+ labels:
+ foo: bar
+ annotations:
+ description: |-
+ More than [[ .threshold ]]% of MySQL connections are in use on {{ $labels.instance }}
+ VALUE = {{ $value }}
+ LABELS: {{ $labels }}
+ summary: MySQL too many
diff --git a/api-tests/testdata/ia/template.yaml b/api-tests/testdata/ia/template.yaml
new file mode 100644
index 0000000000..5ad6d74197
--- /dev/null
+++ b/api-tests/testdata/ia/template.yaml
@@ -0,0 +1,25 @@
+---
+templates:
+ - name: %s
+ version: 1
+ summary: Test summary
+ tiers: [anonymous, registered]
+ expr: %s
+ params:
+ - name: param1
+ summary: first parameter with default value and defined range
+ unit: "%s"
+ type: float
+ range: [0, 100]
+ value: 80
+ - name: param2
+ summary: second parameter without default value and defined range
+ unit: "%s"
+ type: float
+ for: 300s
+ severity: warning
+ labels:
+ foo: bar
+ annotations:
+ description: test description
+ summary: test summary
diff --git a/api-tests/tools/go.mod b/api-tests/tools/go.mod
new file mode 100644
index 0000000000..7b49db65bd
--- /dev/null
+++ b/api-tests/tools/go.mod
@@ -0,0 +1,9 @@
+module tools
+
+go 1.16
+
+require (
+ github.com/golangci/golangci-lint v1.38.0
+ github.com/jstemmer/go-junit-report v0.9.1
+ github.com/reviewdog/reviewdog v0.11.0
+)
diff --git a/api-tests/tools/go.sum b/api-tests/tools/go.sum
new file mode 100644
index 0000000000..2d6a423fcd
--- /dev/null
+++ b/api-tests/tools/go.sum
@@ -0,0 +1,947 @@
+4d63.com/gochecknoglobals v0.0.0-20201008074935-acfc0b28355a h1:wFEQiK85fRsEVF0CRrPAos5LoAryUsIX1kPW/WrIqFw=
+4d63.com/gochecknoglobals v0.0.0-20201008074935-acfc0b28355a/go.mod h1:wfdC5ZjKSPr7CybKEcgJhUOgeAQW1+7WcyK8OvUilfo=
+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=
+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.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
+cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
+cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
+cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
+cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
+cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
+cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
+cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
+cloud.google.com/go v0.70.0 h1:ujhG1RejZYi+HYfJNlgBh3j/bVKD8DewM7AkJ5UPyBc=
+cloud.google.com/go v0.70.0/go.mod h1:/UTKYRQTWjVnSe7nGvoSzxEFUELzSI/yAYd0JQT6cRo=
+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
+cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
+cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
+cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
+cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/datastore v1.1.0 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ=
+cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
+cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
+cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
+cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
+cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
+cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
+cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
+cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
+cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
+cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
+contrib.go.opencensus.io/exporter/stackdriver v0.13.4/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+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/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM=
+github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs=
+github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
+github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
+github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/OpenPeeDeeP/depguard v1.0.1 h1:VlW4R6jmBIv3/u1JNlawEvJMM4J+dPORPaZasQee8Us=
+github.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM=
+github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw=
+github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE=
+github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
+github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
+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/ashanbrown/forbidigo v1.1.0 h1:SJOPJyqsrVL3CvR0veFZFmIM0fXS/Kvyikqvfphd0Z4=
+github.com/ashanbrown/forbidigo v1.1.0/go.mod h1:vVW7PEdqEFqapJe95xHkTfB1+XvZXBFg8t0sG2FIxmI=
+github.com/ashanbrown/makezero v0.0.0-20201205152432-7b7cdbb3025a h1:/U9tbJzDRof4fOR51vwzWdIBsIH6R2yU0KG1MBRM2Js=
+github.com/ashanbrown/makezero v0.0.0-20201205152432-7b7cdbb3025a/go.mod h1:oG9Dnez7/ESBqc4EdrdNlryeo7d0KcW1ftXHm7nU/UU=
+github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
+github.com/aws/aws-sdk-go v1.30.15/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
+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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
+github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
+github.com/bkielbasa/cyclop v1.2.0 h1:7Jmnh0yL2DjKfw28p86YTd/B4lRGcNuu12sKE35sM7A=
+github.com/bkielbasa/cyclop v1.2.0/go.mod h1:qOI0yy6A7dYC4Zgsa72Ppm9kONl0RoIlPbzot9mhmeI=
+github.com/bombsimon/wsl/v3 v3.2.0 h1:x3QUbwW7tPGcCNridvqmhSRthZMTALnkg5/1J+vaUas=
+github.com/bombsimon/wsl/v3 v3.2.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc=
+github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
+github.com/bradleyfalzon/ghinstallation v1.1.1 h1:pmBXkxgM1WeF8QYvDLT5kuQiHMcmf+X015GI0KM/E3I=
+github.com/bradleyfalzon/ghinstallation v1.1.1/go.mod h1:vyCmHTciHx/uuyN82Zc3rXN3X2KTK8nUTCrTMwAhcug=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/charithe/durationcheck v0.0.6 h1:Tsy7EppNow2pDC0jN7Hsmcb6mHd71ZbI1vFissRBtc0=
+github.com/charithe/durationcheck v0.0.6/go.mod h1:SSbRIBVfMjCi/kEB6K65XEA83D6prSM8ap1UCpNKtgg=
+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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/daixiang0/gci v0.2.8 h1:1mrIGMBQsBu0P7j7m1M8Lb+ZeZxsZL+jyGX4YoMJJpg=
+github.com/daixiang0/gci v0.2.8/go.mod h1:+4dZ7TISfSmqfAGv59ePaHfNzgGtIkHAhhdKggP1JAc=
+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/denis-tingajkin/go-header v0.4.2 h1:jEeSF4sdv8/3cT/WY8AgDHUoItNSoEZ7qg9dX7pc218=
+github.com/denis-tingajkin/go-header v0.4.2/go.mod h1:eLRHAVXzE5atsKAnNRDB90WHCFFnBUn4RN0nRcs1LJA=
+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-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
+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/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/esimonov/ifshort v1.0.1 h1:p7hlWD15c9XwvwxYg3W7f7UZHmwg7l9hC0hBiF95gd0=
+github.com/esimonov/ifshort v1.0.1/go.mod h1:yZqNJUrNn20K8Q9n2CrjTKYyVEmX209Hgu+M1LBpeZE=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
+github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
+github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
+github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
+github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
+github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/fzipp/gocyclo v0.3.1 h1:A9UeX3HJSXTBzvHzhqoYVuE0eAhe+aM8XBCCwsPMZOc=
+github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
+github.com/go-critic/go-critic v0.5.4 h1:fPNMqImVjELN6Du7NVVuvKA4cgASNmc7e4zSYQCOnv8=
+github.com/go-critic/go-critic v0.5.4/go.mod h1:cjB4YGw+n/+X8gREApej7150Uyy1Tg8If6F2XOAUXNE=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
+github.com/go-sql-driver/mysql v1.4.0/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/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g=
+github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4=
+github.com/go-toolsmith/astcopy v1.0.0 h1:OMgl1b1MEpjFQ1m5ztEO06rz5CUd3oBv9RF7+DyvdG8=
+github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ=
+github.com/go-toolsmith/astequal v1.0.0 h1:4zxD8j3JRFNyLN46lodQuqz3xdKSrur7U/sr0SDS/gQ=
+github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
+github.com/go-toolsmith/astfmt v1.0.0 h1:A0vDDXt+vsvLEdbMFJAUBI/uTbRw1ffOPnxsILnFL6k=
+github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw=
+github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU=
+github.com/go-toolsmith/astp v1.0.0 h1:alXE75TXgcmupDsMK1fRAy0YUzLzqPVvBKoyWV+KPXg=
+github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI=
+github.com/go-toolsmith/pkgload v1.0.0 h1:4DFWWMXVfbcN5So1sBNW9+yeiMqLFGl1wFLTL5R0Tgg=
+github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc=
+github.com/go-toolsmith/strparse v1.0.0 h1:Vcw78DnpCAKlM20kSbAyO4mPfJn/lyYA4BJUDxe2Jb4=
+github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
+github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
+github.com/go-toolsmith/typep v1.0.2 h1:8xdsa1+FSIH/RhEkgnD1j2CJOy5mNllW1Q9tRiYwvlk=
+github.com/go-toolsmith/typep v1.0.2/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
+github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b h1:khEcpUM4yFcxg4/FHQWkvVRmgijNXRfzkIDHh23ggEo=
+github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
+github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
+github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
+github.com/gofrs/flock v0.8.0 h1:MSdYClljsF3PbENUUEx85nkWfJSGfzYI9yEBZOJz6CY=
+github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
+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/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/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-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/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/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+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/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0=
+github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4=
+github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM=
+github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk=
+github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613 h1:9kfjN3AdxcbsZBf8NjltjWihK2QfBBBZuv91cMFfDHw=
+github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8=
+github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a h1:iR3fYXUjHCR97qWS8ch1y9zPNsgXThGwjKPrYfqMPks=
+github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU=
+github.com/golangci/golangci-lint v1.38.0 h1:hgZsLRzZrjhpp44Ak+fhXNzgrbDF39ETf22a+Jd3fJQ=
+github.com/golangci/golangci-lint v1.38.0/go.mod h1:Knp/sd5ATrVp7EOzWzwIIFH+c8hUfpW+oOQb8NvdZDo=
+github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 h1:MfyDlzVjl1hoaPzPD4Gpb/QgoRfSBR0jdhwGyAWwMSA=
+github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg=
+github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA=
+github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o=
+github.com/golangci/misspell v0.3.5 h1:pLzmVdl3VxTOncgzHcvLOKirdvcx/TydsClUQXTehjo=
+github.com/golangci/misspell v0.3.5/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA=
+github.com/golangci/revgrep v0.0.0-20210208091834-cd28932614b5 h1:c9Mqqrm/Clj5biNaG7rABrmwUq88nHh0uABo2b/WYmc=
+github.com/golangci/revgrep v0.0.0-20210208091834-cd28932614b5/go.mod h1:LK+zW4MpyytAWQRz0M4xnzEk50lSvqDQKfx304apFkY=
+github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSWhqsYNgcuWO2kFlpdOZbP0+yRjmvPGys=
+github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+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/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
+github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
+github.com/google/go-github/v29 v29.0.2 h1:opYN6Wc7DOz7Ku3Oh4l7prmkOMwEcQxpFtxdU8N8Pts=
+github.com/google/go-github/v29 v29.0.2/go.mod h1:CHKiKKPHJ0REzfwc14QMklvtHwCveD0PxlMjLlzAM5E=
+github.com/google/go-github/v32 v32.1.0 h1:GWkQOdXqviCPx7Q7Fj+KyPoGm4SwHRh8rheoPhd27II=
+github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI=
+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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+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-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+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/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20201009210932-67992a1a5a35/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
+github.com/google/uuid v1.0.0/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 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/gookit/color v1.3.6/go.mod h1:R3ogXq2B9rTbXoSHJ1HyUVAZ3poOJHpd9nQmyGZsfvQ=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gordonklaus/ineffassign v0.0.0-20210225214923-2e10b2664254 h1:Nb2aRlC404yz7gQIfRZxX9/MLvQiqXyiBTJtgAy6yrI=
+github.com/gordonklaus/ineffassign v0.0.0-20210225214923-2e10b2664254/go.mod h1:M9mZEtGIsR1oDaZagNPNG9iq9n2HrhZ17dsXk73V3Lw=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
+github.com/gostaticanalysis/analysisutil v0.0.3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
+github.com/gostaticanalysis/analysisutil v0.1.0/go.mod h1:dMhHRU9KTiDcuLGdy87/2gTR8WruwYZrKdRq9m1O6uw=
+github.com/gostaticanalysis/analysisutil v0.4.1 h1:/7clKqrVfiVwiBQLM0Uke4KvXnO6JcCTS7HwF2D6wG8=
+github.com/gostaticanalysis/analysisutil v0.4.1/go.mod h1:18U/DLpRgIUd459wGxVHE0fRgmo1UgHDcbw7F5idXu0=
+github.com/gostaticanalysis/comment v1.3.0/go.mod h1:xMicKDx7XRXYdVwY9f9wQpDJVnqWxw9wCauCMKp+IBI=
+github.com/gostaticanalysis/comment v1.4.1 h1:xHopR5L2lRz6OsjH4R2HG5wRhW9ySl3FsHIvi5pcXwc=
+github.com/gostaticanalysis/comment v1.4.1/go.mod h1:ih6ZxzTHLdadaiSnF5WY3dxUoXfXAlTaRzuaNDlSado=
+github.com/gostaticanalysis/forcetypeassert v0.0.0-20200621232751-01d4955beaa5 h1:rx8127mFPqXXsfPSo8BwnIU97MKFZc89WHAHt8PwDVY=
+github.com/gostaticanalysis/forcetypeassert v0.0.0-20200621232751-01d4955beaa5/go.mod h1:qZEedyP/sY1lTGV1uJ3VhWZ2mqag3IkWsDHVbplHXak=
+github.com/gostaticanalysis/nilerr v0.1.1 h1:ThE+hJP0fEp4zWLkWHWcRyI2Od0p7DlgYG3Uqrmrcpk=
+github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW0HU0GPE3+5PWN4A=
+github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+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 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
+github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
+github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
+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-retryablehttp v0.6.4 h1:BbgctKO892xEyOXnGiaAwIoSq1QZ/SS4AhjoAh9DnfY=
+github.com/hashicorp/go-retryablehttp v0.6.4/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
+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-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/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+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/haya14busa/go-actions-toolkit v0.0.0-20200105081403-ca0307860f01 h1:HiJF8Mek+I7PY0Bm+SuhkwaAZSZP83sw6rrTMrgZ0io=
+github.com/haya14busa/go-actions-toolkit v0.0.0-20200105081403-ca0307860f01/go.mod h1:1DWDZmeYf0LX30zscWb7K9rUMeirNeBMd5Dum+seUhc=
+github.com/haya14busa/go-checkstyle v0.0.0-20170303121022-5e9d09f51fa1/go.mod h1:RsN5RGgVYeXpcXNtWyztD5VIe7VNSEqpJvF2iEH7QvI=
+github.com/haya14busa/go-sarif v0.0.0-20200721090635-d2343efc5d00/go.mod h1:1Hkn3JseGMB/hv1ywzkapVQDWV3bFgp6POZobZmR/5g=
+github.com/haya14busa/secretbox v0.0.0-20180525171038-07c7ecf409f5/go.mod h1:FGO/dXIFZnan7KvvUSFk1hYMnoVNzB6NTMPrmke8SSI=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
+github.com/jgautheron/goconst v1.4.0 h1:hp9XKUpe/MPyDamUbfsrGpe+3dnY2whNK4EtB86dvLM=
+github.com/jgautheron/goconst v1.4.0/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4=
+github.com/jingyugao/rowserrcheck v0.0.0-20210130005344-c6a0c12dd98d h1:BYDZtm80MLJpTWalkwHxNnIbO/2akQHERcfLq4TbIWE=
+github.com/jingyugao/rowserrcheck v0.0.0-20210130005344-c6a0c12dd98d/go.mod h1:/EZlaYCnEX24i7qdVhT9du5JrtFWYRQr67bVgR7JJC8=
+github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af h1:KA9BjwUk7KlCh6S9EAGWBt1oExIUv9WyNCiRz5amv48=
+github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0=
+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
+github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
+github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+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/julz/importas v0.0.0-20210226073942-60b4fa260dd0 h1:exZBMUS/kB/AhxSj/9lIIxhqkCpXXdKScjFWQUTbi3M=
+github.com/julz/importas v0.0.0-20210226073942-60b4fa260dd0/go.mod h1:oSFU2R4XK/P7kNBrnL/FEQlDGN1/6WoxXEjSSXO0DV0=
+github.com/justinas/nosurf v1.1.1/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ=
+github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
+github.com/kisielk/errcheck v1.6.0 h1:YTDO4pNy7AUN/021p+JGHycQyYNIyMoenM1YDVK6RlY=
+github.com/kisielk/errcheck v1.6.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+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.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kulti/thelper v0.4.0 h1:2Nx7XbdbE/BYZeoip2mURKUdtHQRuy6Ug+wR7K9ywNM=
+github.com/kulti/thelper v0.4.0/go.mod h1:vMu2Cizjy/grP+jmsvOFDx1kYP6+PD1lqg4Yu5exl2U=
+github.com/kunwardeep/paralleltest v1.0.2 h1:/jJRv0TiqPoEy/Y8dQxCFJhD56uS/pnvtatgTZBHokU=
+github.com/kunwardeep/paralleltest v1.0.2/go.mod h1:ZPqNm1fVHPllh5LPVujzbVz1JN2GhLxSfY+oqUsvG30=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/kyoh86/exportloopref v0.1.8 h1:5Ry/at+eFdkX9Vsdw3qU4YkvGtzuVfzT4X7S77LoN/M=
+github.com/kyoh86/exportloopref v0.1.8/go.mod h1:1tUcJeiioIs7VWe5gcOObrux3lb66+sBqGZrRkMwPgg=
+github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
+github.com/magefile/mage v1.10.0 h1:3HiXzCUY12kh9bIuyXShaVe529fJfyqoVM42o/uom2g=
+github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
+github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
+github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/maratori/testpackage v1.0.1 h1:QtJ5ZjqapShm0w5DosRjg0PRlSdAdlx+W6cCKoALdbQ=
+github.com/maratori/testpackage v1.0.1/go.mod h1:ddKdw+XG0Phzhx8BFDTKgpWP4i7MpApTE5fXSKAqwDU=
+github.com/matoous/godox v0.0.0-20210227103229-6504466cf951 h1:pWxk9e//NbPwfxat7RXkts09K+dEBJWakUWwICVqYbA=
+github.com/matoous/godox v0.0.0-20210227103229-6504466cf951/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
+github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+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.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
+github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
+github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw=
+github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
+github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/mbilski/exhaustivestruct v1.2.0 h1:wCBmUnSYufAHO6J4AVWY6ff+oxWxsVFrwgOdMUQePUo=
+github.com/mbilski/exhaustivestruct v1.2.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc=
+github.com/mgechev/dots v0.0.0-20190921121421-c36f7dcfbb81 h1:QASJXOGm2RZ5Ardbc86qNFvby9AqkLDibfChMtAg5QM=
+github.com/mgechev/dots v0.0.0-20190921121421-c36f7dcfbb81/go.mod h1:KQ7+USdGKfpPjXk4Ga+5XxQM4Lm4e3gAogrreFAYpOg=
+github.com/mgechev/revive v1.0.3 h1:z3FL6IFFN3JKzHYHD8O1ExH9g/4lAGJ5x1+9rPZgsFg=
+github.com/mgechev/revive v1.0.3/go.mod h1:POGGZagSo/0frdr7VeAifzS5Uka0d0GPiM35MsTO8nE=
+github.com/miekg/dns v1.0.14/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-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
+github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
+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-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/moricho/tparallel v0.2.1 h1:95FytivzT6rYzdJLdtfn6m1bfFJylOJK41+lgv/EHf4=
+github.com/moricho/tparallel v0.2.1/go.mod h1:fXEIZxG2vdfl0ZF8b42f5a78EhjjD5mX8qUplsoSU4k=
+github.com/mozilla/tls-observatory v0.0.0-20201209171846-0547674fceff/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/nakabonne/nestif v0.3.0 h1:+yOViDGhg8ygGrmII72nV9B/zGxY188TYpfolntsaPw=
+github.com/nakabonne/nestif v0.3.0/go.mod h1:dI314BppzXjJ4HsCnbo7XzrJHPszZsjnk5wEBSYHI2c=
+github.com/nbutton23/zxcvbn-go v0.0.0-20201221231540-e56b841a3c88 h1:o+O3Cd1HO9CTgxE3/C8p5I5Y4C0yYWbF8d4IkfOLtcQ=
+github.com/nbutton23/zxcvbn-go v0.0.0-20201221231540-e56b841a3c88/go.mod h1:KSVJerMDfblTH7p5MZaTt+8zaT2iEk3AkVb9PQdZuE8=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/nishanths/exhaustive v0.1.0 h1:kVlMw8h2LHPMGUVqUj6230oQjjTMFjwcZrnkhXzFfl8=
+github.com/nishanths/exhaustive v0.1.0/go.mod h1:S1j9110vxV1ECdCudXRkeMnFQ/DQk9ajLT0Uf2MYZQQ=
+github.com/nishanths/predeclared v0.2.1 h1:1TXtjmy4f3YCFjTxRd8zcFHOmoUir+gp0ESzjFzG2sw=
+github.com/nishanths/predeclared v0.2.1/go.mod h1:HvkGJcA3naj4lOwnFXFDkFxVtSqQMB9sbB1usJ+xjQE=
+github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
+github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
+github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M=
+github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/onsi/gomega v1.10.4 h1:NiTx7EEvBzu9sFOD1zORteLSt3o8gnlvZZwSE9TnY9U=
+github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ=
+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/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d h1:CdDQnGF8Nq9ocOS/xlSptM1N3BbrA6/kmaep5ggwaIA=
+github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d/go.mod h1:3OzsM7FXDQlpCiw2j81fOmAwQLnZnLGXVKUzeKQXIAw=
+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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/polyfloyd/go-errorlint v0.0.0-20201127212506-19bd8db6546f h1:xAw10KgJqG5NJDfmRqJ05Z0IFblKumjtMeyiOLxj3+4=
+github.com/polyfloyd/go-errorlint v0.0.0-20201127212506-19bd8db6546f/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw=
+github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
+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/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
+github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI=
+github.com/quasilyte/go-ruleguard v0.3.0 h1:A3OfpsK2ynOTbz/KMi62qWzignjGCOZVChATSf4P+A0=
+github.com/quasilyte/go-ruleguard v0.3.0/go.mod h1:p2miAhLp6fERzFNbcuQ4bevXs8rgK//uCHsUDkumITg=
+github.com/quasilyte/go-ruleguard/dsl v0.0.0-20210106184943-e47d54850b18/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
+github.com/quasilyte/go-ruleguard/dsl v0.0.0-20210115110123-c73ee1cbff1f/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
+github.com/quasilyte/go-ruleguard/rules v0.0.0-20201231183845-9e62ed36efe1/go.mod h1:7JTjp89EGyU1d6XfBiXihJNG37wB2VRkd125Q1u7Plc=
+github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95 h1:L8QM9bvf68pVdQ3bCFZMDmnt9yqcMBro1pC7F+IPYMY=
+github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0=
+github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc=
+github.com/reviewdog/errorformat v0.0.0-20201020160743-a656ed371170 h1:mXnYP0Ar2dpAorxqoIERETwelsF8jkOR3BWPU4ByTH4=
+github.com/reviewdog/errorformat v0.0.0-20201020160743-a656ed371170/go.mod h1:Akd5vemrJaAHgnEOFrC4yMbEKaOsddwF1LKkfovSFI8=
+github.com/reviewdog/go-bitbucket v0.0.0-20201024094602-708c3f6a7de0 h1:XZ60Bp2UqwaJ6fDQExoFVrgs4nIzwBCy9ct6GCj9hH8=
+github.com/reviewdog/go-bitbucket v0.0.0-20201024094602-708c3f6a7de0/go.mod h1:5JbWAMFyq9hbISZawRyIe7QTcLaptvCIvmZnYo+1VvA=
+github.com/reviewdog/reviewdog v0.11.0 h1:C54aH7Tx6vHpELvWm/4wR2vSLYHgfIWzdAsyomvooAI=
+github.com/reviewdog/reviewdog v0.11.0/go.mod h1:8rqSsvh/kWI4TEe+4niTzA94XQ/wQNTZknhFePZozJQ=
+github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/ryancurrah/gomodguard v1.2.0 h1:YWfhGOrXwLGiqcC/u5EqG6YeS8nh+1fw0HEc85CVZro=
+github.com/ryancurrah/gomodguard v1.2.0/go.mod h1:rNqbC4TOIdUDcVMSIpNNAzTbzXAZa6W5lnUepvuMMgQ=
+github.com/ryanrolds/sqlclosecheck v0.3.0 h1:AZx+Bixh8zdUBxUA1NxbxVAS78vTPq4rCb8OUZI9xFw=
+github.com/ryanrolds/sqlclosecheck v0.3.0/go.mod h1:1gREqxyTGR3lVtpngyFo3hZAgk0KCtEdgEkHwDbigdA=
+github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
+github.com/sanposhiho/wastedassign v0.1.3 h1:qIMpTh4NGZYRbFJ+DSpLoVn8F4SLciX2afRvXPefC7w=
+github.com/sanposhiho/wastedassign v0.1.3/go.mod h1:LGpq5Hsv74QaqM47WtIsRSF/ik9kqk07kchgv66tLVE=
+github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+github.com/securego/gosec/v2 v2.6.1 h1:+KCw+uz16FYfFyJ/A5aU6uP7mnrL+j1TbDnk1yN+8R0=
+github.com/securego/gosec/v2 v2.6.1/go.mod h1:I76p3NTHBXsGhybUW+cEQ692q2Vp+A0Z6ZLzDIZy+Ao=
+github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c h1:W65qqJCIOVP4jpqPQ0YvHYKwcMEMVWIzWC5iNQQfBTU=
+github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c/go.mod h1:/PevMnwAxekIXwN8qQyfc5gl2NlkB3CQlkizAbOkeBs=
+github.com/shirou/gopsutil/v3 v3.21.1/go.mod h1:igHnfak0qnw1biGeI2qKQvu0ZkwvEkUcCLlYhZzdr/4=
+github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
+github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
+github.com/sirupsen/logrus v1.8.0 h1:nfhvjKcUMhBMVqbKHJlk5RPrrfYr/NMo3692g0dwfWU=
+github.com/sirupsen/logrus v1.8.0/go.mod h1:4GuYW9TZmE769R5STWrRakJc4UqQ3+QQ95fyz7ENv1A=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+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/sonatard/noctx v0.0.1 h1:VC1Qhl6Oxx9vvWo3UDgrGXYCeKCe3Wbw7qAWL6FrmTY=
+github.com/sonatard/noctx v0.0.1/go.mod h1:9D2D/EoULe8Yy2joDHJj7bv3sZoq9AaSb8B4lqBjiZI=
+github.com/sourcegraph/go-diff v0.6.1 h1:hmA1LzxW0n1c3Q4YbrFgg4P99GSnebYa3x8gr0HZqLQ=
+github.com/sourcegraph/go-diff v0.6.1/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M=
+github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
+github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+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/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
+github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
+github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
+github.com/ssgreg/nlreturn/v2 v2.1.0 h1:6/s4Rc49L6Uo6RLjhWZGBpWWjfzk2yrf1nIW8m4wgVA=
+github.com/ssgreg/nlreturn/v2 v2.1.0/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.1.4/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/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
+github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
+github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
+github.com/tdakkota/asciicheck v0.0.0-20200416200610-e657995f937b h1:HxLVTlqcHhFAz3nWUcuvpH7WuOMv8LQoCWmruLfFH2U=
+github.com/tdakkota/asciicheck v0.0.0-20200416200610-e657995f937b/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM=
+github.com/tetafro/godot v1.4.4 h1:VAtLEoAMmopIzHVWVBrztjVWDeYm1OD/DKqhqXR4828=
+github.com/tetafro/godot v1.4.4/go.mod h1:FVDd4JuKliW3UgjswZfJfHq4vAx0bD/Jd5brJjGeaz4=
+github.com/timakin/bodyclose v0.0.0-20200424151742-cb6215831a94 h1:ig99OeTyDwQWhPe2iw9lwfQVF1KB3Q4fpP3X7/2VBG8=
+github.com/timakin/bodyclose v0.0.0-20200424151742-cb6215831a94/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/tomarrell/wrapcheck v0.0.0-20201130113247-1683564d9756 h1:zV5mu0ESwb+WnzqVaW2z1DdbAP0S46UtjY8DHQupQP4=
+github.com/tomarrell/wrapcheck v0.0.0-20201130113247-1683564d9756/go.mod h1:yiFB6fFoV7saXirUGfuK+cPtUh4NX/Hf5y2WC2lehu0=
+github.com/tommy-muehle/go-mnd/v2 v2.3.1 h1:a1S4+4HSXDJMgeODJH/t0EEKxcVla6Tasw+Zx9JJMog=
+github.com/tommy-muehle/go-mnd/v2 v2.3.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw=
+github.com/ultraware/funlen v0.0.3 h1:5ylVWm8wsNwH5aWo9438pwvsK0QiqVuUrt9bn7S/iLA=
+github.com/ultraware/funlen v0.0.3/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA=
+github.com/ultraware/whitespace v0.0.4 h1:If7Va4cM03mpgrNH9k49/VOicWpGoG70XPBFFODYDsg=
+github.com/ultraware/whitespace v0.0.4/go.mod h1:aVMh/gQve5Maj9hQ/hg+F75lr/X5A89uZnzAmWSineA=
+github.com/uudashr/gocognit v1.0.1 h1:MoG2fZ0b/Eo7NXoIwCVFLG5JED3qgQz5/NEE+rOsjPs=
+github.com/uudashr/gocognit v1.0.1/go.mod h1:j44Ayx2KW4+oB6SWMv8KsmHzZrOInQav7D3cQMJ5JUM=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasthttp v1.16.0/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA=
+github.com/valyala/quicktemplate v1.6.3/go.mod h1:fwPzK2fHuYEODzJ9pkw0ipCPNHZ2tD5KW4lOuSdPKzY=
+github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
+github.com/vvakame/sdlog v0.0.0-20200409072131-7c0d359efddc h1:El7LEavRpa49dYFE9ezO8aQxQn5E7u7eQkFsaXsoQAY=
+github.com/vvakame/sdlog v0.0.0-20200409072131-7c0d359efddc/go.mod h1:MmhrKtbECoUJTctfak+MnOFoJ9XQqYZ7chcwV9O7v3I=
+github.com/xanzy/go-gitlab v0.38.2 h1:FF4WgwFsLfOC4Wl67c9UDIC73C+UaYJ0pkZ2irbSu4M=
+github.com/xanzy/go-gitlab v0.38.2/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+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/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=
+go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
+go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
+golang.org/x/build v0.0.0-20200616162219-07bebbe343e9 h1:SgmspiKqqI4Du0T87bPBEezUSzVOKhKDgconpLrfyuc=
+golang.org/x/build v0.0.0-20200616162219-07bebbe343e9/go.mod h1:ia5pRNoJUuxRhXkmwkySu4YBTbXHSKig2ie6daQXihg=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/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-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
+golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
+golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+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/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
+golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+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.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.0 h1:8pl+sMODzuvGJkmj2W4kZihvVb5mKm8pB/X44PIQHv8=
+golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+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-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-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-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-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/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-20200222125558-5a598a2470a0/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/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201024042810-be3efd7ff127/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U=
+golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+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-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 h1:ld7aEMNHoBnnDAX15v1T6z31v8HwR2A9FYOuAhWqkwc=
+golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
+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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+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-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-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-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/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-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201024232916-9f70ab9862d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/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.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+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-20180525024113-a5b4c53f6e8b/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-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190307163923-6a08e3108db3/go.mod h1:25r3+/G6/xytQM8iWZKq3Hn0kr0rgFKPUNVEL/dr3z4=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/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-20190321232350-e250d351ecad/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/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-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-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-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578/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-20190916130336-e45ffcd953cc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191010075000-0337d82405ff/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-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/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-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200117220505-0cba7a3a9ee9/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/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-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200324003944-a576cf524670/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
+golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
+golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
+golang.org/x/tools v0.0.0-20200406213809-066fd1390ee0/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200414032229-332987a829c3/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200422022333-3d57cf2e726e/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200622203043-20e05c1c8ffa/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200624225443-88f3c62a19ff/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200625211823-6506e20df31f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200812195022-5ae4c3c160a0/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200831203904-5a2aa26beb65/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
+golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
+golang.org/x/tools v0.0.0-20201001104356-43ebab892c4c/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
+golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
+golang.org/x/tools v0.0.0-20201011145850-ed2f50202694/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
+golang.org/x/tools v0.0.0-20201017001424-6003fad69a88/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
+golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201028025901-8cd080b735b3/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201114224030-61ea331ec02b/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201118003311-bd56c0adb394/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201230224404-63754364767c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210101214203-2dba1e4ea05c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210102185154-773b96fafca2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210104081019-d8d6ddbec6ee/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
+golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.10.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.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
+google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
+google.golang.org/api v0.33.0 h1:+gL0XvACeMIvpwLZ5rQZzLn5cwOsgg8dIcfJ2SYfBVw=
+google.golang.org/api v0.33.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+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.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
+google.golang.org/appengine v1.6.6/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-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-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-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
+google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
+google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154 h1:bFFRpT+e8JJVY7lMMfvezL1ZIwqiwmPl2bsE2yx4HqM=
+google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+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.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+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.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/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
+google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
+google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.32.0 h1:zWTV+LMdc3kaiJMSTOFz2UgSBgx8RNQoTGiZu3fR9S0=
+google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
+google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+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/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
+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/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.1.2 h1:SMdYLJl312RXuxXziCCHhRsp/tvct9cGKey0yv95tZM=
+honnef.co/go/tools v0.1.2/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
+mvdan.cc/gofumpt v0.1.0 h1:hsVv+Y9UsZ/mFZTxJZuHVI6shSQCtzZ11h1JEFPAZLw=
+mvdan.cc/gofumpt v0.1.0/go.mod h1:yXG1r1WqZVKWbVRtBWKWX9+CxGYfA51nSomhM0woR48=
+mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I=
+mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc=
+mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo=
+mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
+mvdan.cc/unparam v0.0.0-20210104141923-aac4ce9116a7 h1:HT3e4Krq+IE44tiN36RvVEb6tvqeIdtsVSsxmNPqlFU=
+mvdan.cc/unparam v0.0.0-20210104141923-aac4ce9116a7/go.mod h1:hBpJkZE8H/sb+VRFvw2+rBpHNsTBcvSpk61hr8mzXZE=
+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
diff --git a/api-tests/tools/tools.go b/api-tests/tools/tools.go
new file mode 100644
index 0000000000..4ad6d529b6
--- /dev/null
+++ b/api-tests/tools/tools.go
@@ -0,0 +1,27 @@
+// pmm-managed
+// Copyright (C) 2017 Percona LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+//go:build tools
+// +build tools
+
+package tools
+
+import (
+ _ "github.com/golangci/golangci-lint/cmd/golangci-lint"
+ _ "github.com/jstemmer/go-junit-report"
+ _ "github.com/reviewdog/reviewdog/cmd/reviewdog"
+ _ "golang.org/x/tools/cmd/goimports"
+)