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 + +[![Build Status](https://travis-ci.com/Percona-Lab/pmm-api-tests.svg?branch=master)](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" +)